-
Notifications
You must be signed in to change notification settings - Fork 121
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
actions: Add ledger backend functions.
- Loading branch information
1 parent
587d099
commit b1c1525
Showing
5 changed files
with
700 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,235 @@ | ||
import TransportNodeHid from "@ledgerhq/hw-transport-node-hid-singleton"; | ||
import { createTransaction } from "@ledgerhq/hw-app-btc/lib/createTransaction"; | ||
import Btc from "@ledgerhq/hw-app-btc"; | ||
import * as ledgerHelpers from "../helpers/ledger"; | ||
import { wallet } from "wallet-preload-shim"; | ||
import { publishTransactionAttempt } from "./ControlActions"; | ||
import { hexToBytes } from "helpers"; | ||
import { getTxFromInputs } from "../actions/TransactionActions"; | ||
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"; | ||
|
||
const coin = "decred"; | ||
// 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"; | ||
|
||
async function connect(dispatch, getState) { | ||
dispatch({ type: LDG_CONNECT_ATTEMPT }); | ||
try { | ||
// If unable to connect, a timeout error will occur. | ||
await deviceRun(dispatch, getState, async (transport) => {}) | ||
} catch (error) { | ||
dispatch({ type: LDG_CONNECT_FAILED }); | ||
throw error | ||
} | ||
dispatch({ type: LDG_CONNECT_SUCCESS }); | ||
return device; | ||
} | ||
|
||
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 }); | ||
}; | ||
|
||
// deviceRun is the main function for executing ledger operations. This handles | ||
// cleanup for cancellations and device disconnections during mid-operation (eg: | ||
// someone disconnected ledger while it was waiting for a pin input). | ||
// In general, fn itself shouldn't handle errors, letting this function handle | ||
// the common cases, which are then propagated up the call stack into fn's | ||
// parent. | ||
async function deviceRun(dispatch, getState, fn) { | ||
const timoutErr = new Error("timeout waiting for transport") | ||
const handleError = (error) => { | ||
if (error === timeoutErr) { | ||
alertNoConnectedDevice()(dispatch); | ||
} | ||
return error; | ||
}; | ||
|
||
try { | ||
// TODO: Enable on mainnet. | ||
const isTestnet = selectors.isTestNet(getState()); | ||
if (!isTestnet) | ||
throw "ledger is currently under development and should only be used on testnet"; | ||
|
||
const pWithTransport = new Promise(async (resolve, reject) => { | ||
resolve(await TransportNodeHid.create()) | ||
}) | ||
|
||
let timeout; | ||
const pTimeout = new Promise((resolve, reject) => { | ||
timeout = setTimeout(() => {reject(timeoutErr)}, 2000) | ||
}) | ||
|
||
// Creating the transport will wait indefinitely. Timeout if it does not | ||
// connect in a couple seconds. | ||
let res; | ||
await Promise.race([pWithTransport, pTimeout]).then( async (t) => { | ||
clearTimeout(timeout) | ||
res = await fn(t) | ||
}).catch((err) => { | ||
throw err | ||
}) | ||
|
||
return res; | ||
} catch (error) { | ||
throw handleError(error); | ||
} | ||
} | ||
|
||
// 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 deviceRun(dispatch, getState, async (transport) => { | ||
const btc = new Btc({ transport, currency: coin }); | ||
const res = await btc.getWalletPublicKey(path, { | ||
verify: false | ||
}); | ||
return res; | ||
}); | ||
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 decodedUnsigTxResp = wallet.decodeRawTransaction( | ||
Buffer.from(rawUnsigTx, "hex"), | ||
chainParams | ||
); | ||
const getInputs = (tx) => { | ||
return async () => { | ||
return await dispatch(getTxFromInputs(tx)); | ||
}; | ||
}; | ||
const arg = await ledgerHelpers.signArgs( | ||
decodedUnsigTxResp, | ||
wallet, | ||
walletService, | ||
getInputs | ||
); | ||
|
||
const signedRaw = await deviceRun( | ||
dispatch, | ||
getState, | ||
async (transport) => { | ||
await dispatch(checkLedgerIsDcrwallet()); | ||
|
||
const res = await createTransaction(transport, arg); | ||
return res; | ||
} | ||
); | ||
|
||
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 }); | ||
try { | ||
const payload = await deviceRun(dispatch, getState, async (transport) => { | ||
const btc = new Btc({ transport, currency: "decred" }); | ||
const res = await btc.getWalletPublicKey("44'/42'/0'", { | ||
verify: false | ||
}); | ||
return res; | ||
}); | ||
const hdpk = ledgerHelpers.pubkeyToHDPubkey( | ||
payload.publicKey, | ||
payload.chainCode | ||
); | ||
|
||
dispatch({ type: LDG_GETWALLETCREATIONMASTERPUBKEY_SUCCESS }); | ||
|
||
return hdpk; | ||
} catch (error) { | ||
dispatch({ error, type: LDG_GETWALLETCREATIONMASTERPUBKEY_FAILED }); | ||
throw error; | ||
} | ||
}; |
Oops, something went wrong.