Skip to content

Commit

Permalink
Merge pull request #342 from Emurgo/feature/fromDaedalusKey
Browse files Browse the repository at this point in the history
Daedalus Master Key restore
  • Loading branch information
vsubhuman authored Mar 13, 2019
2 parents c45c32c + 3fc56bf commit 11588b2
Show file tree
Hide file tree
Showing 34 changed files with 529 additions and 163 deletions.
4 changes: 3 additions & 1 deletion app/actions/ada/daedalus-transfer-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import Action from '../lib/Action';
export default class DaedalusTranferActions {
startTransferFunds: Action<any> = new Action();
startTransferPaperFunds: Action<any> = new Action();
setupTransferFunds: Action<any> = new Action();
startTransferMasterKey: Action<any> = new Action();
setupTransferFundsWithMnemonic: Action<any> = new Action();
setupTransferFundsWithMasterKey: Action<any> = new Action();
backToUninitialized: Action<any> = new Action();
transferFunds: Action<any> = new Action();
cancelTransferFunds: Action<any> = new Action();
Expand Down
17 changes: 5 additions & 12 deletions app/api/ada/daedalusTransfer.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@ import {
NoInputsError,
GenerateTransferTxError
} from './errors';
import {
getCryptoDaedalusWalletFromMnemonics
} from './lib/cardanoCrypto/cryptoWallet';
import {
getAllUTXOsForAddresses
} from './adaTransactions/adaNewTransactions';
Expand All @@ -38,14 +35,11 @@ import { getReceiverAddress } from './adaAddress';
* @param fullUtxo the full utxo of the Cardano blockchain
*/
export function getAddressesWithFunds(payload: {
secretWords: string,
checker: CryptoAddressChecker,
fullUtxo: Array<string>
}): Array<CryptoDaedalusAddressRestored> {
try {
const { secretWords, fullUtxo } = payload;
const checker: CryptoAddressChecker = getResultOrFail(
RandomAddressChecker.newCheckerFromMnemonics(secretWords)
);
const { checker, fullUtxo } = payload;
const addressesWithFunds: Array<CryptoDaedalusAddressRestored> = getResultOrFail(
RandomAddressChecker.checkAddresses(checker, fullUtxo)
);
Expand All @@ -58,12 +52,12 @@ export function getAddressesWithFunds(payload: {

/** Generate transaction including all addresses with no change */
export async function generateTransferTx(payload: {
secretWords: string,
wallet: CryptoDaedalusWallet,
addressesWithFunds: Array<CryptoDaedalusAddressRestored>
}): Promise<TransferTx> {
try {

const { secretWords, addressesWithFunds } = payload;
const { wallet, addressesWithFunds } = payload;

// fetch data to make transaction
const senders = addressesWithFunds.map(a => a.address);
Expand All @@ -78,8 +72,7 @@ export async function generateTransferTx(payload: {
// pick which address to send transfer to
const output = await getReceiverAddress();

// get wallet and make transaction
const wallet = getCryptoDaedalusWalletFromMnemonics(secretWords);
// make transaction
const tx: MoveResponse = getResultOrFail(Wallet.move(wallet, inputs, output));

// return summary of transaction
Expand Down
13 changes: 13 additions & 0 deletions app/api/ada/lib/cardanoCrypto/cryptoWallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,16 @@ export function getCryptoDaedalusWalletFromMnemonics(
wallet.config.protocol_magic = protocolMagic;
return wallet;
}

/** Generate a Daedalus /wallet/ to create transactions. Do not save this. Regenerate every time.
* Note: key encoded as hex-string
*/
export function getCryptoDaedalusWalletFromMasterKey(
masterKey: string,
): CryptoDaedalusWallet {
const encodedKey = Buffer.from(masterKey, 'hex');

const wallet: CryptoDaedalusWallet = getResultOrFail(Wallet.fromDaedalusMasterKey(encodedKey));
wallet.config.protocol_magic = protocolMagic;
return wallet;
}
18 changes: 13 additions & 5 deletions app/components/transfer/TransferInstructionsPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,6 @@ import globalMessages from '../../i18n/global-messages';
import styles from './TransferInstructionsPage.scss';

const messages = defineMessages({
instructionTitle: {
id: 'transfer.instructions.instructions.title.label',
defaultMessage: '!!!Instructions',
description: 'Label "Instructions" on the transfer instructions page.'
},
instructionsText: {
id: 'transfer.instructions.instructions.text',
defaultMessage: '!!!Before you can transfer funds, you must create a Yoroi wallet and back it up. Upon completion, you will receive a 15-word recovery phrase which can be used to restore your Yoroi wallet at any time.',
Expand All @@ -33,15 +28,18 @@ const messages = defineMessages({
});

messages.fieldIsRequired = globalMessages.fieldIsRequired;
messages.instructionTitle = globalMessages.instructionTitle;

type Props = {
onFollowInstructionsPrerequisites: Function,
onConfirm: Function,
onPaperConfirm: Function,
onMasterKeyConfirm: Function,
disableTransferFunds: boolean,
attentionText: string,
confirmationText: string,
confirmationPaperText: string,
confirmationMasterKeyText: string,
};

@observer
Expand All @@ -57,10 +55,12 @@ export default class TransferInstructionsPage extends Component<Props> {
onFollowInstructionsPrerequisites,
onConfirm,
onPaperConfirm,
onMasterKeyConfirm,
disableTransferFunds,
attentionText,
confirmationText,
confirmationPaperText,
confirmationMasterKeyText,
} = this.props;

const instructionsButtonClasses = classnames([
Expand Down Expand Up @@ -138,6 +138,14 @@ export default class TransferInstructionsPage extends Component<Props> {
skin={ButtonSkin}
/>

<Button
className={`masterKey ${confirmButtonClasses}`} // append class for testing
label={confirmationMasterKeyText}
onClick={onMasterKeyConfirm}
disabled={disableTransferFunds}
skin={ButtonSkin}
/>

</div>

</BorderedBox>
Expand Down
168 changes: 168 additions & 0 deletions app/components/transfer/TransferMasterKeyPage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// @flow
import React, { Component } from 'react';
import { observer } from 'mobx-react';
import classnames from 'classnames';
import { Input } from 'react-polymorph/lib/components/Input';
import { InputSkin } from 'react-polymorph/lib/skins/simple/InputSkin';
import { Button } from 'react-polymorph/lib/components/Button';
import { ButtonSkin } from 'react-polymorph/lib/skins/simple/ButtonSkin';
import { defineMessages, intlShape } from 'react-intl';
import ReactToolboxMobxForm from '../../utils/ReactToolboxMobxForm';
import BorderedBox from '../widgets/BorderedBox';
import globalMessages from '../../i18n/global-messages';
import styles from './TransferMasterKeyPage.scss';
import config from '../../config';

const messages = defineMessages({
masterKeyInputLabel: {
id: 'transfer.form.masterkey.input.label',
defaultMessage: '!!!Master key',
description: 'Label for the master key input on the transfer master key page.'
},
masterKeyInputHint: {
id: 'transfer.form.masterkey.input.hint',
defaultMessage: '!!!Enter master key',
description: 'Hint "Enter master key" for the master keyinput on the transfer master key page.'
},
masterKeyRequirements: {
id: 'transfer.form.masterkey.requirement',
defaultMessage: '!!!Note: master keys are 192 characters and hexadecimal-encoded',
description: 'Description of master key format'
},
});

messages.fieldIsRequired = globalMessages.fieldIsRequired;
messages.invalidMasterKey = globalMessages.invalidMasterKey;
messages.nextButtonLabel = globalMessages.nextButtonLabel;
messages.backButtonLabel = globalMessages.backButtonLabel;
messages.step1 = globalMessages.step1;
messages.instructionTitle = globalMessages.instructionTitle;

type Props = {
onSubmit: Function,
onBack: Function,
step0: string,
};

@observer
export default class TransferMasterKeyPage extends Component<Props> {

static contextTypes = {
intl: intlShape.isRequired
};

form = new ReactToolboxMobxForm({
fields: {
masterKey: {
label: this.context.intl.formatMessage(messages.masterKeyInputLabel),
placeholder: this.context.intl.formatMessage(messages.masterKeyInputHint),
value: '',
validators: [({ field }) => {
const value = field.value;
if (value === '') {
return [false, this.context.intl.formatMessage(messages.fieldIsRequired)];
}
if (value.length !== 192) {
return [false, this.context.intl.formatMessage(messages.invalidMasterKey)];
}
if (!value.match('^[0-9a-fA-F]+$')) {
return [false, this.context.intl.formatMessage(messages.invalidMasterKey)];
}
return true;
}],
},
},
}, {
options: {
validateOnChange: true,
validationDebounceWait: config.forms.FORM_VALIDATION_DEBOUNCE_WAIT,
},
});

submit = () => {
this.form.submit({
onSuccess: (form) => {
const { masterKey } = form.values();
const payload = {
masterKey,
};
this.props.onSubmit(payload);
},
onError: () => {}
});
};

render() {
const { intl } = this.context;
const { form } = this;
const { onBack, step0 } = this.props;

const nextButtonClasses = classnames([
'proceedTransferButtonClasses',
'primary',
styles.button,
]);
const backButtonClasses = classnames([
'backTransferButtonClasses',
'flat',
styles.button,
]);

const masterKeyField = form.$('masterKey');

return (
<div className={styles.component}>
<BorderedBox>

<div className={styles.body}>

{ /* Instructions for how to transfer */ }
<div>
<div className={styles.title}>
{intl.formatMessage(messages.instructionTitle)}
</div>

<ul className={styles.instructionsList}>
{
<div className={styles.text}>
{step0}
{intl.formatMessage(messages.step1)}
<br /><br />
{intl.formatMessage(messages.masterKeyRequirements)}
</div>
}
</ul>
</div>

<Input
className="masterKey"
autoComplete="off"
{...masterKeyField.bind()}
error={masterKeyField.error}
skin={InputSkin}
/>

<div className={styles.buttonsWrapper}>
<Button
className={nextButtonClasses}
label={intl.formatMessage(messages.nextButtonLabel)}
onClick={this.submit}
skin={ButtonSkin}
/>

<Button
className={backButtonClasses}
label={intl.formatMessage(messages.backButtonLabel)}
onClick={onBack}
skin={ButtonSkin}
/>
</div>

</div>

</BorderedBox>

</div>
);
}
}
52 changes: 52 additions & 0 deletions app/components/transfer/TransferMasterKeyPage.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
@import '../../themes/mixins/loading-spinner';

.component {
padding: 20px;
}

.button {
display: block !important;
margin: 20px auto 0;
}

.body {
color: var(--theme-bordered-box-text-color);
font-family: var(--font-regular);
font-size: 14px;
line-height: 19px;

.title {
font-size: 11px;
font-family: var(--font-medium);
font-weight: 600;
line-height: 1.38;
margin-bottom: 4px;
padding-left: 10px;
text-transform: uppercase;
}

.text {
font-size: 15px;
margin-bottom: 0.3%;
word-break: break-word;
}

.instructionsList {
list-style-type: disc;
list-style-position: inside;
margin-bottom: 20px;
}

.buttonsWrapper {
display: flex;
flex-direction: row-reverse;
justify-content: center;
}

.submitWithPasswordButton {
&.spinning {
@include loading-spinner("../../assets/images/spinner-light.svg");
}
}
}

Loading

0 comments on commit 11588b2

Please sign in to comment.