@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
135 lines • 5.34 kB
JavaScript
import { UserRefusedAddress } from "@ledgerhq/errors";
import { log } from "@ledgerhq/logs";
import invariant from "invariant";
import { useCallback, useEffect, useRef, useState } from "react";
import { firstValueFrom, from } from "rxjs";
import { AcreMessageType } from "@ledgerhq/wallet-api-acre-module";
import perFamily from "../../generated/hw-signMessage";
import { createAction as createAppAction } from "../actions/app";
import { withDevice } from "../deviceAccess";
import { messageSigner as ACREMessageSigner } from "../../families/bitcoin/ACRESetup";
import { decodeAccountId } from "../../account";
export const prepareMessageToSign = (account, message) => {
const utf8Message = Buffer.from(message, "hex").toString();
if (!perFamily[account.currency.family]) {
throw new Error("Crypto does not support signMessage");
}
if ("prepareMessageToSign" in perFamily[account.currency.family]) {
return perFamily[account.currency.family].prepareMessageToSign({
account,
message: utf8Message,
});
}
// Default implementation
return { message: utf8Message };
};
const signMessage = (transport, account, opts) => {
const { currency } = account;
let signMessage = perFamily[currency.family].signMessage;
if ("type" in opts) {
switch (opts.type) {
case AcreMessageType.Withdraw:
signMessage = ACREMessageSigner.signWithdraw;
break;
case AcreMessageType.SignIn:
signMessage = ACREMessageSigner.signIn;
break;
default:
signMessage = ACREMessageSigner.signMessage;
break;
}
}
invariant(signMessage, `signMessage is not implemented for ${currency.id}`);
return signMessage(transport, account, opts)
.then(result => {
const path = "path" in opts && opts.path ? opts.path : account.freshAddressPath;
log("hw", `signMessage ${currency.id} on ${path} with message [${opts.message}]`, result);
return result;
})
.catch(e => {
const path = "path" in opts && opts.path ? opts.path : account.freshAddressPath;
log("hw", `signMessage ${currency.id} on ${path} FAILED ${String(e)}`);
if (e && e.name === "TransportStatusError") {
if (e.statusCode === 0x6985 || e.statusCode === 0x5501) {
throw new UserRefusedAddress();
}
}
throw e;
});
};
export const signMessageExec = ({ request, deviceId }) => {
if (!request.account) {
throw new Error("account is required");
}
const { type } = decodeAccountId(request.account.id);
if (type === "mock") {
return from(Promise.resolve({
signature: "mockedSignature",
}));
}
const result = withDevice(deviceId)(transport => from(signMessage(transport, request.account, request.message)));
return result;
};
const initialState = {
signMessageRequested: null,
signMessageError: null,
signMessageResult: null,
};
export const createAction = (connectAppExec, signMessage = signMessageExec) => {
const useHook = (reduxDevice, request) => {
const appState = createAppAction(connectAppExec).useHook(reduxDevice, {
appName: request.appName,
dependencies: request.dependencies,
account: request.isACRE ? undefined : request.account, // Bypass derivation check with ACRE as we can use other addresses than the freshest
});
const { device, opened, inWrongDeviceForAccount, error } = appState;
const [state, setState] = useState({
...initialState,
signMessageRequested: request.message,
});
const signedFired = useRef();
const sign = useCallback(async () => {
let result;
if (!device) {
setState({
...initialState,
signMessageError: new Error("no Device"),
});
return;
}
try {
result = await firstValueFrom(signMessage({
request,
deviceId: device.deviceId,
}));
}
catch (e) {
if (e.name === "UserRefusedAddress") {
e.name = "UserRefusedOnDevice";
e.message = "UserRefusedOnDevice";
}
return setState({ ...initialState, signMessageError: e });
}
setState({ ...initialState, signMessageResult: result?.signature });
}, [device, request]);
useEffect(() => {
if (!device || !opened || inWrongDeviceForAccount || error) {
return;
}
if (state.signMessageRequested && !signedFired.current) {
signedFired.current = true;
sign();
}
}, [device, opened, inWrongDeviceForAccount, error, sign, state.signMessageRequested]);
return { ...appState, ...state };
};
return {
useHook,
mapResult: (state) => ({
signature: state.signMessageResult,
error: state.signMessageError,
}),
};
};
export default signMessage;
//# sourceMappingURL=index.js.map