Skip to content

Commit

Permalink
actions: Add ledger backend functions.
Browse files Browse the repository at this point in the history
  • Loading branch information
JoeGruffins committed Jun 1, 2023
1 parent 587d099 commit afece2a
Show file tree
Hide file tree
Showing 14 changed files with 747 additions and 16 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
*~
app/dist/
app/prebuilds/
app/dist-trezor/
app/main.js
app/main.js.map
app/main.js.LICENSE.txt
app/*.node
build/*.node
node_modules/
npm-debug.log
public/js/bundle.js
Expand Down
166 changes: 166 additions & 0 deletions app/actions/LedgerActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { ledgerTransport } from "wallet-preload-shim";
import * as ledgerHelpers from "../helpers/ledger";
import { wallet } from "wallet-preload-shim";
import { publishTransactionAttempt } from "./ControlActions";
import { hexToBytes } from "helpers";
import {
SIGNTX_ATTEMPT,
SIGNTX_FAILED,
SIGNTX_SUCCESS
} from "./ControlActions";

import * as selectors from "selectors";
import * as cfgConstants from "constants/config";

export const LDG_LEDGER_ENABLED = "LDG_LEDGER_ENABLED";
export const LDG_WALLET_CLOSED = "LDG_WALLET_CLOSED";

// This is an error's message when an app is open but we are trying to get
// device info.
// const DEVICE_ON_DASHBOARD_EXPECTED = "DeviceOnDashboardExpected";

// enableLedger only sets a value in the config. Ledger connections are made
// per action then dropped.
export const enableLedger = () => (dispatch, getState) => {
const walletName = selectors.getWalletName(getState());

if (walletName) {
const config = wallet.getWalletCfg(
selectors.isTestNet(getState()),
walletName
);
config.set(cfgConstants.LEDGER, true);
}

dispatch({ type: LDG_LEDGER_ENABLED });

connect()(dispatch, getState);
};

export const LDG_CONNECT_ATTEMPT = "LDG_CONNECT_ATTEMPT";
export const LDG_CONNECT_FAILED = "LDG_CONNECT_FAILED";
export const LDG_CONNECT_SUCCESS = "LDG_CONNECT_SUCCESS";

export const connect = () => async (dispatch/*, getState*/) => {
dispatch({ type: LDG_CONNECT_ATTEMPT });
try {
await ledgerTransport.doWithLedger("connect");
} catch (error) {
dispatch({ type: LDG_CONNECT_FAILED });
throw error;
}
dispatch({ type: LDG_CONNECT_SUCCESS });
};

export const LDG_LEDGER_DISABLED = "LDG_LEDGER_DISABLED";

// disableLedger disables ledger integration for the current wallet. Note
// that it does **not** disable in the config, so the wallet will restart as a
// ledger wallet next time it's opened.
export const disableLedger = () => (dispatch) => {
dispatch({ type: LDG_LEDGER_DISABLED });
};

export const LDG_NOCONNECTEDDEVICE = "LDG_NOCONNECTEDDEVICE";

export const alertNoConnectedDevice = () => (dispatch) => {
dispatch({ type: LDG_NOCONNECTEDDEVICE });
};

// checkLedgerIsDcrwallet verifies whether the wallet currently running on
// dcrwallet (presumably a watch only wallet created from a ledger provided
// xpub) is the same wallet as the one of the currently connected ledger. This
// function throws an error if they are not the same.
// This is useful for making sure, prior to performing some wallet related
// function such as transaction signing, that ledger will correctly perform the
// operation.
// Note that this might trigger pin/passphrase modals, depending on the current
// ledger configuration.
// The way the check is performed is by generating the first address from the
// ledger wallet and then validating this address agains dcrwallet, ensuring
// this is an owned address at the appropriate branch/index.
// This check is only valid for a single session (ie, a single execution of
// `deviceRun`) as the physical device might change between sessions.
const checkLedgerIsDcrwallet = () => async (dispatch, getState) => {
const {
grpc: { walletService }
} = getState();

const path = ledgerHelpers.addressPath(0, 0);
const payload = await ledgerTransport.doWithLedger("getaddress", path);
const addr = payload.bitcoinAddress;

const addrValidResp = await wallet.validateAddress(walletService, addr);
if (!addrValidResp.isValid)
throw "Ledger provided an invalid address " + addr;

if (!addrValidResp.isMine)
throw "Ledger and dcrwallet not running from the same extended public key";

if (addrValidResp.index !== 0) throw "Wallet replied with wrong index.";
};

export const signTransactionAttemptLedger =
(rawUnsigTx) => async (dispatch, getState) => {
dispatch({ type: SIGNTX_ATTEMPT });

const {
grpc: { walletService }
} = getState();
const chainParams = selectors.chainParams(getState());

try {
const arg = await ledgerHelpers.signArg(
rawUnsigTx,
chainParams,
walletService,
dispatch
);

await dispatch(checkLedgerIsDcrwallet());
const signedRaw = await ledgerTransport.doWithLedger(
"createtransaction",
arg
);
if (signedRaw.message) {
throw signedRaw.message;
}

dispatch({ type: SIGNTX_SUCCESS });
dispatch(publishTransactionAttempt(hexToBytes(signedRaw)));
} catch (error) {
dispatch({ error, type: SIGNTX_FAILED });
}
};

export const LDG_GETWALLETCREATIONMASTERPUBKEY_ATTEMPT =
"LDG_GETWALLETCREATIONMASTERPUBKEY_ATTEMPT";
export const LDG_GETWALLETCREATIONMASTERPUBKEY_FAILED =
"LDG_GETWALLETCREATIONMASTERPUBKEY_FAILED";
export const LDG_GETWALLETCREATIONMASTERPUBKEY_SUCCESS =
"LDG_GETWALLETCREATIONMASTERPUBKEY_SUCCESS";

export const getWalletCreationMasterPubKey =
() => async (dispatch/*, getState*/) => {
dispatch({ type: LDG_GETWALLETCREATIONMASTERPUBKEY_ATTEMPT });
// TODO: Enable on mainnet.
const isTestnet = true;
try {
const payload = await ledgerTransport.doWithLedger(
"getaddress",
"44'/42'/0'"
);
const hdpk = ledgerHelpers.pubkeyToHDPubkey(
payload.publicKey,
payload.chainCode,
isTestnet
);

dispatch({ type: LDG_GETWALLETCREATIONMASTERPUBKEY_SUCCESS });

return hdpk;
} catch (error) {
dispatch({ error, type: LDG_GETWALLETCREATIONMASTERPUBKEY_FAILED });
throw error;
}
};
175 changes: 175 additions & 0 deletions app/helpers/ledger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { wallet } from "wallet-preload-shim";
import { hexToBytes, strHashToRaw } from "helpers";
import * as secp256k1 from "secp256k1";
import { default as blake } from "blake-hash";
import * as bs58 from "bs58";
import toBuffer from "typedarray-to-buffer";
import { getTxFromInputs } from "../actions/TransactionActions";

export function addressPath(branch, index) {
const prefix = "44'/42'/0'/";
const i = (index || 0).toString();
const b = (branch || 0).toString();
return prefix + b + "/" + i;
}

export function pubkeyToHDPubkey(pubkey, chainCode, isTestnet) {
const pk = secp256k1.publicKeyConvert(hexToBytes(pubkey), true); // from uncompressed to compressed
const cc = hexToBytes(chainCode);
let hdPublicKeyID = hexToBytes("02fda926"); // dpub
if (isTestnet) {
hdPublicKeyID = hexToBytes("043587d1"); // tpub
}
const parentFP = hexToBytes("00000000"); // not true but we dont know the fingerprint
const childNum = hexToBytes("80000000"); // always first hardened child
const depth = 2; // account is depth 2
const buff = new Uint8Array(78); // 4 network identifier + 1 depth + 4 parent fingerprint + 4 child number + 32 chain code + 33 compressed public key
let i = 0;
buff.set(hdPublicKeyID, i);
i += 4;
buff[i] = depth;
i += 1;
buff.set(parentFP, i);
i += 4;
buff.set(childNum, i);
i += 4;
buff.set(cc, i);
i += 32;
buff.set(pk, i);
const firstPass = blake("blake256").update(Buffer.from(buff)).digest();
const secondPass = blake("blake256").update(firstPass).digest();
const fullSerialize = Buffer.concat([
Buffer.from(buff),
secondPass.slice(0, 4)
]);
return bs58.encode(fullSerialize);
}

function writeUint16LE(n) {
const buff = new Buffer(2);
buff.writeUInt16LE(n, 0);
return buff;
}

function writeUint32LE(n) {
const buff = new Buffer(4);
buff.writeUInt32LE(n, 0);
return buff;
}

function writeUint64LE(n) {
const buff = new Buffer(8);
const lower = 0xffffffff & n;
// bitshift right (>>>) does not seem to throw away the lower half, so
// subtracting.
const upper = n - lower;
buff.writeUInt32LE(lower, 0);
buff.writeUInt32LE(upper, 4);
return buff;
}

function inputToTx(tx) {
const inputs = [];
for (const inp of tx.inputs) {
const sequence = writeUint32LE(inp.sequence);
const tree = new Uint8Array(1);
tree[0] = inp.outputTree;
const prevout = new Uint8Array(36);
prevout.set(strHashToRaw(inp.prevTxId), 0);
prevout.set(writeUint32LE(inp.outputIndex), 32);
const input = {
prevout: toBuffer(prevout),
script: toBuffer(new Uint8Array(25)),
sequence: sequence,
tree: toBuffer(tree)
};
inputs.push(input);
}
const outputs = [];
for (const out of tx.outputs) {
const output = {
amount: writeUint64LE(out.value),
script: out.script
};
outputs.push(output);
}
return {
version: writeUint32LE(tx.version), // Pretty sure this is a uint16 but ledger does not want that.
inputs: inputs,
outputs: outputs,
locktime: writeUint32LE(tx.lockTime),
nExpiryHeight: writeUint32LE(tx.expiry)
};
}

function createPrefix(tx) {
const numOuts = tx.outputs.length;
if (numOuts > 2) {
throw "more than two outputs is not expected";
}
let buffLen = 1;
for (const out of tx.outputs) {
buffLen += 11 + out.script.length;
}
const buff = new Uint8Array(buffLen); // 1 varInt + ( 8 value + 2 tx version + 1 varInt + (23/25?) variable script length) * number of outputs
let i = 0;
buff[i] = numOuts;
i += 1;
for (const out of tx.outputs) {
buff.set(writeUint64LE(out.value), i);
i += 8;
buff.set(writeUint16LE(out.version), i);
i += 2;
// TODO: Clean this up for production? Should use smarter logic to get varInts.
buff[i] = out.script.length; // varInt for 23/25 bytes
i += 1;
buff.set(out.script, i);
i += out.script.length;
}
return toBuffer(buff);
}

export async function signArg(txHex, chainParams, walletService, dispatch) {
const tx = await wallet.decodeTransactionLocal(txHex, chainParams);
const inputTxs = await dispatch(getTxFromInputs(tx));
const inputs = [];
const paths = [];
let i = 0;
for (const inp of inputTxs) {
const prevOut = inputToTx(inp);
const idx = tx.inputs[i].outputIndex;
inputs.push([prevOut, idx]);
const addr = inp.outputs[idx].decodedScript.address;
const val = await wallet.validateAddress(walletService, addr);
const acct = val.accountNumber.toString();
const branch = val.isInternal ? "1" : "0";
const index = val.index.toString();
paths.push("44'/42'/" + acct + "'/" + branch + "/" + index);
i++;
}
let changePath = null;
for (const out of tx.outputs) {
const addr = out.decodedScript.address;
const val = await wallet.validateAddress(walletService, addr);
if (!val.isInternal) {
continue;
} // assume the internal address is change
const acct = val.accountNumber.toString();
const index = val.index.toString();
changePath = "44'/42'/" + acct + "'/1/" + index;
break;
}

return {
inputs: inputs,
associatedKeysets: paths,
changePath: changePath,
outputScriptHex: createPrefix(tx),
lockTime: tx.lockTime,
sigHashType: 1, // SIGHASH_ALL
segwit: false,
expiryHeight: writeUint32LE(tx.expiry),
useTrustedInputForSegwit: false,
additionals: ["decred"]
};
}
3 changes: 3 additions & 0 deletions app/ledger/transport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { invocable } from "helpers/electronRenderer";

export const doWithLedger = invocable("do-with-ledger");
3 changes: 3 additions & 0 deletions app/main.development.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ import {
logoutDex,
exportSeed
} from "./main_dev/ipc";
import { doWithLedger } from "./main_dev/ledger";
import {
initTemplate,
getVersionWin,
Expand Down Expand Up @@ -885,3 +886,5 @@ app.on("before-quit", async (event) => {
}
app.exit(0);
});

handle("do-with-ledger", doWithLedger);
2 changes: 1 addition & 1 deletion app/main_dev/externalRequests.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export const installSessionHandlers = (mainLogger) => {
);
callback({ cancel: true, requestHeaders: details.requestHeaders });
} else {
//logger.log("verbose", details.method + " " + details.url);
// logger.log("verbose", details.method + " " + details.url);
if (
allowedExternalRequests[EXTERNALREQUEST_TREZOR_BRIDGE] &&
/^http:\/\/127.0.0.1:21325\//.test(details.url)
Expand Down
2 changes: 2 additions & 0 deletions app/main_dev/ipc.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const getAvailableWallets = (network) => {
const lastAccess = cfg.get(cfgConstants.LAST_ACCESS);
const isWatchingOnly = cfg.get(cfgConstants.IS_WATCH_ONLY);
const isTrezor = cfg.get(cfgConstants.TREZOR);
const isLedger = cfg.get(cfgConstants.LEDGER);
const isPrivacy = cfg.get(cfgConstants.MIXED_ACCOUNT_CFG);
const walletDbFilePath = getWalletDb(isTestNet, wallet);
const finished = fs.existsSync(walletDbFilePath);
Expand All @@ -68,6 +69,7 @@ export const getAvailableWallets = (network) => {
lastAccess,
isWatchingOnly,
isTrezor,
isLedger,
isPrivacy,
isLN,
displayWalletGradient
Expand Down
Loading

0 comments on commit afece2a

Please sign in to comment.