From ba5bd97f5cd503dded7beb53e11e94c4471bfeec Mon Sep 17 00:00:00 2001 From: shendel Date: Thu, 15 Dec 2022 07:03:26 +0300 Subject: [PATCH 01/30] Shamir's Secret-Sharing for Mnemonic Codes (base) --- src/common/utils/mnemonic.ts | 40 + src/common/utils/slip39/slip39.js | 193 ++ src/common/utils/slip39/slip39_helper.js | 2272 ++++++++++++++++++++++ 3 files changed, 2505 insertions(+) create mode 100644 src/common/utils/slip39/slip39.js create mode 100644 src/common/utils/slip39/slip39_helper.js diff --git a/src/common/utils/mnemonic.ts b/src/common/utils/mnemonic.ts index e7436ee9d6..bc1123ff7d 100644 --- a/src/common/utils/mnemonic.ts +++ b/src/common/utils/mnemonic.ts @@ -2,6 +2,9 @@ import * as bitcoin from 'bitcoinjs-lib' import * as bip32 from 'bip32' import { hdkey } from 'ethereumjs-wallet' import * as bip39 from 'bip39' +import Slip39 from './slip39/slip39.js' +import * as slipHelper from './slip39/slip39_helper.js' + const getRandomMnemonicWords = () => { return bip39.generateMnemonic() @@ -11,6 +14,43 @@ const validateMnemonicWords = (mnemonic) => { return bip39.validateMnemonic(convertMnemonicToValid(mnemonic)) } +// Shamir's Secret Sharing alternative to saving 12 words seed (Split mnemonic to three secrets) +const splitMnemonicToSecretParts = (mnemonic, passphrase = ``) => { + mnemonic = convertMnemonicToValid(mnemonic) + const mnemonicEntropy: string = bip39.mnemonicToEntropy(mnemonic) + const masterSecret: number[] = slipHelper.toByteArray(mnemonicEntropy) + + const getMnemonicInt = (mnemonic) => { + return slipHelper.intFromIndices(slipHelper.mnemonicToIndices(mnemonic)) + } + + const slip = Slip39.fromArray(masterSecret, { + passphrase, + threshold: 2, // number of group-shares required to reconstruct the master secret. + groups: [ + [1, 1], [1, 1], [1, 1] // split master key to 3 parts + ] + }) + + const mnemonics = [ + slip.fromPath('r/0/0').mnemonics[0], + slip.fromPath('r/1/0').mnemonics[0], + slip.fromPath('r/2/0').mnemonics[0] + ] + + const secretParts = [ + getMnemonicInt(mnemonics[0]), + getMnemonicInt(mnemonics[1]), + getMnemonicInt(mnemonics[2]) + ] + return { + mnemonics, + secretParts + } +} + +// @ts-ignore +window.splitMnemonicToSecretParts = splitMnemonicToSecretParts const convertMnemonicToValid = (mnemonic) => { return mnemonic .trim() diff --git a/src/common/utils/slip39/slip39.js b/src/common/utils/slip39/slip39.js new file mode 100644 index 0000000000..8fcacd4f33 --- /dev/null +++ b/src/common/utils/slip39/slip39.js @@ -0,0 +1,193 @@ +/* eslint-disable radix */ +const slipHelper = require('./slip39_helper.js'); + +const MAX_DEPTH = 2; + +/** + * Slip39Node + * For root node, description refers to the whole set's title e.g. "Hardware wallet X SSSS shares" + * For children nodes, description refers to the group e.g. "Family group: mom, dad, sister, wife" + */ +class Slip39Node { + constructor(index = 0, description = '', mnemonic = '', children = [], secret = []) { + this.index = index; + this.description = description; + this.mnemonic = mnemonic; + this.children = children; + this.secret = secret; + } + + get mnemonics() { + if (this.children.length === 0) { + return [this.mnemonic]; + } + const result = this.children.reduce((prev, item) => { + return prev.concat(item.mnemonics); + }, []); + return result; + } +} + +// +// The javascript implementation of the SLIP-0039: Shamir's Secret-Sharing for Mnemonic Codes +// see: https://github.com/satoshilabs/slips/blob/master/slip-0039.md) +// +class Slip39 { + constructor({ + iterationExponent = 0, + identifier, + groupCount, + groupThreshold + } = {}) { + this.iterationExponent = iterationExponent; + this.identifier = identifier; + this.groupCount = groupCount; + this.groupThreshold = groupThreshold; + } + + static fromArray(masterSecret, { + passphrase = '', + threshold = 1, + groups = [ + [1, 1, 'Default 1-of-1 group share'] + ], + iterationExponent = 0, + title = 'My default slip39 shares' + } = {}) { + if (masterSecret.length * 8 < slipHelper.MIN_ENTROPY_BITS) { + throw Error(`The length of the master secret (${masterSecret.length} bytes) must be at least ${slipHelper.bitsToBytes(slipHelper.MIN_ENTROPY_BITS)} bytes.`); + } + + if (masterSecret.length % 2 !== 0) { + throw Error('The length of the master secret in bytes must be an even number.'); + } + + if (!/^[\x20-\x7E]*$/.test(passphrase)) { + throw Error('The passphrase must contain only printable ASCII characters (code points 32-126).'); + } + + if (threshold > groups.length) { + throw Error(`The requested group threshold (${threshold}) must not exceed the number of groups (${groups.length}).`); + } + + groups.forEach((item) => { + if (item[0] === 1 && item[1] > 1) { + throw Error(`Creating multiple member shares with member threshold 1 is not allowed. Use 1-of-1 member sharing instead. ${groups.join()}`); + } + }); + + const identifier = slipHelper.generateIdentifier(); + + const slip = new Slip39({ + iterationExponent: iterationExponent, + identifier: identifier, + groupCount: groups.length, + groupThreshold: threshold + }); + + const encryptedMasterSecret = slipHelper.crypt( + masterSecret, passphrase, iterationExponent, slip.identifier); + + const root = slip.buildRecursive( + new Slip39Node(0, title), + groups, + encryptedMasterSecret, + threshold + ); + + slip.root = root; + return slip; + } + + buildRecursive(currentNode, nodes, secret, threshold, index) { + //console.log('>>> buildRecursive', secret, threshold, index) + // It means it's a leaf. + if (nodes.length === 0) { + const mnemonic = slipHelper.encodeMnemonic(this.identifier, this.iterationExponent, index, + this.groupThreshold, this.groupCount, currentNode.index, threshold, secret); + + currentNode.mnemonic = mnemonic; + currentNode.secret = secret; + return currentNode; + } + + const secretShares = slipHelper.splitSecret(threshold, nodes.length, secret); + let children = []; + let idx = 0; + + nodes.forEach((item) => { + // n=threshold + const n = item[0]; + // m=members + const m = item[1]; + // d=description + const d = item[2] || ''; + + // Generate leaf members, means their `m` is `0` + const members = Array().slip39Generate(m, () => [n, 0, d]); + + const node = new Slip39Node(idx, d); + const branch = this.buildRecursive( + node, + members, + secretShares[idx], + n, + currentNode.index); + + children = children.concat(branch); + idx = idx + 1; + }); + currentNode.children = children; + return currentNode; + } + + static recoverSecret(mnemonics, passphrase) { + return slipHelper.combineMnemonics(mnemonics, passphrase); + } + + static validateMnemonic(mnemonic) { + return slipHelper.validateMnemonic(mnemonic); + } + + fromPath(path) { + this.validatePath(path); + + const children = this.parseChildren(path); + + if (typeof children === 'undefined' || children.length === 0) { + return this.root; + } + + return children.reduce((prev, childNumber) => { + let childrenLen = prev.children.length; + if (childNumber >= childrenLen) { + throw new Error(`The path index (${childNumber}) exceeds the children index (${childrenLen - 1}).`); + } + + return prev.children[childNumber]; + }, this.root); + } + + validatePath(path) { + if (!path.match(/(^r)(\/\d{1,2}){0,2}$/)) { + throw new Error('Expected valid path e.g. "r/0/0".'); + } + + const depth = path.split('/'); + const pathLength = depth.length - 1; + if (pathLength > MAX_DEPTH) { + throw new Error(`Path\'s (${path}) max depth (${MAX_DEPTH}) is exceeded (${pathLength}).`); + } + } + + parseChildren(path) { + const splitted = path.split('/').slice(1); + + const result = splitted.map((pathFragment) => { + return parseInt(pathFragment); + }); + return result; + } +} + +export default Slip39 diff --git a/src/common/utils/slip39/slip39_helper.js b/src/common/utils/slip39/slip39_helper.js new file mode 100644 index 0000000000..56d349d95c --- /dev/null +++ b/src/common/utils/slip39/slip39_helper.js @@ -0,0 +1,2272 @@ +/* eslint-disable no-array-constructor */ +let crypto; +try { + crypto = require('crypto'); +} catch (err) { + throw new Error('crypto support must be enabled'); +} + + +// The length of the radix in bits. +const RADIX_BITS = 10; + +// The length of the random identifier in bits. +const ID_BITS_LENGTH = 15; + +// The length of the iteration exponent in bits. +const ITERATION_EXP_BITS_LENGTH = 5; + +// The length of the random identifier and iteration exponent in words. +const ITERATION_EXP_WORDS_LENGTH = + parseInt((ID_BITS_LENGTH + ITERATION_EXP_BITS_LENGTH + RADIX_BITS - 1) / RADIX_BITS, 10); + +// The maximum iteration exponent +const MAX_ITERATION_EXP = Math.pow(2, ITERATION_EXP_BITS_LENGTH); + +// The maximum number of shares that can be created. +const MAX_SHARE_COUNT = 16; + +// The length of the RS1024 checksum in words. +const CHECKSUM_WORDS_LENGTH = 3; + +// The length of the digest of the shared secret in bytes. +const DIGEST_LENGTH = 4; + +// The customization string used in the RS1024 checksum and in the PBKDF2 salt. +const SALT_STRING = 'shamir'; + +// The minimum allowed entropy of the master secret. +const MIN_ENTROPY_BITS = 128; + +// The minimum allowed length of the mnemonic in words. +const METADATA_WORDS_LENGTH = + ITERATION_EXP_WORDS_LENGTH + 2 + CHECKSUM_WORDS_LENGTH; + +// The length of the mnemonic in words without the share value. +const MNEMONICS_WORDS_LENGTH = parseInt( + METADATA_WORDS_LENGTH + (MIN_ENTROPY_BITS + RADIX_BITS - 1) / RADIX_BITS, 10); + +// The minimum number of iterations to use in PBKDF2. +const ITERATION_COUNT = 10000; + +// The number of rounds to use in the Feistel cipher. +const ROUND_COUNT = 4; + +// The index of the share containing the digest of the shared secret. +const DIGEST_INDEX = 254; + +// The index of the share containing the shared secret. +const SECRET_INDEX = 255; + +// +// Helper functions for SLIP39 implementation. +// +String.prototype.slip39EncodeHex = function () { + let bytes = []; + for (let i = 0; i < this.length; ++i) { + bytes.push(this.charCodeAt(i)); + } + return bytes; +}; + +Array.prototype.slip39DecodeHex = function () { + let str = []; + const hex = this.toString().split(','); + for (let i = 0; i < hex.length; i++) { + str.push(String.fromCharCode(hex[i])); + } + return str.toString().replace(/,/g, ''); +}; + +Array.prototype.slip39Generate = function (m, v = _ => _) { + let n = m || this.length; + for (let i = 0; i < n; i++) { + this[i] = v(i); + } + return this; +}; + +const toHexString = function () { + return Array.prototype.map.call(this, function (byte) { + return ('0' + (byte & 0xFF).toString(16)).slice(-2); + }).join(''); +}; + +const toByteArray = function (hexString) { + const ret = [] + for (let i = 0; i < hexString.length; i = i + 2) { + ret.push(parseInt(hexString.substr(i, 2), 16)); + } + return ret; +}; + + +const BIGINT_WORD_BITS = BigInt(8); + +function decodeBigInt(bytes) { + let result = BigInt(0); + for (let i = 0; i < bytes.length; i++) { + let b = BigInt(bytes[bytes.length - i - 1]); + result = result + (b << BIGINT_WORD_BITS * BigInt(i)); + } + return result; +} + +function encodeBigInt(number, paddedLength = 0) { + let num = number; + const BYTE_MASK = BigInt(0xff); + const BIGINT_ZERO = BigInt(0); + let result = new Array(0); + + while (num > BIGINT_ZERO) { + let i = parseInt(num & BYTE_MASK, 10); + result.unshift(i); + num = num >> BIGINT_WORD_BITS; + } + + // Zero padding to the length + for (let i = result.length; i < paddedLength; i++) { + result.unshift(0); + } + + if (paddedLength !== 0 && result.length > paddedLength) { + throw new Error(`Error in encoding BigInt value, expected less than ${paddedLength} length value, got ${result.length}`); + } + + return result; +} + +function bitsToBytes(n) { + const res = (n + 7) / 8; + const b = parseInt(res, RADIX_BITS); + return b; +} + +function bitsToWords(n) { + const res = (n + RADIX_BITS - 1) / RADIX_BITS; + const b = parseInt(res, RADIX_BITS); + return b; +} + +// +// Returns a randomly generated integer in the range 0, ... , 2**ID_LENGTH_BITS - 1. +// +function randomBytes(length = 32) { + let randoms = crypto.randomBytes(length); + return Array.prototype.slice.call(randoms, 0); +} + +// +// The round function used internally by the Feistel cipher. +// +function roundFunction(round, passphrase, exp, salt, secret) { + const saltedSecret = salt.concat(secret); + const roundedPhrase = [round].concat(passphrase); + const count = (ITERATION_COUNT << exp) / ROUND_COUNT; + + const key = crypto.pbkdf2Sync(Buffer.from(roundedPhrase), Buffer.from(saltedSecret), count, secret.length, 'sha256'); + return Array.prototype.slice.call(key, 0); +} + +function crypt(masterSecret, passphrase, iterationExponent, + identifier, + encrypt = true) { + // Iteration exponent validated here. + if (iterationExponent < 0 || iterationExponent > MAX_ITERATION_EXP) { + throw Error(`Invalid iteration exponent (${iterationExponent}). Expected between 0 and ${MAX_ITERATION_EXP}`); + } + + let IL = masterSecret.slice().slice(0, masterSecret.length / 2); + let IR = masterSecret.slice().slice(masterSecret.length / 2); + + const pwd = passphrase.slip39EncodeHex(); + + const salt = getSalt(identifier); + + let range = Array().slip39Generate(ROUND_COUNT); + range = encrypt ? range : range.reverse(); + + range.forEach((round) => { + const f = roundFunction(round, pwd, iterationExponent, salt, IR); + const t = xor(IL, f); + IL = IR; + IR = t; + }); + return IR.concat(IL); +} + +function createDigest(randomData, sharedSecret) { + const hmac = crypto.createHmac('sha256', Buffer.from(randomData)); + + hmac.update(Buffer.from(sharedSecret)); + + let result = hmac.digest(); + result = result.slice(0, 4); + return Array.prototype.slice.call(result, 0); +} + +function splitSecret(threshold, shareCount, sharedSecret) { + if (threshold <= 0) { + throw Error(`The requested threshold (${threshold}) must be a positive integer.`); + } + + if (threshold > shareCount) { + throw Error(`The requested threshold (${threshold}) must not exceed the number of shares (${shareCount}).`); + } + + if (shareCount > MAX_SHARE_COUNT) { + throw Error(`The requested number of shares (${shareCount}) must not exceed ${MAX_SHARE_COUNT}.`); + } + // If the threshold is 1, then the digest of the shared secret is not used. + if (threshold === 1) { + return Array().slip39Generate(shareCount, () => sharedSecret); + } + + const randomShareCount = threshold - 2; + + const randomPart = randomBytes(sharedSecret.length - DIGEST_LENGTH); + const digest = createDigest(randomPart, sharedSecret); + + let baseShares = new Map(); + let shares = []; + if (randomShareCount) { + shares = Array().slip39Generate( + randomShareCount, () => randomBytes(sharedSecret.length)); + shares.forEach((item, idx) => { + baseShares.set(idx, item); + }); + } + baseShares.set(DIGEST_INDEX, digest.concat(randomPart)); + baseShares.set(SECRET_INDEX, sharedSecret); + + for (let i = randomShareCount; i < shareCount; i++) { + const rr = interpolate(baseShares, i); + shares.push(rr); + } + + return shares; +} + +// +// Returns a randomly generated integer in the range 0, ... , 2**ID_BITS_LENGTH - 1. +// +function generateIdentifier() { + const byte = bitsToBytes(ID_BITS_LENGTH); + const bits = ID_BITS_LENGTH % 8; + const identifier = randomBytes(byte); + + identifier[0] = identifier[0] & (1 << bits) - 1; + + return identifier; +} + +function xor(a, b) { + if (a.length !== b.length) { + throw new Error(`Invalid padding in mnemonic or insufficient length of mnemonics (${a.length} or ${b.length})`); + } + return Array().slip39Generate(a.length, (i) => a[i] ^ b[i]); +} + +function getSalt(identifier) { + const salt = SALT_STRING.slip39EncodeHex(); + return salt.concat(identifier); +} + +function interpolate(shares, x) { + let xCoord = new Set(shares.keys()); + let arr = Array.from(shares.values(), (v) => v.length); + let sharesValueLengths = new Set(arr); + + if (sharesValueLengths.size !== 1) { + throw new Error('Invalid set of shares. All share values must have the same length.'); + } + + if (xCoord.has(x)) { + shares.forEach((v, k) => { + if (k === x) { + return v; + } + }); + } + + // Logarithm of the product of (x_i - x) for i = 1, ... , k. + let logProd = 0; + + shares.forEach((v, k) => { + logProd = logProd + LOG_TABLE[k ^ x]; + }); + + let results = Array().slip39Generate(sharesValueLengths.values().next().value, () => 0); + + shares.forEach((v, k) => { + // The logarithm of the Lagrange basis polynomial evaluated at x. + let sum = 0; + shares.forEach((vv, kk) => { + sum = sum + LOG_TABLE[k ^ kk]; + }); + + // FIXME: -18 % 255 = 237. IT shoulud be 237 and not -18 as it's + // implemented in javascript. + const basis = (logProd - LOG_TABLE[k ^ x] - sum) % 255; + + const logBasisEval = basis < 0 ? 255 + basis : basis; + + v.forEach((item, idx) => { + const shareVal = item; + const intermediateSum = results[idx]; + const r = shareVal !== 0 ? + EXP_TABLE[(LOG_TABLE[shareVal] + logBasisEval) % 255] : 0; + + const res = intermediateSum ^ r; + results[idx] = res; + }); + }); + return results; +} + +function rs1024Polymod(data) { + const GEN = [ + 0xE0E040, + 0x1C1C080, + 0x3838100, + 0x7070200, + 0xE0E0009, + 0x1C0C2412, + 0x38086C24, + 0x3090FC48, + 0x21B1F890, + 0x3F3F120 + ]; + let chk = 1; + + data.forEach((byte) => { + const b = chk >> 20; + chk = (chk & 0xFFFFF) << 10 ^ byte; + + for (let i = 0; i < 10; i++) { + let gen = (b >> i & 1) !== 0 ? GEN[i] : 0; + chk = chk ^ gen; + } + }); + + return chk; +} + +function rs1024CreateChecksum(data) { + const values = SALT_STRING.slip39EncodeHex() + .concat(data) + .concat(Array().slip39Generate(CHECKSUM_WORDS_LENGTH, () => 0)); + const polymod = rs1024Polymod(values) ^ 1; + const result = + Array().slip39Generate(CHECKSUM_WORDS_LENGTH, (i) => polymod >> 10 * i & 1023).reverse(); + + return result; +} + +function rs1024VerifyChecksum(data) { + return rs1024Polymod(SALT_STRING.slip39EncodeHex().concat(data)) === 1; +} + +// +// Converts a list of base 1024 indices in big endian order to an integer value. +// +function intFromIndices(indices) { + let value = BigInt(0); + const radix = BigInt(Math.pow(2, RADIX_BITS)); + indices.forEach((index) => { + value = value * radix + BigInt(index); + }); + + return value; +} + +// +// Converts a Big integer value to indices in big endian order. +// +function intToIndices(value, length, bits) { + const mask = BigInt((1 << bits) - 1); + const result = + Array().slip39Generate(length, (i) => parseInt(value >> BigInt(i) * BigInt(bits) & mask, 10)); + return result.reverse(); +} + +function mnemonicFromIndices(indices) { + const result = indices.map((index) => { + return WORD_LIST[index]; + }); + return result.toString().split(',').join(' '); +} + +function mnemonicToIndices(mnemonic) { + if (typeof mnemonic !== 'string') { + throw new Error(`Mnemonic expected to be typeof string with white space separated words. Instead found typeof ${typeof mnemonic}.`); + } + + const words = mnemonic.toLowerCase().split(' '); + const result = words.reduce((prev, item) => { + const index = WORD_LIST_MAP[item]; + if (typeof index === 'undefined') { + throw new Error(`Invalid mnemonic word ${item}.`); + } + return prev.concat(index); + }, []); + return result; +} + +function recoverSecret(threshold, shares) { + // If the threshold is 1, then the digest of the shared secret is not used. + if (threshold === 1) { + return shares.values().next().value; + } + + const sharedSecret = interpolate(shares, SECRET_INDEX); + const digestShare = interpolate(shares, DIGEST_INDEX); + const digest = digestShare.slice(0, DIGEST_LENGTH); + const randomPart = digestShare.slice(DIGEST_LENGTH); + + const recoveredDigest = createDigest( + randomPart, sharedSecret); + if (!listsAreEqual(digest, recoveredDigest)) { + throw new Error('Invalid digest of the shared secret.'); + } + return sharedSecret; +} + +// +// Combines mnemonic shares to obtain the master secret which was previously +// split using Shamir's secret sharing scheme. +// +function combineMnemonics(mnemonics, passphrase = '') { + if (mnemonics === null || mnemonics.length === 0) { + throw new Error('The list of mnemonics is empty.'); + } + + const decoded = decodeMnemonics(mnemonics); + const identifier = decoded.identifier; + const iterationExponent = decoded.iterationExponent; + const groupThreshold = decoded.groupThreshold; + const groupCount = decoded.groupCount; + const groups = decoded.groups; + + if (groups.size < groupThreshold) { + throw new Error(`Insufficient number of mnemonic groups (${groups.size}). The required number of groups is ${groupThreshold}.`); + } + + if (groups.size !== groupThreshold) { + throw new Error(`Wrong number of mnemonic groups. Expected ${groupThreshold} groups, but ${groups.size} were provided.`); + } + + let allShares = new Map(); + groups.forEach((members, groupIndex) => { + const threshold = members.keys().next().value; + const shares = members.values().next().value; + if (shares.size !== threshold) { + const prefix = groupPrefix( + identifier, + iterationExponent, + groupIndex, + groupThreshold, + groupCount + ); + throw new Error(`Wrong number of mnemonics. Expected ${threshold} mnemonics starting with "${mnemonicFromIndices(prefix)}", \n but ${shares.size} were provided.`); + } + + const recovered = recoverSecret(threshold, shares); + allShares.set(groupIndex, recovered); + }); + + const ems = recoverSecret(groupThreshold, allShares); + const id = intToIndices(BigInt(identifier), ITERATION_EXP_WORDS_LENGTH, 8); + const ms = crypt(ems, passphrase, iterationExponent, id, false); + + return ms; +} + +function combineSecrets(secrets, secretsGroups, identifier, groupThreshold=2, passphrase = '') { + + if (secrets === null || secrets.length === 0) { + throw new Error('The list of secrets is empty.'); + } + if (secretsGroups === null || secretsGroups.length !== secrets.length) { + throw new Error('The list of secrets groups must be equal length like secrets.'); + } + + let allShares = new Map(); + secrets.forEach((index, secret) => { + allShares.set(secretsGroups[index], secret); + }); + + const ems = recoverSecret(groupThreshold, allShares); + const id = intToIndices(BigInt(identifier), ITERATION_EXP_WORDS_LENGTH, 8); + const ms = crypt(ems, passphrase, iterationExponent, id, false); + + return ms; +} + +function decodeMnemonics(mnemonics) { + if (!(mnemonics instanceof Array)) { + throw new Error('Mnemonics should be an array of strings'); + } + const identifiers = new Set(); + const iterationExponents = new Set(); + const groupThresholds = new Set(); + const groupCounts = new Set(); + const groups = new Map(); + + mnemonics.forEach((mnemonic) => { + const decoded = decodeMnemonic(mnemonic); + + identifiers.add(decoded.identifier); + iterationExponents.add(decoded.iterationExponent); + const groupIndex = decoded.groupIndex; + groupThresholds.add(decoded.groupThreshold); + groupCounts.add(decoded.groupCount); + const memberIndex = decoded.memberIndex; + const memberThreshold = decoded.memberThreshold; + const share = decoded.share; + + const group = !groups.has(groupIndex) ? new Map() : groups.get(groupIndex); + const member = !group.has(memberThreshold) ? new Map() : group.get(memberThreshold); + member.set(memberIndex, share); + group.set(memberThreshold, member); + if (group.size !== 1) { + throw new Error('Invalid set of mnemonics. All mnemonics in a group must have the same member threshold.'); + } + groups.set(groupIndex, group); + }); + + if (identifiers.size !== 1 || iterationExponents.size !== 1) { + throw new Error(`Invalid set of mnemonics. All mnemonics must begin with the same ${ITERATION_EXP_WORDS_LENGTH} words.`); + } + + if (groupThresholds.size !== 1) { + throw new Error('Invalid set of mnemonics. All mnemonics must have the same group threshold.'); + } + + if (groupCounts.size !== 1) { + throw new Error('Invalid set of mnemonics. All mnemonics must have the same group count.'); + } + + return { + identifier: identifiers.values().next().value, + iterationExponent: iterationExponents.values().next().value, + groupThreshold: groupThresholds.values().next().value, + groupCount: groupCounts.values().next().value, + groups: groups + }; +} + +// +// Converts a share mnemonic to share data. +// +function decodeMnemonic(mnemonic) { + const data = mnemonicToIndices(mnemonic); + + if (data.length < MNEMONICS_WORDS_LENGTH) { + throw new Error(`Invalid mnemonic length. The length of each mnemonic must be at least ${MNEMONICS_WORDS_LENGTH} words.`); + } + + const paddingLen = RADIX_BITS * (data.length - METADATA_WORDS_LENGTH) % 16; + if (paddingLen > 8) { + throw new Error('Invalid mnemonic length.'); + } + + if (!rs1024VerifyChecksum(data)) { + throw new Error('Invalid mnemonic checksum'); + } + + const idExpInt = + parseInt(intFromIndices(data.slice(0, ITERATION_EXP_WORDS_LENGTH)), 10); + const identifier = idExpInt >> ITERATION_EXP_BITS_LENGTH; + const iterationExponent = idExpInt & (1 << ITERATION_EXP_BITS_LENGTH) - 1; + const tmp = intFromIndices( + data.slice(ITERATION_EXP_WORDS_LENGTH, ITERATION_EXP_WORDS_LENGTH + 2)); + + const indices = intToIndices(tmp, 5, 4); + + const groupIndex = indices[0]; + const groupThreshold = indices[1]; + const groupCount = indices[2]; + const memberIndex = indices[3]; + const memberThreshold = indices[4]; + + const valueData = data.slice( + ITERATION_EXP_WORDS_LENGTH + 2, data.length - CHECKSUM_WORDS_LENGTH); + + if (groupCount < groupThreshold) { + throw new Error(`Invalid mnemonic: ${mnemonic}.\n Group threshold (${groupThreshold}) cannot be greater than group count (${groupCount}).`); + } + + const valueInt = intFromIndices(valueData); + + try { + const valueByteCount = bitsToBytes(RADIX_BITS * valueData.length - paddingLen); + const share = encodeBigInt(valueInt, valueByteCount); + + return { + identifier: identifier, + iterationExponent: iterationExponent, + groupIndex: groupIndex, + groupThreshold: groupThreshold + 1, + groupCount: groupCount + 1, + memberIndex: memberIndex, + memberThreshold: memberThreshold + 1, + share: share + }; + } catch (e) { + throw new Error(`Invalid mnemonic padding (${e})`); + } +} + +function validateMnemonic(mnemonic) { + try { + decodeMnemonic(mnemonic); + return true; + } catch (error) { + return false; + } +} + +function groupPrefix( + identifier, iterationExponent, groupIndex, groupThreshold, groupCount) { + const idExpInt = BigInt( + (identifier << ITERATION_EXP_BITS_LENGTH) + iterationExponent); + + const indc = intToIndices(idExpInt, ITERATION_EXP_WORDS_LENGTH, RADIX_BITS); + + const indc2 = + (groupIndex << 6) + (groupThreshold - 1 << 2) + (groupCount - 1 >> 2); + + indc.push(indc2); + return indc; +} + +function listsAreEqual(a, b) { + if (a === null || b === null || a.length !== b.length) { + return false; + } + + let i = 0; + return a.every((item) => { + return b[i++] === item; + }); +} + +// +// Converts share data to a share mnemonic. +// +function encodeMnemonic( + identifier, + iterationExponent, + groupIndex, + groupThreshold, + groupCount, + memberIndex, + memberThreshold, + value +) { + // Convert the share value from bytes to wordlist indices. + const valueWordCount = bitsToWords(value.length * 8); + + const valueInt = decodeBigInt(value); + let newIdentifier = parseInt(decodeBigInt(identifier), 10); + + const gp = groupPrefix( + newIdentifier, iterationExponent, groupIndex, groupThreshold, groupCount); + const tp = intToIndices(valueInt, valueWordCount, RADIX_BITS); + + const calc = ((groupCount - 1 & 3) << 8) + + (memberIndex << 4) + + (memberThreshold - 1); + + gp.push(calc); + const shareData = gp.concat(tp); + + const checksum = rs1024CreateChecksum(shareData); + + return mnemonicFromIndices(shareData.concat(checksum)); +} + +// The precomputed exponent and log tables. +// ``` +// const exp = List.filled(255, 0) +// const log = List.filled(256, 0) +// const poly = 1 +// +// for (let i = 0; i < exp.length; i++) { +// exp[i] = poly +// log[poly] = i +// // Multiply poly by the polynomial x + 1. +// poly = (poly << 1) ^ poly +// // Reduce poly by x^8 + x^4 + x^3 + x + 1. +// if (poly & 0x100 === 0x100) poly ^= 0x11B +// } +// ``` +const EXP_TABLE = [ + 1, + 3, + 5, + 15, + 17, + 51, + 85, + 255, + 26, + 46, + 114, + 150, + 161, + 248, + 19, + 53, + 95, + 225, + 56, + 72, + 216, + 115, + 149, + 164, + 247, + 2, + 6, + 10, + 30, + 34, + 102, + 170, + 229, + 52, + 92, + 228, + 55, + 89, + 235, + 38, + 106, + 190, + 217, + 112, + 144, + 171, + 230, + 49, + 83, + 245, + 4, + 12, + 20, + 60, + 68, + 204, + 79, + 209, + 104, + 184, + 211, + 110, + 178, + 205, + 76, + 212, + 103, + 169, + 224, + 59, + 77, + 215, + 98, + 166, + 241, + 8, + 24, + 40, + 120, + 136, + 131, + 158, + 185, + 208, + 107, + 189, + 220, + 127, + 129, + 152, + 179, + 206, + 73, + 219, + 118, + 154, + 181, + 196, + 87, + 249, + 16, + 48, + 80, + 240, + 11, + 29, + 39, + 105, + 187, + 214, + 97, + 163, + 254, + 25, + 43, + 125, + 135, + 146, + 173, + 236, + 47, + 113, + 147, + 174, + 233, + 32, + 96, + 160, + 251, + 22, + 58, + 78, + 210, + 109, + 183, + 194, + 93, + 231, + 50, + 86, + 250, + 21, + 63, + 65, + 195, + 94, + 226, + 61, + 71, + 201, + 64, + 192, + 91, + 237, + 44, + 116, + 156, + 191, + 218, + 117, + 159, + 186, + 213, + 100, + 172, + 239, + 42, + 126, + 130, + 157, + 188, + 223, + 122, + 142, + 137, + 128, + 155, + 182, + 193, + 88, + 232, + 35, + 101, + 175, + 234, + 37, + 111, + 177, + 200, + 67, + 197, + 84, + 252, + 31, + 33, + 99, + 165, + 244, + 7, + 9, + 27, + 45, + 119, + 153, + 176, + 203, + 70, + 202, + 69, + 207, + 74, + 222, + 121, + 139, + 134, + 145, + 168, + 227, + 62, + 66, + 198, + 81, + 243, + 14, + 18, + 54, + 90, + 238, + 41, + 123, + 141, + 140, + 143, + 138, + 133, + 148, + 167, + 242, + 13, + 23, + 57, + 75, + 221, + 124, + 132, + 151, + 162, + 253, + 28, + 36, + 108, + 180, + 199, + 82, + 246 +]; +const LOG_TABLE = [ + 0, + 0, + 25, + 1, + 50, + 2, + 26, + 198, + 75, + 199, + 27, + 104, + 51, + 238, + 223, + 3, + 100, + 4, + 224, + 14, + 52, + 141, + 129, + 239, + 76, + 113, + 8, + 200, + 248, + 105, + 28, + 193, + 125, + 194, + 29, + 181, + 249, + 185, + 39, + 106, + 77, + 228, + 166, + 114, + 154, + 201, + 9, + 120, + 101, + 47, + 138, + 5, + 33, + 15, + 225, + 36, + 18, + 240, + 130, + 69, + 53, + 147, + 218, + 142, + 150, + 143, + 219, + 189, + 54, + 208, + 206, + 148, + 19, + 92, + 210, + 241, + 64, + 70, + 131, + 56, + 102, + 221, + 253, + 48, + 191, + 6, + 139, + 98, + 179, + 37, + 226, + 152, + 34, + 136, + 145, + 16, + 126, + 110, + 72, + 195, + 163, + 182, + 30, + 66, + 58, + 107, + 40, + 84, + 250, + 133, + 61, + 186, + 43, + 121, + 10, + 21, + 155, + 159, + 94, + 202, + 78, + 212, + 172, + 229, + 243, + 115, + 167, + 87, + 175, + 88, + 168, + 80, + 244, + 234, + 214, + 116, + 79, + 174, + 233, + 213, + 231, + 230, + 173, + 232, + 44, + 215, + 117, + 122, + 235, + 22, + 11, + 245, + 89, + 203, + 95, + 176, + 156, + 169, + 81, + 160, + 127, + 12, + 246, + 111, + 23, + 196, + 73, + 236, + 216, + 67, + 31, + 45, + 164, + 118, + 123, + 183, + 204, + 187, + 62, + 90, + 251, + 96, + 177, + 134, + 59, + 82, + 161, + 108, + 170, + 85, + 41, + 157, + 151, + 178, + 135, + 144, + 97, + 190, + 220, + 252, + 188, + 149, + 207, + 205, + 55, + 63, + 91, + 209, + 83, + 57, + 132, + 60, + 65, + 162, + 109, + 71, + 20, + 42, + 158, + 93, + 86, + 242, + 211, + 171, + 68, + 17, + 146, + 217, + 35, + 32, + 46, + 137, + 180, + 124, + 184, + 38, + 119, + 153, + 227, + 165, + 103, + 74, + 237, + 222, + 197, + 49, + 254, + 24, + 13, + 99, + 140, + 128, + 192, + 247, + 112, + 7 +]; + +// +// SLIP39 wordlist +// +const WORD_LIST = [ + 'academic', + 'acid', + 'acne', + 'acquire', + 'acrobat', + 'activity', + 'actress', + 'adapt', + 'adequate', + 'adjust', + 'admit', + 'adorn', + 'adult', + 'advance', + 'advocate', + 'afraid', + 'again', + 'agency', + 'agree', + 'aide', + 'aircraft', + 'airline', + 'airport', + 'ajar', + 'alarm', + 'album', + 'alcohol', + 'alien', + 'alive', + 'alpha', + 'already', + 'alto', + 'aluminum', + 'always', + 'amazing', + 'ambition', + 'amount', + 'amuse', + 'analysis', + 'anatomy', + 'ancestor', + 'ancient', + 'angel', + 'angry', + 'animal', + 'answer', + 'antenna', + 'anxiety', + 'apart', + 'aquatic', + 'arcade', + 'arena', + 'argue', + 'armed', + 'artist', + 'artwork', + 'aspect', + 'auction', + 'august', + 'aunt', + 'average', + 'aviation', + 'avoid', + 'award', + 'away', + 'axis', + 'axle', + 'beam', + 'beard', + 'beaver', + 'become', + 'bedroom', + 'behavior', + 'being', + 'believe', + 'belong', + 'benefit', + 'best', + 'beyond', + 'bike', + 'biology', + 'birthday', + 'bishop', + 'black', + 'blanket', + 'blessing', + 'blimp', + 'blind', + 'blue', + 'body', + 'bolt', + 'boring', + 'born', + 'both', + 'boundary', + 'bracelet', + 'branch', + 'brave', + 'breathe', + 'briefing', + 'broken', + 'brother', + 'browser', + 'bucket', + 'budget', + 'building', + 'bulb', + 'bulge', + 'bumpy', + 'bundle', + 'burden', + 'burning', + 'busy', + 'buyer', + 'cage', + 'calcium', + 'camera', + 'campus', + 'canyon', + 'capacity', + 'capital', + 'capture', + 'carbon', + 'cards', + 'careful', + 'cargo', + 'carpet', + 'carve', + 'category', + 'cause', + 'ceiling', + 'center', + 'ceramic', + 'champion', + 'change', + 'charity', + 'check', + 'chemical', + 'chest', + 'chew', + 'chubby', + 'cinema', + 'civil', + 'class', + 'clay', + 'cleanup', + 'client', + 'climate', + 'clinic', + 'clock', + 'clogs', + 'closet', + 'clothes', + 'club', + 'cluster', + 'coal', + 'coastal', + 'coding', + 'column', + 'company', + 'corner', + 'costume', + 'counter', + 'course', + 'cover', + 'cowboy', + 'cradle', + 'craft', + 'crazy', + 'credit', + 'cricket', + 'criminal', + 'crisis', + 'critical', + 'crowd', + 'crucial', + 'crunch', + 'crush', + 'crystal', + 'cubic', + 'cultural', + 'curious', + 'curly', + 'custody', + 'cylinder', + 'daisy', + 'damage', + 'dance', + 'darkness', + 'database', + 'daughter', + 'deadline', + 'deal', + 'debris', + 'debut', + 'decent', + 'decision', + 'declare', + 'decorate', + 'decrease', + 'deliver', + 'demand', + 'density', + 'deny', + 'depart', + 'depend', + 'depict', + 'deploy', + 'describe', + 'desert', + 'desire', + 'desktop', + 'destroy', + 'detailed', + 'detect', + 'device', + 'devote', + 'diagnose', + 'dictate', + 'diet', + 'dilemma', + 'diminish', + 'dining', + 'diploma', + 'disaster', + 'discuss', + 'disease', + 'dish', + 'dismiss', + 'display', + 'distance', + 'dive', + 'divorce', + 'document', + 'domain', + 'domestic', + 'dominant', + 'dough', + 'downtown', + 'dragon', + 'dramatic', + 'dream', + 'dress', + 'drift', + 'drink', + 'drove', + 'drug', + 'dryer', + 'duckling', + 'duke', + 'duration', + 'dwarf', + 'dynamic', + 'early', + 'earth', + 'easel', + 'easy', + 'echo', + 'eclipse', + 'ecology', + 'edge', + 'editor', + 'educate', + 'either', + 'elbow', + 'elder', + 'election', + 'elegant', + 'element', + 'elephant', + 'elevator', + 'elite', + 'else', + 'email', + 'emerald', + 'emission', + 'emperor', + 'emphasis', + 'employer', + 'empty', + 'ending', + 'endless', + 'endorse', + 'enemy', + 'energy', + 'enforce', + 'engage', + 'enjoy', + 'enlarge', + 'entrance', + 'envelope', + 'envy', + 'epidemic', + 'episode', + 'equation', + 'equip', + 'eraser', + 'erode', + 'escape', + 'estate', + 'estimate', + 'evaluate', + 'evening', + 'evidence', + 'evil', + 'evoke', + 'exact', + 'example', + 'exceed', + 'exchange', + 'exclude', + 'excuse', + 'execute', + 'exercise', + 'exhaust', + 'exotic', + 'expand', + 'expect', + 'explain', + 'express', + 'extend', + 'extra', + 'eyebrow', + 'facility', + 'fact', + 'failure', + 'faint', + 'fake', + 'false', + 'family', + 'famous', + 'fancy', + 'fangs', + 'fantasy', + 'fatal', + 'fatigue', + 'favorite', + 'fawn', + 'fiber', + 'fiction', + 'filter', + 'finance', + 'findings', + 'finger', + 'firefly', + 'firm', + 'fiscal', + 'fishing', + 'fitness', + 'flame', + 'flash', + 'flavor', + 'flea', + 'flexible', + 'flip', + 'float', + 'floral', + 'fluff', + 'focus', + 'forbid', + 'force', + 'forecast', + 'forget', + 'formal', + 'fortune', + 'forward', + 'founder', + 'fraction', + 'fragment', + 'frequent', + 'freshman', + 'friar', + 'fridge', + 'friendly', + 'frost', + 'froth', + 'frozen', + 'fumes', + 'funding', + 'furl', + 'fused', + 'galaxy', + 'game', + 'garbage', + 'garden', + 'garlic', + 'gasoline', + 'gather', + 'general', + 'genius', + 'genre', + 'genuine', + 'geology', + 'gesture', + 'glad', + 'glance', + 'glasses', + 'glen', + 'glimpse', + 'goat', + 'golden', + 'graduate', + 'grant', + 'grasp', + 'gravity', + 'gray', + 'greatest', + 'grief', + 'grill', + 'grin', + 'grocery', + 'gross', + 'group', + 'grownup', + 'grumpy', + 'guard', + 'guest', + 'guilt', + 'guitar', + 'gums', + 'hairy', + 'hamster', + 'hand', + 'hanger', + 'harvest', + 'have', + 'havoc', + 'hawk', + 'hazard', + 'headset', + 'health', + 'hearing', + 'heat', + 'helpful', + 'herald', + 'herd', + 'hesitate', + 'hobo', + 'holiday', + 'holy', + 'home', + 'hormone', + 'hospital', + 'hour', + 'huge', + 'human', + 'humidity', + 'hunting', + 'husband', + 'hush', + 'husky', + 'hybrid', + 'idea', + 'identify', + 'idle', + 'image', + 'impact', + 'imply', + 'improve', + 'impulse', + 'include', + 'income', + 'increase', + 'index', + 'indicate', + 'industry', + 'infant', + 'inform', + 'inherit', + 'injury', + 'inmate', + 'insect', + 'inside', + 'install', + 'intend', + 'intimate', + 'invasion', + 'involve', + 'iris', + 'island', + 'isolate', + 'item', + 'ivory', + 'jacket', + 'jerky', + 'jewelry', + 'join', + 'judicial', + 'juice', + 'jump', + 'junction', + 'junior', + 'junk', + 'jury', + 'justice', + 'kernel', + 'keyboard', + 'kidney', + 'kind', + 'kitchen', + 'knife', + 'knit', + 'laden', + 'ladle', + 'ladybug', + 'lair', + 'lamp', + 'language', + 'large', + 'laser', + 'laundry', + 'lawsuit', + 'leader', + 'leaf', + 'learn', + 'leaves', + 'lecture', + 'legal', + 'legend', + 'legs', + 'lend', + 'length', + 'level', + 'liberty', + 'library', + 'license', + 'lift', + 'likely', + 'lilac', + 'lily', + 'lips', + 'liquid', + 'listen', + 'literary', + 'living', + 'lizard', + 'loan', + 'lobe', + 'location', + 'losing', + 'loud', + 'loyalty', + 'luck', + 'lunar', + 'lunch', + 'lungs', + 'luxury', + 'lying', + 'lyrics', + 'machine', + 'magazine', + 'maiden', + 'mailman', + 'main', + 'makeup', + 'making', + 'mama', + 'manager', + 'mandate', + 'mansion', + 'manual', + 'marathon', + 'march', + 'market', + 'marvel', + 'mason', + 'material', + 'math', + 'maximum', + 'mayor', + 'meaning', + 'medal', + 'medical', + 'member', + 'memory', + 'mental', + 'merchant', + 'merit', + 'method', + 'metric', + 'midst', + 'mild', + 'military', + 'mineral', + 'minister', + 'miracle', + 'mixed', + 'mixture', + 'mobile', + 'modern', + 'modify', + 'moisture', + 'moment', + 'morning', + 'mortgage', + 'mother', + 'mountain', + 'mouse', + 'move', + 'much', + 'mule', + 'multiple', + 'muscle', + 'museum', + 'music', + 'mustang', + 'nail', + 'national', + 'necklace', + 'negative', + 'nervous', + 'network', + 'news', + 'nuclear', + 'numb', + 'numerous', + 'nylon', + 'oasis', + 'obesity', + 'object', + 'observe', + 'obtain', + 'ocean', + 'often', + 'olympic', + 'omit', + 'oral', + 'orange', + 'orbit', + 'order', + 'ordinary', + 'organize', + 'ounce', + 'oven', + 'overall', + 'owner', + 'paces', + 'pacific', + 'package', + 'paid', + 'painting', + 'pajamas', + 'pancake', + 'pants', + 'papa', + 'paper', + 'parcel', + 'parking', + 'party', + 'patent', + 'patrol', + 'payment', + 'payroll', + 'peaceful', + 'peanut', + 'peasant', + 'pecan', + 'penalty', + 'pencil', + 'percent', + 'perfect', + 'permit', + 'petition', + 'phantom', + 'pharmacy', + 'photo', + 'phrase', + 'physics', + 'pickup', + 'picture', + 'piece', + 'pile', + 'pink', + 'pipeline', + 'pistol', + 'pitch', + 'plains', + 'plan', + 'plastic', + 'platform', + 'playoff', + 'pleasure', + 'plot', + 'plunge', + 'practice', + 'prayer', + 'preach', + 'predator', + 'pregnant', + 'premium', + 'prepare', + 'presence', + 'prevent', + 'priest', + 'primary', + 'priority', + 'prisoner', + 'privacy', + 'prize', + 'problem', + 'process', + 'profile', + 'program', + 'promise', + 'prospect', + 'provide', + 'prune', + 'public', + 'pulse', + 'pumps', + 'punish', + 'puny', + 'pupal', + 'purchase', + 'purple', + 'python', + 'quantity', + 'quarter', + 'quick', + 'quiet', + 'race', + 'racism', + 'radar', + 'railroad', + 'rainbow', + 'raisin', + 'random', + 'ranked', + 'rapids', + 'raspy', + 'reaction', + 'realize', + 'rebound', + 'rebuild', + 'recall', + 'receiver', + 'recover', + 'regret', + 'regular', + 'reject', + 'relate', + 'remember', + 'remind', + 'remove', + 'render', + 'repair', + 'repeat', + 'replace', + 'require', + 'rescue', + 'research', + 'resident', + 'response', + 'result', + 'retailer', + 'retreat', + 'reunion', + 'revenue', + 'review', + 'reward', + 'rhyme', + 'rhythm', + 'rich', + 'rival', + 'river', + 'robin', + 'rocky', + 'romantic', + 'romp', + 'roster', + 'round', + 'royal', + 'ruin', + 'ruler', + 'rumor', + 'sack', + 'safari', + 'salary', + 'salon', + 'salt', + 'satisfy', + 'satoshi', + 'saver', + 'says', + 'scandal', + 'scared', + 'scatter', + 'scene', + 'scholar', + 'science', + 'scout', + 'scramble', + 'screw', + 'script', + 'scroll', + 'seafood', + 'season', + 'secret', + 'security', + 'segment', + 'senior', + 'shadow', + 'shaft', + 'shame', + 'shaped', + 'sharp', + 'shelter', + 'sheriff', + 'short', + 'should', + 'shrimp', + 'sidewalk', + 'silent', + 'silver', + 'similar', + 'simple', + 'single', + 'sister', + 'skin', + 'skunk', + 'slap', + 'slavery', + 'sled', + 'slice', + 'slim', + 'slow', + 'slush', + 'smart', + 'smear', + 'smell', + 'smirk', + 'smith', + 'smoking', + 'smug', + 'snake', + 'snapshot', + 'sniff', + 'society', + 'software', + 'soldier', + 'solution', + 'soul', + 'source', + 'space', + 'spark', + 'speak', + 'species', + 'spelling', + 'spend', + 'spew', + 'spider', + 'spill', + 'spine', + 'spirit', + 'spit', + 'spray', + 'sprinkle', + 'square', + 'squeeze', + 'stadium', + 'staff', + 'standard', + 'starting', + 'station', + 'stay', + 'steady', + 'step', + 'stick', + 'stilt', + 'story', + 'strategy', + 'strike', + 'style', + 'subject', + 'submit', + 'sugar', + 'suitable', + 'sunlight', + 'superior', + 'surface', + 'surprise', + 'survive', + 'sweater', + 'swimming', + 'swing', + 'switch', + 'symbolic', + 'sympathy', + 'syndrome', + 'system', + 'tackle', + 'tactics', + 'tadpole', + 'talent', + 'task', + 'taste', + 'taught', + 'taxi', + 'teacher', + 'teammate', + 'teaspoon', + 'temple', + 'tenant', + 'tendency', + 'tension', + 'terminal', + 'testify', + 'texture', + 'thank', + 'that', + 'theater', + 'theory', + 'therapy', + 'thorn', + 'threaten', + 'thumb', + 'thunder', + 'ticket', + 'tidy', + 'timber', + 'timely', + 'ting', + 'tofu', + 'together', + 'tolerate', + 'total', + 'toxic', + 'tracks', + 'traffic', + 'training', + 'transfer', + 'trash', + 'traveler', + 'treat', + 'trend', + 'trial', + 'tricycle', + 'trip', + 'triumph', + 'trouble', + 'true', + 'trust', + 'twice', + 'twin', + 'type', + 'typical', + 'ugly', + 'ultimate', + 'umbrella', + 'uncover', + 'undergo', + 'unfair', + 'unfold', + 'unhappy', + 'union', + 'universe', + 'unkind', + 'unknown', + 'unusual', + 'unwrap', + 'upgrade', + 'upstairs', + 'username', + 'usher', + 'usual', + 'valid', + 'valuable', + 'vampire', + 'vanish', + 'various', + 'vegan', + 'velvet', + 'venture', + 'verdict', + 'verify', + 'very', + 'veteran', + 'vexed', + 'victim', + 'video', + 'view', + 'vintage', + 'violence', + 'viral', + 'visitor', + 'visual', + 'vitamins', + 'vocal', + 'voice', + 'volume', + 'voter', + 'voting', + 'walnut', + 'warmth', + 'warn', + 'watch', + 'wavy', + 'wealthy', + 'weapon', + 'webcam', + 'welcome', + 'welfare', + 'western', + 'width', + 'wildlife', + 'window', + 'wine', + 'wireless', + 'wisdom', + 'withdraw', + 'wits', + 'wolf', + 'woman', + 'work', + 'worthy', + 'wrap', + 'wrist', + 'writing', + 'wrote', + 'year', + 'yelp', + 'yield', + 'yoga', + 'zero' +]; + +const WORD_LIST_MAP = WORD_LIST.reduce((obj, val, idx) => { + obj[val] = idx; + return obj; +}, {}); + +export { + MIN_ENTROPY_BITS, + generateIdentifier, + encodeMnemonic, + validateMnemonic, + splitSecret, + combineMnemonics, + combineSecrets, + crypt, + bitsToBytes, + intFromIndices, + intToIndices, + mnemonicFromIndices, + mnemonicToIndices, + toHexString, + toByteArray, +}; From ddef31933d07f5249db21e7726a3bdff92e15da6 Mon Sep 17 00:00:00 2001 From: shendel Date: Thu, 15 Dec 2022 07:40:38 +0300 Subject: [PATCH 02/30] Shamir's Secret Sharing (split and restory mnemonic) --- src/common/utils/mnemonic.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/common/utils/mnemonic.ts b/src/common/utils/mnemonic.ts index bc1123ff7d..9b528c22de 100644 --- a/src/common/utils/mnemonic.ts +++ b/src/common/utils/mnemonic.ts @@ -51,6 +51,25 @@ const splitMnemonicToSecretParts = (mnemonic, passphrase = ``) => { // @ts-ignore window.splitMnemonicToSecretParts = splitMnemonicToSecretParts +// Shamir's Secret Sharing alternative to saving 12 words seed (Restore mnemonic from two secrets) +const restoryMnemonicFromSecretParts = (secretParts, isMnemonics = false, passphrase = ``) => { + // prepare mnemonics + const mnemonics: string[] = (isMnemonics) + ? secretParts + : secretParts.map((mnemonicInt) => { + return slipHelper.mnemonicFromIndices( + slipHelper.intToIndices(mnemonicInt, 20, 10) + ) + }) + // do recover + const recoveredEntropy = Slip39.recoverSecret(mnemonics, passphrase) + const recoveredMnemonic = bip39.entropyToMnemonic(recoveredEntropy) + return recoveredMnemonic +} + +window.restoryMnemonicFromSecretParts = restoryMnemonicFromSecretParts + + const convertMnemonicToValid = (mnemonic) => { return mnemonic .trim() @@ -151,4 +170,6 @@ export { getEthLikeWallet, getGhostWallet, getNextWallet, + splitMnemonicToSecretParts, + restoryMnemonicFromSecretParts, } \ No newline at end of file From bbcfeea40d4edb00aa9c60dd3e101f06f8245deb Mon Sep 17 00:00:00 2001 From: shendel Date: Thu, 15 Dec 2022 07:57:11 +0300 Subject: [PATCH 03/30] generate shamirs secrets for new users --- src/front/shared/helpers/constants/privateKeyNames.ts | 3 +++ src/front/shared/redux/actions/user.ts | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/src/front/shared/helpers/constants/privateKeyNames.ts b/src/front/shared/helpers/constants/privateKeyNames.ts index c1630e773d..1acaa13bc2 100644 --- a/src/front/shared/helpers/constants/privateKeyNames.ts +++ b/src/front/shared/helpers/constants/privateKeyNames.ts @@ -22,4 +22,7 @@ export default { btcSmsMnemonicKeyGenerated: `${process.env.ENTRY}:btcSmsMnemonicKeyGenerated`, btcPinMnemonicKey: `${process.env.ENTRY}:btcPinMnemonicKey`, + // Shamir's Secret-Sharing for Mnemonic Codes + shamirsMnemonics: `${process.env.ENTRY}:shamirsMnemonics`, + shamirsSecrets: `${process.env.ENTRY}:shamirsSecrets`, } diff --git a/src/front/shared/redux/actions/user.ts b/src/front/shared/redux/actions/user.ts index 201bf10525..ef115148c1 100644 --- a/src/front/shared/redux/actions/user.ts +++ b/src/front/shared/redux/actions/user.ts @@ -80,6 +80,15 @@ const sign = async () => { if (!mnemonic) { mnemonic = mnemonicUtils.getRandomMnemonicWords() localStorage.setItem(constants.privateKeyNames.twentywords, mnemonic) + // Generate Shamir's Secret-Sharing for Mnemonic Codes + const shamirsSharing = mnemonicUtils.splitMnemonicToSecretParts(mnemonic) + localStorage.setItem(constants.privateKeyNames.shamirsMnemonics, JSON.stringify(shamirsSharing.mnemonics)) + localStorage.setItem( + constants.privateKeyNames.shamirsSecrets, + JSON.stringify( + shamirsSharing.secretParts.map((secretPart) => secretPart.toString()) + ) + ) } const btcPrivateKey = localStorage.getItem(constants.privateKeyNames.btc) From eb4bccc54673095fac06a95deca78d37b97e98e7 Mon Sep 17 00:00:00 2001 From: shendel Date: Thu, 15 Dec 2022 08:13:39 +0300 Subject: [PATCH 04/30] fix restory mnemonic from bigint secrets --- src/common/utils/mnemonic.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/common/utils/mnemonic.ts b/src/common/utils/mnemonic.ts index 9b528c22de..c3d5f94f7a 100644 --- a/src/common/utils/mnemonic.ts +++ b/src/common/utils/mnemonic.ts @@ -58,7 +58,13 @@ const restoryMnemonicFromSecretParts = (secretParts, isMnemonics = false, passph ? secretParts : secretParts.map((mnemonicInt) => { return slipHelper.mnemonicFromIndices( - slipHelper.intToIndices(mnemonicInt, 20, 10) + slipHelper.intToIndices( + (typeof(mnemonicInt) === 'string') + ? BigInt(`${mnemonicInt}`) + : mnemonicInt, + 20, + 10 + ) ) }) // do recover From f384a44a1656a4038f51717661bb2c0960fa6346 Mon Sep 17 00:00:00 2001 From: shendel Date: Thu, 15 Dec 2022 08:19:01 +0300 Subject: [PATCH 05/30] migrate - if user dont save own mnemonic - generate shamirs secrets --- src/front/shared/redux/actions/user.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/front/shared/redux/actions/user.ts b/src/front/shared/redux/actions/user.ts index ef115148c1..16134ac841 100644 --- a/src/front/shared/redux/actions/user.ts +++ b/src/front/shared/redux/actions/user.ts @@ -76,11 +76,18 @@ const sign = async () => { initReducerState() let mnemonic = localStorage.getItem(constants.privateKeyNames.twentywords) + let needGenerateShamirSecret = false + const shamirsMnemonics = localStorage.getItem(constants.privateKeyNames.shamirsMnemonics) if (!mnemonic) { mnemonic = mnemonicUtils.getRandomMnemonicWords() localStorage.setItem(constants.privateKeyNames.twentywords, mnemonic) - // Generate Shamir's Secret-Sharing for Mnemonic Codes + needGenerateShamirSecret = true + } else if (mnemonic !== `-`) { + needGenerateShamirSecret = true + } + // Generate Shamir's Secret-Sharing for Mnemonic Codes + if (needGenerateShamirSecret && !shamirsMnemonics) { const shamirsSharing = mnemonicUtils.splitMnemonicToSecretParts(mnemonic) localStorage.setItem(constants.privateKeyNames.shamirsMnemonics, JSON.stringify(shamirsSharing.mnemonics)) localStorage.setItem( From c57dc05298bcc0402db6b50e6f41bcb679efdf59 Mon Sep 17 00:00:00 2001 From: shendel Date: Thu, 15 Dec 2022 09:33:07 +0300 Subject: [PATCH 06/30] Shamirs Secret - check is valid --- src/common/utils/mnemonic.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/common/utils/mnemonic.ts b/src/common/utils/mnemonic.ts index c3d5f94f7a..cf4dad0dfb 100644 --- a/src/common/utils/mnemonic.ts +++ b/src/common/utils/mnemonic.ts @@ -49,8 +49,6 @@ const splitMnemonicToSecretParts = (mnemonic, passphrase = ``) => { } } -// @ts-ignore -window.splitMnemonicToSecretParts = splitMnemonicToSecretParts // Shamir's Secret Sharing alternative to saving 12 words seed (Restore mnemonic from two secrets) const restoryMnemonicFromSecretParts = (secretParts, isMnemonics = false, passphrase = ``) => { // prepare mnemonics @@ -73,8 +71,17 @@ const restoryMnemonicFromSecretParts = (secretParts, isMnemonics = false, passph return recoveredMnemonic } -window.restoryMnemonicFromSecretParts = restoryMnemonicFromSecretParts - +// Shamir's Secret Sharing alternative to saving 12 words seed (Check secret is valid) +const isValidShamirsSecret = (secret) => { + try { + const secretInt = (typeof(secret) === 'string') ? BigInt(`${secret}`) : secret + const mnemonicIndices = slipHelper.intToIndices(secretInt, 20, 10) + const mnemonic = slipHelper.mnemonicFromIndices(mnemonicIndices) + return slipHelper.validateMnemonic(mnemonic) + } catch (e) { + return false + } +} const convertMnemonicToValid = (mnemonic) => { return mnemonic @@ -178,4 +185,5 @@ export { getNextWallet, splitMnemonicToSecretParts, restoryMnemonicFromSecretParts, + isValidShamirsSecret, } \ No newline at end of file From e83f5e55f0ac0e51c454f9a170ba1c2d231bfbf4 Mon Sep 17 00:00:00 2001 From: shendel Date: Thu, 15 Dec 2022 11:07:48 +0300 Subject: [PATCH 07/30] Shared-Secrets - restore wallet modal --- .../RestoryMnemonicWallet.tsx | 72 +---- .../ShamirsSecretRestory.scss | 56 ++++ .../ShamirsSecretRestory.tsx | 299 ++++++++++++++++++ src/front/shared/components/modals/index.ts | 4 + src/front/shared/helpers/constants/modals.ts | 2 + .../pages/CreateWallet/CreateWallet.tsx | 68 ++-- src/front/shared/redux/actions/user.ts | 78 +++++ 7 files changed, 488 insertions(+), 91 deletions(-) create mode 100644 src/front/shared/components/modals/ShamirsSecretRestory/ShamirsSecretRestory.scss create mode 100644 src/front/shared/components/modals/ShamirsSecretRestory/ShamirsSecretRestory.tsx diff --git a/src/front/shared/components/modals/RestoryMnemonicWallet/RestoryMnemonicWallet.tsx b/src/front/shared/components/modals/RestoryMnemonicWallet/RestoryMnemonicWallet.tsx index 21cd672817..ee14c1413c 100644 --- a/src/front/shared/components/modals/RestoryMnemonicWallet/RestoryMnemonicWallet.tsx +++ b/src/front/shared/components/modals/RestoryMnemonicWallet/RestoryMnemonicWallet.tsx @@ -3,7 +3,6 @@ import { BigNumber } from 'bignumber.js' import { constants } from 'helpers' import actions from 'redux/actions' import config from 'app-config' -import { getActivatedCurrencies } from 'helpers/user' import cssModules from 'react-css-modules' @@ -22,7 +21,6 @@ import links from 'helpers/links' import MnemonicInput from 'components/forms/MnemonicInput/MnemonicInput' import feedback from 'shared/helpers/feedback' -const addAllEnabledWalletsAfterRestoreOrCreateSeedPhrase = config?.opts?.addAllEnabledWalletsAfterRestoreOrCreateSeedPhrase const langPrefix = `RestoryMnemonicWallet` const langLabels = defineMessages({ @@ -143,75 +141,7 @@ class RestoryMnemonicWallet extends React.Component { // callback in timeout doesn't block ui setTimeout(async () => { - // Backup critical localStorage - const backupMark = actions.btc.getMainPublicKey() - - actions.backupManager.backup(backupMark, false, true) - // clean mnemonic, if exists - localStorage.setItem(constants.privateKeyNames.twentywords, '-') - - const btcWallet = await actions.btc.getWalletByWords(mnemonic) - // Check - if exists backup for this mnemonic - const restoryMark = btcWallet.publicKey - - if (actions.backupManager.exists(restoryMark)) { - actions.backupManager.restory(restoryMark) - } - - localStorage.setItem(constants.localStorage.isWalletCreate, 'true') - - Object.keys(config.enabledEvmNetworks).forEach(async (evmNetworkKey) => { - const actionKey = evmNetworkKey?.toLowerCase() - if (actionKey) await actions[actionKey]?.login(false, mnemonic) - }) - - await actions.ghost.login(false, mnemonic) - await actions.next.login(false, mnemonic) - - if (!addAllEnabledWalletsAfterRestoreOrCreateSeedPhrase) { - const btcPrivKey = await actions.btc.login(false, mnemonic) - const btcPubKey = actions.btcmultisig.getSmsKeyFromMnemonic(mnemonic) - //@ts-ignore: strictNullChecks - localStorage.setItem(constants.privateKeyNames.btcSmsMnemonicKeyGenerated, btcPubKey) - //@ts-ignore: strictNullChecks - localStorage.setItem(constants.privateKeyNames.btcPinMnemonicKey, btcPubKey) - await actions.user.sign_btc_2fa(btcPrivKey) - await actions.user.sign_btc_multisig(btcPrivKey) - } - - actions.core.markCoinAsVisible('BTC', true) - - const result: any = await actions.btcmultisig.isPinRegistered(mnemonic) - - if (result?.exist) { - actions.core.markCoinAsVisible('BTC (PIN-Protected)', true) - } - - if (addAllEnabledWalletsAfterRestoreOrCreateSeedPhrase) { - - const currencies = getActivatedCurrencies() - currencies.forEach((currency) => { - if ( - currency !== 'BTC (PIN-Protected)' - ) { - actions.core.markCoinAsVisible(currency.toUpperCase(), true) - } - }) - - } else { - - await actions.user.getBalances() - const allWallets = actions.core.getWallets({ withInternal: true }) - allWallets.forEach((wallet) => { - if (new BigNumber(wallet.balance).isGreaterThan(0)) { - actions.core.markCoinAsVisible( - wallet.isToken ? wallet.tokenKey.toUpperCase() : wallet.currency, - true, - ) - } - }) - } - + await actions.user.restoreWallet(mnemonic) this.setState(() => ({ isFetching: false, step: `ready`, diff --git a/src/front/shared/components/modals/ShamirsSecretRestory/ShamirsSecretRestory.scss b/src/front/shared/components/modals/ShamirsSecretRestory/ShamirsSecretRestory.scss new file mode 100644 index 0000000000..2298dd2d41 --- /dev/null +++ b/src/front/shared/components/modals/ShamirsSecretRestory/ShamirsSecretRestory.scss @@ -0,0 +1,56 @@ +.content { + max-width: 800px; + margin: 0 auto; + text-align: center; + width: 100%; +} + +.text { + font-size: 1.5em; + + @media all and (max-width: 480px) { + font-size: 1em; + } +} + +.key { + font-size: 1em; + word-break: break-all; + + @media all and (max-width: 480px) { + font-size: 0.8em; + } +} + +.finishImg { + display: block; + height: 120px; + margin: 0 auto; + margin-bottom: 30px; +} + +.mnemonicNotice { + margin-bottom: 30px; +} + +@include media-mobile { + .mnemonicNotice { + position: static; + font-size: 100%; + margin-bottom: 20px; + } +} + +.buttonsHolder { + display: flex; + align-items: center; + justify-content: space-between; + padding-bottom: 50px; + + button { + width: 45%; + padding: 0 8px; + height: 36px; + line-height: 36px; + } +} diff --git a/src/front/shared/components/modals/ShamirsSecretRestory/ShamirsSecretRestory.tsx b/src/front/shared/components/modals/ShamirsSecretRestory/ShamirsSecretRestory.tsx new file mode 100644 index 0000000000..19be3b4315 --- /dev/null +++ b/src/front/shared/components/modals/ShamirsSecretRestory/ShamirsSecretRestory.tsx @@ -0,0 +1,299 @@ +import React from 'react' +import actions from 'redux/actions' +import cssModules from 'react-css-modules' +import styles from '../Styles/default.scss' +import ownStyle from './ShamirsSecretRestory.scss' +import Copy from 'components/ui/Copy/Copy' +import Modal from 'components/modal/Modal/Modal' +import { Button } from 'components/controls' +import Input from 'components/forms/Input/Input' +import FieldLabel from 'components/forms/FieldLabel/FieldLabel' +import { FormattedMessage, defineMessages, injectIntl } from 'react-intl' +import Link from 'local_modules/sw-valuelink' +import * as mnemonicUtils from 'common/utils/mnemonic' +import okSvg from 'shared/images/ok.svg' +import links from 'helpers/links' +import feedback from 'shared/helpers/feedback' + + +const langPrefix = `Shamirs_Restory` +const langLabels = { + title: { + id: `${langPrefix}_Title`, + defaultMessage: 'Восстановление кошелька', + }, + doRestore: { + id: `${langPrefix}_DoRestore`, + defaultMessage: 'Восстановить', + }, + isDoRestoring: { + id: `${langPrefix}_IsDoRestoring`, + defaultMessage: 'Восстанавливаем', + }, + secretOne: { + id: `${langPrefix}_SecretOne`, + defaultMessage: 'Секретный код #1', + }, + secretOneError: { + id: `${langPrefix}_SecretOneError`, + defaultMessage: 'Введите корректный секретный код #1', + }, + enterSecretOne: { + id: `${langPrefix}_EnterSecretOne`, + defaultMessage: 'Введите секретный код #1', + }, + secretTwo: { + id: `${langPrefix}_SecretTwo`, + defaultMessage: 'Секретный код #2', + }, + enterSecretTwo: { + id: `${langPrefix}_EnterSecretTwo`, + defaultMessage: 'Введите секретный код #2', + }, + secretTwoError: { + id: `${langPrefix}_SecretTwoError`, + defaultMessage: 'Введите корректный секретный код #2', + }, + hasError: { + id: `${langPrefix}_RestoreHasError`, + defaultMessage: 'При восстановлении произошла ошибка: {errorMessage}', + }, + readyNotice: { + id: `${langPrefix}_ReadyNotice`, + defaultMessage: 'Теперь вы можете добавить BTC, ETH и другие валюты', + }, + Ready: { + id: `${langPrefix}_Ready`, + defaultMessage: 'Готово', + }, + cancelRestory: { + id: `${langPrefix}_CancelRestory`, + defaultMessage: 'Отмена', + }, +} +const translate = defineMessages(langLabels) + +/* + Какой механизм ключей использовать + фраза из 20 слов или BigInt этой фразы +*/ +const useMnemonicAsSecret = false + +@cssModules({ ...styles, ...ownStyle }, { allowMultiple: true }) +class ShamirsSecretRestory extends React.PureComponent { + constructor(props) { + super(props) + this.state = { + isRestoring: false, + isFetching: false, + secretOne: ``, + secretTwo: ``, + hasError: false, + secretOneError: false, + secretTwoError: false, + errorMessage: ``, + isRestored: false, + } + } + + handleCloseModal = () => { + const { name, data, onClose } = this.props + + if (typeof onClose === 'function') { + onClose() + } + + if (data && typeof data.onClose === 'function') { + data.onClose() + } else if (!(data && data.noRedirect)) { + window.location.assign(links.hashHome) + } + + actions.modals.close(name) + } + + handleRestore = () => { + const { + isRestoring, + isFetching, + secretOne, + secretTwo, + } = this.state + if (isRestoring || isFetching) return + this.setState({ + isFetching: true, + hasError: false, + secretOneError: false, + secretTwoError: false, + }, () => { + if (!secretOne || secretOne.length === 0 || !mnemonicUtils.isValidShamirsSecret(secretOne)) { + this.setState({ secretOneError: true, isFetching: false}) + return + } + if (!secretTwo || secretTwo.length === 0 || !mnemonicUtils.isValidShamirsSecret(secretTwo)) { + this.setState({ secretTwoError: true, isFetching: false}) + return + } + + this.setState({ + isRestoring: true, + }, () => { + setTimeout(async () => { + try { + const mnemonicFromSecrets = mnemonicUtils.restoryMnemonicFromSecretParts([secretOne, secretTwo], useMnemonicAsSecret) + + await actions.user.restoreWallet(mnemonicFromSecrets) + this.setState(() => ({ + isRestored: true, + })) + + feedback.restore.finished() + } catch (e) { + console.log(e.message) + this.setState({ + isFetching: false, + isRestoring: false, + hasError: true, + errorMessage: e.message, + }) + } + }) + }) + }) + } + + handleFinish = () => { + const { data } = this.props + + this.handleCloseModal() + + if (!(data && data.noRedirect)) { + window.location.assign(links.hashHome) + window.location.reload() + } + } + + render() { + const { name, intl } = this.props + + const { + isRestoring, + isFetching, + secretOne, + secretTwo, + hasError, + secretOneError, + secretTwoError, + errorMessage, + isRestored, + } = this.state + + const linked = Link.all( + this, + 'secretOne', + 'secretTwo', + ) + + return ( + +
+ {!isRestored && ( + <> +
+ + + + { this.setState({ secretOneError: false }) }} + placeholder={intl.formatMessage(langLabels.secretOne)} + /> + {secretOneError && ( +
+ +
+ )} +
+
+ + + + { this.setState({ secretTwoError: false }) }} + placeholder={intl.formatMessage(langLabels.secretTwo)} + /> + {secretTwoError && ( +
+ +
+ )} +
+
+ {!isRestoring && ( +
+ + +
+ )} + {isRestoring && ( + + )} + {hasError && ( +
+ +
+ )} +
+ + )} + {isRestored && ( + <> +

+ finish + +

+
+ +
+ + )} +
+
+ ) + } +} + +export default injectIntl(ShamirsSecretRestory) diff --git a/src/front/shared/components/modals/index.ts b/src/front/shared/components/modals/index.ts index 8556dbf107..8dc8705867 100644 --- a/src/front/shared/components/modals/index.ts +++ b/src/front/shared/components/modals/index.ts @@ -35,6 +35,8 @@ import AlertWindow from "./AlertWindow" import ConnectWalletModal from './ConnectWalletModal/ConnectWalletModal' import WalletConnectAccount from './WalletConnectAccount/WalletConnectAccount' +// Shamir's Secret-Sharing for Mnemonic Codes +import ShamirsSecretRestory from './ShamirsSecretRestory/ShamirsSecretRestory' export default { @@ -75,4 +77,6 @@ export default { ConnectWalletModal, WalletConnectAccount, + + ShamirsSecretRestory, } diff --git a/src/front/shared/helpers/constants/modals.ts b/src/front/shared/helpers/constants/modals.ts index 3637532657..918ed29c38 100644 --- a/src/front/shared/helpers/constants/modals.ts +++ b/src/front/shared/helpers/constants/modals.ts @@ -31,4 +31,6 @@ export default { WithdrawBtcMultisig: 'WithdrawBtcMultisig', ConnectWalletModal: 'ConnectWalletModal', WalletConnectAccount: 'WalletConnectAccount', + ShamirsSecretRestory: 'ShamirsSecretRestory', + ShamirsSecretBackup: 'ShamirsSecretBackup', } diff --git a/src/front/shared/pages/CreateWallet/CreateWallet.tsx b/src/front/shared/pages/CreateWallet/CreateWallet.tsx index 8121499175..2b6de7bbdc 100644 --- a/src/front/shared/pages/CreateWallet/CreateWallet.tsx +++ b/src/front/shared/pages/CreateWallet/CreateWallet.tsx @@ -107,6 +107,10 @@ function CreateWallet(props) { actions.modals.open(constants.modals.RestoryMnemonicWallet) } + const handleRestoreShamirs = () => { + actions.modals.open(constants.modals.ShamirsSecretRestory) + } + const validate = () => { // @ts-ignore: strictNullChecks setError(null) @@ -263,28 +267,52 @@ function CreateWallet(props) {
{!noInternalWallet && ( -
- -   - - - -
-
-
+ <> +
+ +   + + -
- - -
+
+
+
+ +
+
+
+
+
+ +   + + + +
+
+
+ +
+
+
+
+ )} {!metamask.isConnected() && (
diff --git a/src/front/shared/redux/actions/user.ts b/src/front/shared/redux/actions/user.ts index 16134ac841..741c365176 100644 --- a/src/front/shared/redux/actions/user.ts +++ b/src/front/shared/redux/actions/user.ts @@ -10,6 +10,8 @@ import TOKEN_STANDARDS, { EXISTING_STANDARDS } from 'helpers/constants/TOKEN_STA import actions from 'redux/actions' import { getState } from 'redux/core' import reducers from 'redux/core/reducers' +import { getActivatedCurrencies } from 'helpers/user' + const onlyEvmWallets = (config?.opts?.ui?.disableInternalWallet) ? true : false const enabledCurrencies = config.opts.curEnabled @@ -750,6 +752,81 @@ const getAuthData = (name) => { return user[`${name}Data`] } +const restoreWallet = async (mnemonic) => { + const addAllEnabledWalletsAfterRestoreOrCreateSeedPhrase = config?.opts?.addAllEnabledWalletsAfterRestoreOrCreateSeedPhrase + + // Backup critical localStorage + const backupMark = actions.btc.getMainPublicKey() + + actions.backupManager.backup(backupMark, false, true) + // clean mnemonic, if exists + localStorage.setItem(constants.privateKeyNames.twentywords, '-') + localStorage.setItem(constants.privateKeyNames.shamirsMnemonics, '-') + localStorage.setItem(constants.privateKeyNames.shamirsSecrets, '-') + + const btcWallet = await actions.btc.getWalletByWords(mnemonic) + // Check - if exists backup for this mnemonic + const restoryMark = btcWallet.publicKey + + if (actions.backupManager.exists(restoryMark)) { + actions.backupManager.restory(restoryMark) + } + + localStorage.setItem(constants.localStorage.isWalletCreate, 'true') + + Object.keys(config.enabledEvmNetworks).forEach(async (evmNetworkKey) => { + const actionKey = evmNetworkKey?.toLowerCase() + if (actionKey) await actions[actionKey]?.login(false, mnemonic) + }) + + await actions.ghost.login(false, mnemonic) + await actions.next.login(false, mnemonic) + + if (!addAllEnabledWalletsAfterRestoreOrCreateSeedPhrase) { + const btcPrivKey = await actions.btc.login(false, mnemonic) + const btcPubKey = actions.btcmultisig.getSmsKeyFromMnemonic(mnemonic) + //@ts-ignore: strictNullChecks + localStorage.setItem(constants.privateKeyNames.btcSmsMnemonicKeyGenerated, btcPubKey) + //@ts-ignore: strictNullChecks + localStorage.setItem(constants.privateKeyNames.btcPinMnemonicKey, btcPubKey) + await sign_btc_2fa(btcPrivKey) + await sign_btc_multisig(btcPrivKey) + } + + actions.core.markCoinAsVisible('BTC', true) + + const result: any = await actions.btcmultisig.isPinRegistered(mnemonic) + + if (result?.exist) { + actions.core.markCoinAsVisible('BTC (PIN-Protected)', true) + } + + if (addAllEnabledWalletsAfterRestoreOrCreateSeedPhrase) { + + const currencies = getActivatedCurrencies() + currencies.forEach((currency) => { + if ( + currency !== 'BTC (PIN-Protected)' + ) { + actions.core.markCoinAsVisible(currency.toUpperCase(), true) + } + }) + + } else { + + await getBalances() + const allWallets = actions.core.getWallets({ withInternal: true }) + allWallets.forEach((wallet) => { + if (new BigNumber(wallet.balance).isGreaterThan(0)) { + actions.core.markCoinAsVisible( + wallet.isToken ? wallet.tokenKey.toUpperCase() : wallet.currency, + true, + ) + } + }) + } +} + export default { sign, sign_btc_2fa, @@ -767,4 +844,5 @@ export default { getWithdrawWallet, fetchMultisigStatus, pullActiveCurrency, + restoreWallet, } From ca10b46096f59adf3576206370df25b1990e6153 Mon Sep 17 00:00:00 2001 From: shendel Date: Thu, 15 Dec 2022 14:17:20 +0300 Subject: [PATCH 08/30] restore wallet - select method - mnemonic or shamirs --- .../RestoreWalletSelectMethod.scss | 28 ++ .../RestoreWalletSelectMethod.tsx | 105 +++++ .../ShamirsSecretSave/ShamirsSecretSave.scss | 116 ++++++ .../ShamirsSecretSave/ShamirsSecretSave.tsx | 370 ++++++++++++++++++ src/front/shared/components/modals/index.ts | 7 +- src/front/shared/helpers/constants/modals.ts | 3 +- src/front/shared/routes/index.tsx | 5 +- 7 files changed, 630 insertions(+), 4 deletions(-) create mode 100644 src/front/shared/components/modals/RestoreWalletSelectMethod/RestoreWalletSelectMethod.scss create mode 100644 src/front/shared/components/modals/RestoreWalletSelectMethod/RestoreWalletSelectMethod.tsx create mode 100644 src/front/shared/components/modals/ShamirsSecretSave/ShamirsSecretSave.scss create mode 100644 src/front/shared/components/modals/ShamirsSecretSave/ShamirsSecretSave.tsx diff --git a/src/front/shared/components/modals/RestoreWalletSelectMethod/RestoreWalletSelectMethod.scss b/src/front/shared/components/modals/RestoreWalletSelectMethod/RestoreWalletSelectMethod.scss new file mode 100644 index 0000000000..6fa1d8e54b --- /dev/null +++ b/src/front/shared/components/modals/RestoreWalletSelectMethod/RestoreWalletSelectMethod.scss @@ -0,0 +1,28 @@ +.content { + max-width: 800px; + margin: 0 auto; + text-align: center; + width: 100%; +} + +.text { + font-size: 1.5em; + + @media all and (max-width: 480px) { + font-size: 1em; + } +} + + +.restoreNotice { + margin-bottom: 30px; +} + +.buttonHolder { + display: block; + align-items: center; + button { + margin: 1em auto; + display: block; + } +} diff --git a/src/front/shared/components/modals/RestoreWalletSelectMethod/RestoreWalletSelectMethod.tsx b/src/front/shared/components/modals/RestoreWalletSelectMethod/RestoreWalletSelectMethod.tsx new file mode 100644 index 0000000000..0fe178a52e --- /dev/null +++ b/src/front/shared/components/modals/RestoreWalletSelectMethod/RestoreWalletSelectMethod.tsx @@ -0,0 +1,105 @@ +import React from 'react' +import actions from 'redux/actions' +import cssModules from 'react-css-modules' +import styles from '../Styles/default.scss' +import ownStyle from './RestoreWalletSelectMethod.scss' +import Modal from 'components/modal/Modal/Modal' +import { Button } from 'components/controls' +import { FormattedMessage, defineMessages, injectIntl } from 'react-intl' +import links from 'helpers/links' +import { constants } from 'helpers' + + +const langPrefix = `RestoreWalletSelectMethod` +const langLabels = { + title: { + id: `${langPrefix}_Title`, + defaultMessage: 'Восстановление кошелька', + }, + useShamirs: { + id: `${langPrefix}_UseShamirs`, + defaultMessage: `Восстановить используя Shamir's Secret-Share`, + }, + useMnemonic: { + id: `${langPrefix}_UseMnemonic`, + defaultMessage: 'Восстановить используя 12-слов', + }, + cancel: { + id: `${langPrefix}_Cancel`, + defaultMessage: 'Отмена', + }, + selectMethod: { + id: `${langPrefix}_SelectMethod`, + defaultMessage: 'Выберите способо восстановления', + }, + +} +const translate = defineMessages(langLabels) + + +@cssModules({ ...styles, ...ownStyle }, { allowMultiple: true }) +class RestoreWalletSelectMethod extends React.PureComponent { + constructor(props) { + super(props) + } + + handleCloseModal = () => { + const { name, data, onClose } = this.props + + if (typeof onClose === 'function') { + onClose() + } + + if (data && typeof data.onClose === 'function') { + data.onClose() + } else if (!(data && data.noRedirect)) { + window.location.assign(links.hashHome) + } + + actions.modals.close(name) + } + + handleUseShamirs = () => { + const { data, onClose } = this.props + actions.modals.open(constants.modals.ShamirsSecretRestory, data) + } + + handleUseMnemonic = () => { + const { data, onClose } = this.props + actions.modals.open(constants.modals.RestoryMnemonicWallet, data) + } + + render() { + const { + name, + intl, + } = this.props + + return ( + +
+
+
+ + + +
+
+
+
+ ) + } +} + +export default injectIntl(RestoreWalletSelectMethod) diff --git a/src/front/shared/components/modals/ShamirsSecretSave/ShamirsSecretSave.scss b/src/front/shared/components/modals/ShamirsSecretSave/ShamirsSecretSave.scss new file mode 100644 index 0000000000..c2faf0517b --- /dev/null +++ b/src/front/shared/components/modals/ShamirsSecretSave/ShamirsSecretSave.scss @@ -0,0 +1,116 @@ + +.buttonsHolder { + display: flex; + align-items: center; + justify-content: space-around; + padding-bottom: 50px; + button { + width: 40%; + padding: 0 8px; + line-height: 36px; + @include media-mobile { + width: 49%; + white-space: nowrap; + } + } +} + +.finishImg { + display: block; + height: 120px; + margin: 0 auto; + margin-bottom: 30px; +} + +.mnemonicView { + max-width: 500px; + margin: 0 auto; + text-align: center; + padding: 15px; + padding-bottom: 15px; +} + +.mnemonicViewWordWrapper { + display: inline-block; + + div { + display: inline-block; + padding: 0.2em; + margin: 0.1em 0.2em; + border: 1px solid var(--color-notice); + border-radius: 2px; + + span { + font-size: 120%; + padding: 0 0.2em; + font-family: 'Roboto Mono', monospace; + } + + span:nth-child(1) { + color: var(--color-notice); + } + } +} + +.wordIndex { + user-select: none; +} + +.mnemonicNotice {} + +.mnemonicEnter { + border: 1px solid var(--color-border); + min-height: 120px; + + button { + display: inline-block; + padding: 0.2em; + margin: 0.1em 0.2em; + border: 1px solid var(--color-notice); + border-radius: 2px; + background: none; + font-size: 120%; + padding: 0 0.2em; + font-family: 'Roboto Mono', monospace; + } +} + +.mnemonicError { + border: 1px solid var(--color-f-error); +} + +.mnemonicWords { + max-width: 500px; + margin: 0 auto; + text-align: center; + padding-top: 20px; + padding-bottom: 50px; + button { + display: inline-block; + padding: 0.2em; + margin: 0.1em 0.2em; + border: 1px solid var(--color-indicating); + border-radius: 2px; + background: none; + font-size: 120%; + padding: 0 0.2em; + font-family: 'Roboto Mono', monospace; + } +} + +.mnemonicButtonsWrapper { + display: flex; + justify-content: center; +} + +.continueBtnWrapper { + margin-left: 1em; +} + +@include media-mobile { + .mnemonicNotice { + position: static; + font-size: 100%; + margin-bottom: 20px; + } +} \ No newline at end of file diff --git a/src/front/shared/components/modals/ShamirsSecretSave/ShamirsSecretSave.tsx b/src/front/shared/components/modals/ShamirsSecretSave/ShamirsSecretSave.tsx new file mode 100644 index 0000000000..f077243c52 --- /dev/null +++ b/src/front/shared/components/modals/ShamirsSecretSave/ShamirsSecretSave.tsx @@ -0,0 +1,370 @@ +import React, { Fragment } from 'react' +import { constants } from 'helpers' +import actions from 'redux/actions' +import { Link } from 'react-router-dom' +import { connect } from 'redaction' +import config from 'helpers/externalConfig' +import { getActivatedCurrencies } from 'helpers/user' + +import cssModules from 'react-css-modules' + +import okSvg from 'shared/images/ok.svg' + +import Modal from 'components/modal/Modal/Modal' +import Button from 'components/controls/Button/Button' +import { FormattedMessage, injectIntl, defineMessages } from 'react-intl' +import Copy from 'components/ui/Copy/Copy' + +import links from 'helpers/links' +import feedback from 'shared/helpers/feedback' +import styles from './ShamirsSecretSave.scss' +import defaultStyles from '../Styles/default.scss' + +const langPrefix = `ShamirsSecretSave` +const langLabels = defineMessages({ + title: { + id: `${langPrefix}_Title`, + defaultMessage: `Ваша секретная фраза`, + }, + enterMnemonicNotice: { + id: `${langPrefix}_EnterNotice`, + defaultMessage: `Нажмите слова, чтобы поместить их рядом друг с другом в правильном порядке`, + }, + shareMnemonicTitle: { + id: `${langPrefix}_ShareMnemonicTitle`, + defaultMessage: `Ваша секретная фраза`, + }, + showMnemonicNotice: { + id: `${langPrefix}_ShowMnemonicNotice`, + defaultMessage: `Запишите эти слова в правильном порядке и сохраните их в безопасном месте.`, + }, + readySaveNotice: { + id: `${langPrefix}_ReadySaveNotice`, + defaultMessage: `Храните бумагу в том месте, где вы не забудете`, + }, + saveMnemonicStep1: { + id: `${langPrefix}_SaveMnemonicStep1`, + defaultMessage: `1. Запишите фразу на бумагу`, + }, + saveMnemonicStep2: { + id: `${langPrefix}_SaveMnemonicStep2`, + defaultMessage: `2. Обязательно подпишите что это ключ от {domain}`, + }, + mnemonicDeleted: { + id: `${langPrefix}_MnemoniceDeleted`, + defaultMessage: `You have already saved your 12-words seed. {href}`, + values: { + href: ( + + {' '} + + + ) + }, + }, + beginNotice: { + id: `${langPrefix}_BeginNotice`, + defaultMessage: `Сейчас мы вам покажем 12 слов вашей секретной фразы.{br}Если вы ее потеряете мы не сможем восстановить ваш кошелек`, + }, + beginContinue: { + id: `${langPrefix}_BeginContinue`, + defaultMessage: `Я понимаю`, + }, + beginLater: { + id: `${langPrefix}_BeginLater`, + defaultMessage: `Я сохраню позже`, + }, +}) + +type MnemonicModalProps = { + intl: IUniversalObj + name: string + onClose: () => void + data: { + onClose: () => void + } +} + +type MnemonicModalState = { + step: string + mnemonic: string | null + words: string[] + enteredWords: string[] + randomWords: string[] + mnemonicInvalid: boolean + incorrectWord: boolean +} + +@connect( + ({ + user: { btcMultisigUserData }, + }) => ({ + btcData: btcMultisigUserData, + }), +) +@cssModules({ ...defaultStyles, ...styles }, { allowMultiple: true }) +class ShamirsSecretSave extends React.Component { + constructor(props) { + super(props) + + const mnemonic = localStorage.getItem(constants.privateKeyNames.twentywords) + + const randomWords = (mnemonic && mnemonic !== '-') ? mnemonic.split(` `) : [] + randomWords.sort(() => 0.5 - Math.random()) + + const words = (mnemonic && mnemonic !== '-') ? mnemonic.split(` `) : [] + + this.state = { + step: (mnemonic === '-') ? `removed` : `begin`, + mnemonic, + words, + enteredWords: [], + randomWords, + mnemonicInvalid: true, + incorrectWord: false, + } + } + + handleClose = () => { + const { name, data, onClose } = this.props + + if (typeof onClose === 'function') { + onClose() + } + + if (data && typeof data.onClose === 'function') { + data.onClose() + } else { + window.location.assign(links.hashHome) + } + + const addAllEnabledWalletsAfterRestoreOrCreateSeedPhrase = config?.opts?.addAllEnabledWalletsAfterRestoreOrCreateSeedPhrase + + if (addAllEnabledWalletsAfterRestoreOrCreateSeedPhrase) { + const currencies = getActivatedCurrencies() + currencies.forEach((currency) => { + actions.core.markCoinAsVisible(currency.toUpperCase(), true) + }) + localStorage.setItem(constants.localStorage.isWalletCreate, 'true') + } + + actions.modals.close(name) + } + + handleFinish = () => { + feedback.backup.finished() + this.handleClose() + } + + handleGoToConfirm = () => { + this.setState({ + step: `confirmMnemonic`, + }) + } + + handleClickWord = (index) => { + const { + randomWords, + enteredWords, + words, + mnemonic, + } = this.state + + const currentWord = enteredWords.length + + if (words[currentWord] !== randomWords[index]) { + + this.setState({ + incorrectWord: true, + }, () => { + setTimeout(() => { + this.setState({ + incorrectWord: false, + }) + }, 500) + }) + return + } + + const clickedWord = randomWords.splice(index, 1) + enteredWords.push(...clickedWord) + + this.setState({ + randomWords, + enteredWords, + incorrectWord: false, + mnemonicInvalid: (enteredWords.join(` `) !== mnemonic), + }, () => { + if (randomWords.length === 0) { + localStorage.setItem(constants.privateKeyNames.twentywords, '-') + actions.backupManager.serverCleanupSeed() + + this.setState({ + step: `ready`, + }) + } + }) + } + + render() { + const { + name, + intl, + } = this.props + + const { + step, + words, + enteredWords, + mnemonic, + randomWords, + mnemonicInvalid, + incorrectWord, + } = this.state + + return ( + // @ts-ignore: strictNullChecks + + {step === `confirmMnemonic` && ( +

+ +

+ )} + {step === `show` && ( +

+ +

+ )} + {step === `removed` && ( +

+ +

+ )} +
+ {step === `begin` && ( + <> +

+ , + }} /> +

+
+ + +
+ + )} + {step === `ready` && ( + <> +

+ finish + +

+
+ +
+ + )} + {step === `confirmMnemonic` && ( + <> +
+
+ { + enteredWords.map((word, index) => ( + + )) + } +
+
+ { + randomWords.map((word, index) => ( + + )) + } +
+
+
+ +
+ + )} + {step === `show` && ( + <> +
+
+ { + words.map((word, index) => ( +
+
+ {(index + 1)} + {word} +
+ { + /* space for correct copy-paste */ + index + 1 !== words.length && ' ' + } +
+ )) + } +
+

+ +
+ +

+
+ +
+ + + +
+ +
+
+ + )} +
+
+ ) + } +} + +export default injectIntl(ShamirsSecretSave) diff --git a/src/front/shared/components/modals/index.ts b/src/front/shared/components/modals/index.ts index 8dc8705867..06e9fd1def 100644 --- a/src/front/shared/components/modals/index.ts +++ b/src/front/shared/components/modals/index.ts @@ -37,6 +37,7 @@ import WalletConnectAccount from './WalletConnectAccount/WalletConnectAccount' // Shamir's Secret-Sharing for Mnemonic Codes import ShamirsSecretRestory from './ShamirsSecretRestory/ShamirsSecretRestory' +import RestoreWalletSelectMethod from './RestoreWalletSelectMethod/RestoreWalletSelectMethod' export default { @@ -61,8 +62,7 @@ export default { AddCustomToken, BtcMultisignSwitch, BtcMultisignConfirmTx, - SaveMnemonicModal, - RestoryMnemonicWallet, + HowToWithdrawModal, InfoInvoice, @@ -79,4 +79,7 @@ export default { WalletConnectAccount, ShamirsSecretRestory, + RestoreWalletSelectMethod, + SaveMnemonicModal, + RestoryMnemonicWallet, } diff --git a/src/front/shared/helpers/constants/modals.ts b/src/front/shared/helpers/constants/modals.ts index 918ed29c38..71c5767dd3 100644 --- a/src/front/shared/helpers/constants/modals.ts +++ b/src/front/shared/helpers/constants/modals.ts @@ -32,5 +32,6 @@ export default { ConnectWalletModal: 'ConnectWalletModal', WalletConnectAccount: 'WalletConnectAccount', ShamirsSecretRestory: 'ShamirsSecretRestory', - ShamirsSecretBackup: 'ShamirsSecretBackup', + ShamirsSecretSave: 'ShamirsSecretSave', + RestoreWalletSelectMethod: 'RestoreWalletSelectMethod', } diff --git a/src/front/shared/routes/index.tsx b/src/front/shared/routes/index.tsx index 3c5e9b3432..b74f614351 100644 --- a/src/front/shared/routes/index.tsx +++ b/src/front/shared/routes/index.tsx @@ -26,8 +26,11 @@ import ScrollToTop from '../components/layout/ScrollToTop/ScrollToTop' import SaveMnemonicModal from "components/modals/SaveMnemonicModal/SaveMnemonicModal" import SaveKeysModal from "components/modals/SaveKeysModal/SaveKeysModal" +import RestoreWalletSelectMethod from "components/modals/RestoreWalletSelectMethod/RestoreWalletSelectMethod" +import ShamirsSecretRestory from "components/modals/ShamirsSecretRestory/ShamirsSecretRestory" import RestoryMnemonicWallet from "components/modals/RestoryMnemonicWallet/RestoryMnemonicWallet" + const routes = ( @@ -61,7 +64,7 @@ const routes = ( - + From 370864ed4a7178027f3ef7ad6d64b276608a8308 Mon Sep 17 00:00:00 2001 From: shendel Date: Thu, 15 Dec 2022 15:39:08 +0300 Subject: [PATCH 09/30] Sharims - save keys (draft) --- .../ShamirsSecretSave/ShamirsSecretSave.tsx | 175 +++--------------- src/front/shared/components/modals/index.ts | 2 + .../pages/CreateWallet/CreateWallet.scss | 2 +- src/front/shared/pages/Wallet/Wallet.tsx | 6 + 4 files changed, 35 insertions(+), 150 deletions(-) diff --git a/src/front/shared/components/modals/ShamirsSecretSave/ShamirsSecretSave.tsx b/src/front/shared/components/modals/ShamirsSecretSave/ShamirsSecretSave.tsx index f077243c52..4a1e2a2a00 100644 --- a/src/front/shared/components/modals/ShamirsSecretSave/ShamirsSecretSave.tsx +++ b/src/front/shared/components/modals/ShamirsSecretSave/ShamirsSecretSave.tsx @@ -64,7 +64,7 @@ const langLabels = defineMessages({ }, beginNotice: { id: `${langPrefix}_BeginNotice`, - defaultMessage: `Сейчас мы вам покажем 12 слов вашей секретной фразы.{br}Если вы ее потеряете мы не сможем восстановить ваш кошелек`, + defaultMessage: `Сейчас мы вам покажем три секретных кода.{br}Если вы потеряете хотя-бы два из них, мы не сможем восстановить ваш кошелек`, }, beginContinue: { id: `${langPrefix}_BeginContinue`, @@ -76,55 +76,33 @@ const langLabels = defineMessages({ }, }) -type MnemonicModalProps = { - intl: IUniversalObj - name: string - onClose: () => void - data: { - onClose: () => void - } -} - -type MnemonicModalState = { - step: string - mnemonic: string | null - words: string[] - enteredWords: string[] - randomWords: string[] - mnemonicInvalid: boolean - incorrectWord: boolean -} -@connect( - ({ - user: { btcMultisigUserData }, - }) => ({ - btcData: btcMultisigUserData, - }), -) @cssModules({ ...defaultStyles, ...styles }, { allowMultiple: true }) -class ShamirsSecretSave extends React.Component { +class ShamirsSecretSave extends React.Component { constructor(props) { super(props) const mnemonic = localStorage.getItem(constants.privateKeyNames.twentywords) - - const randomWords = (mnemonic && mnemonic !== '-') ? mnemonic.split(` `) : [] - randomWords.sort(() => 0.5 - Math.random()) - - const words = (mnemonic && mnemonic !== '-') ? mnemonic.split(` `) : [] + let shamirsSecretKeys: any = localStorage.getItem(constants.privateKeyNames.shamirsSecrets) + if (shamirsSecretKeys && shamirsSecretKeys !== '-') { + try { + shamirsSecretKeys = JSON.parse(shamirsSecretKeys) + } catch (e) { + shamirsSecretKeys = false + } + } else { + shamirsSecretKeys = false + } this.state = { - step: (mnemonic === '-') ? `removed` : `begin`, - mnemonic, - words, - enteredWords: [], - randomWords, - mnemonicInvalid: true, - incorrectWord: false, + step: (shamirsSecretKeys) ? `begin` : `removed`, + shamirsSecretKeys, } } + handleGoToConfirm = () => { + } + handleClose = () => { const { name, data, onClose } = this.props @@ -156,56 +134,6 @@ class ShamirsSecretSave extends React.Component { - this.setState({ - step: `confirmMnemonic`, - }) - } - - handleClickWord = (index) => { - const { - randomWords, - enteredWords, - words, - mnemonic, - } = this.state - - const currentWord = enteredWords.length - - if (words[currentWord] !== randomWords[index]) { - - this.setState({ - incorrectWord: true, - }, () => { - setTimeout(() => { - this.setState({ - incorrectWord: false, - }) - }, 500) - }) - return - } - - const clickedWord = randomWords.splice(index, 1) - enteredWords.push(...clickedWord) - - this.setState({ - randomWords, - enteredWords, - incorrectWord: false, - mnemonicInvalid: (enteredWords.join(` `) !== mnemonic), - }, () => { - if (randomWords.length === 0) { - localStorage.setItem(constants.privateKeyNames.twentywords, '-') - actions.backupManager.serverCleanupSeed() - - this.setState({ - step: `ready`, - }) - } - }) - } - render() { const { name, @@ -214,12 +142,7 @@ class ShamirsSecretSave extends React.Component - {step === `confirmMnemonic` && ( -

- -

- )} {step === `show` && (

@@ -287,58 +205,17 @@ class ShamirsSecretSave extends React.Component )} - {step === `confirmMnemonic` && ( + {step === `show` && ( <>

-
- { - enteredWords.map((word, index) => ( - - )) - } +
+ {shamirsSecretKeys[0]}
-
- { - randomWords.map((word, index) => ( - - )) - } +
+ {shamirsSecretKeys[1]}
-
-
- -
- - )} - {step === `show` && ( - <> -
-
- { - words.map((word, index) => ( -
-
- {(index + 1)} - {word} -
- { - /* space for correct copy-paste */ - index + 1 !== words.length && ' ' - } -
- )) - } +
+ {shamirsSecretKeys[2]}

@@ -348,7 +225,7 @@ class ShamirsSecretSave extends React.Component

- + diff --git a/src/front/shared/components/modals/index.ts b/src/front/shared/components/modals/index.ts index 06e9fd1def..7a32fe4a61 100644 --- a/src/front/shared/components/modals/index.ts +++ b/src/front/shared/components/modals/index.ts @@ -37,6 +37,7 @@ import WalletConnectAccount from './WalletConnectAccount/WalletConnectAccount' // Shamir's Secret-Sharing for Mnemonic Codes import ShamirsSecretRestory from './ShamirsSecretRestory/ShamirsSecretRestory' +import ShamirsSecretSave from './ShamirsSecretSave/ShamirsSecretSave' import RestoreWalletSelectMethod from './RestoreWalletSelectMethod/RestoreWalletSelectMethod' @@ -79,6 +80,7 @@ export default { WalletConnectAccount, ShamirsSecretRestory, + ShamirsSecretSave, RestoreWalletSelectMethod, SaveMnemonicModal, RestoryMnemonicWallet, diff --git a/src/front/shared/pages/CreateWallet/CreateWallet.scss b/src/front/shared/pages/CreateWallet/CreateWallet.scss index 9d172994c8..40a29e0ddb 100644 --- a/src/front/shared/pages/CreateWallet/CreateWallet.scss +++ b/src/front/shared/pages/CreateWallet/CreateWallet.scss @@ -366,7 +366,7 @@ } .wrapper h2 { - padding-top: 90px; + padding-top: 110px; margin-bottom: 65px; font-size: 32px; } diff --git a/src/front/shared/pages/Wallet/Wallet.tsx b/src/front/shared/pages/Wallet/Wallet.tsx index 6d3c7d9ebd..96d05f154b 100644 --- a/src/front/shared/pages/Wallet/Wallet.tsx +++ b/src/front/shared/pages/Wallet/Wallet.tsx @@ -148,6 +148,7 @@ class Wallet extends PureComponent { enabledCurrencies: user.getActivatedCurrencies(), multisigPendingCount, } + window.testSaveShamirsSecrets = () => { this.testSaveShamirsSecrets() } } handleConnectWallet() { @@ -371,6 +372,11 @@ class Wallet extends PureComponent { return 0 } + testSaveShamirsSecrets = () => { + actions.modals.open(constants.modals.ShamirsSecretSave) + } + + addFiatBalanceInUserCurrencyData = (currencyData) => { currencyData.forEach((wallet) => { wallet.fiatBalance = this.returnFiatBalanceByWallet(wallet) From cd4831b0e4fd2d63b1e42ed80d6af6dd801cafb1 Mon Sep 17 00:00:00 2001 From: shendel Date: Fri, 16 Dec 2022 22:28:07 +0300 Subject: [PATCH 10/30] save shamirs modal window (draft) --- .../ShamirsSecretSave/ShamirsSecretSave.scss | 43 ++++++++++++++++++ .../ShamirsSecretSave/ShamirsSecretSave.tsx | 45 ++++++++++++++----- 2 files changed, 76 insertions(+), 12 deletions(-) diff --git a/src/front/shared/components/modals/ShamirsSecretSave/ShamirsSecretSave.scss b/src/front/shared/components/modals/ShamirsSecretSave/ShamirsSecretSave.scss index c2faf0517b..04ade6efbc 100644 --- a/src/front/shared/components/modals/ShamirsSecretSave/ShamirsSecretSave.scss +++ b/src/front/shared/components/modals/ShamirsSecretSave/ShamirsSecretSave.scss @@ -103,6 +103,49 @@ justify-content: center; } +.sharedSecret { + display: block; + border-radius: var(--main-component-border-radius); + background: var(--color-background-elements); + color: var(--color); + box-shadow: var(--box-shadow); + padding: 1em; + margin-bottom: 1em; + + .sharedSecretInfo { + display: block; + padding: 10px; + margin-bottom: 0.5em; + } + .sharedSecretKey { + border-radius: 2px; + vertical-align: top; + line-height: 14px; + box-shadow: none; + font-size: 15px; + padding-left: 15px; + font-family: "Roboto Mono", monospace; + border: 1px solid var(--color-border); + outline: none; + margin-bottom: 0.5em; + + span { + display: block; + width: 100%; + padding: 1em; + } + } + .sharedSecretButtons { + display: flex; + padding: 10px; + justify-content: space-between; + + button { + width: 30%; + } + } +} + .continueBtnWrapper { margin-left: 1em; } diff --git a/src/front/shared/components/modals/ShamirsSecretSave/ShamirsSecretSave.tsx b/src/front/shared/components/modals/ShamirsSecretSave/ShamirsSecretSave.tsx index 4a1e2a2a00..a8b2e8506f 100644 --- a/src/front/shared/components/modals/ShamirsSecretSave/ShamirsSecretSave.tsx +++ b/src/front/shared/components/modals/ShamirsSecretSave/ShamirsSecretSave.tsx @@ -1,7 +1,7 @@ import React, { Fragment } from 'react' import { constants } from 'helpers' import actions from 'redux/actions' -import { Link } from 'react-router-dom' +import { Link as HrefLink } from 'react-router-dom' import { connect } from 'redaction' import config from 'helpers/externalConfig' import { getActivatedCurrencies } from 'helpers/user' @@ -20,6 +20,7 @@ import feedback from 'shared/helpers/feedback' import styles from './ShamirsSecretSave.scss' import defaultStyles from '../Styles/default.scss' + const langPrefix = `ShamirsSecretSave` const langLabels = defineMessages({ title: { @@ -55,10 +56,10 @@ const langLabels = defineMessages({ defaultMessage: `You have already saved your 12-words seed. {href}`, values: { href: ( - + {' '} - + ) }, }, @@ -97,6 +98,11 @@ class ShamirsSecretSave extends React.Component { this.state = { step: (shamirsSecretKeys) ? `begin` : `removed`, shamirsSecretKeys, + sharededSecrets: { // Части ключа, сохраненные, скопированные или расшаренные + 0: false, + 1: false, + 2: false, + } } } @@ -134,6 +140,27 @@ class ShamirsSecretSave extends React.Component { this.handleClose() } + renderShareSecret = (secretNumber) => { + const { + shamirsSecretKeys, + sharededSecrets, + } = this.state + + return ( +
+
+ Секретный Shamir's Secret-Share код #{secretNumber+1} от сайта localhost + {shamirsSecretKeys[secretNumber]} +
+
+ + + +
+
+ ) + } + render() { const { name, @@ -208,15 +235,9 @@ class ShamirsSecretSave extends React.Component { {step === `show` && ( <>
-
- {shamirsSecretKeys[0]} -
-
- {shamirsSecretKeys[1]} -
-
- {shamirsSecretKeys[2]} -
+ {this.renderShareSecret(0)} + {this.renderShareSecret(1)} + {this.renderShareSecret(2)}


From 0c07304561ccbeb7d9506d0579ba5f7826067b1d Mon Sep 17 00:00:00 2001 From: shendel Date: Sun, 18 Dec 2022 16:18:02 +0300 Subject: [PATCH 11/30] save shamirs movel + select save method --- src/front/shared/components/Header/Header.tsx | 2 +- .../MultisignJoinLink/MultisignJoinLink.tsx | 2 +- .../modals/ReceiveModal/ReceiveModal.tsx | 2 +- .../RegisterPINProtected.tsx | 2 +- .../SaveWalletSelectMethod.scss | 28 ++++ .../SaveWalletSelectMethod.tsx | 105 +++++++++++++ .../ShamirsSecretSave/ShamirsSecretSave.scss | 13 +- .../ShamirsSecretSave/ShamirsSecretSave.tsx | 142 ++++++++++++++---- src/front/shared/components/modals/index.ts | 2 + src/front/shared/components/ui/Copy/Copy.tsx | 2 + src/front/shared/helpers/constants/modals.ts | 1 + src/front/shared/helpers/wpLogoutModal.ts | 2 +- .../pages/CreateWallet/CreateWallet.tsx | 2 +- .../pages/Exchange/QuickSwap/UserInfo.tsx | 2 +- .../pages/Marketmaker/MarketmakerSettings.tsx | 2 +- src/front/shared/pages/Multisign/Btc/Btc.tsx | 2 +- src/front/shared/pages/Wallet/Row/Row.tsx | 2 +- .../pages/Wallet/WallerSlider/index.tsx | 2 +- 18 files changed, 270 insertions(+), 45 deletions(-) create mode 100644 src/front/shared/components/modals/SaveWalletSelectMethod/SaveWalletSelectMethod.scss create mode 100644 src/front/shared/components/modals/SaveWalletSelectMethod/SaveWalletSelectMethod.tsx diff --git a/src/front/shared/components/Header/Header.tsx b/src/front/shared/components/Header/Header.tsx index fc58649762..5088dcbf3a 100644 --- a/src/front/shared/components/Header/Header.tsx +++ b/src/front/shared/components/Header/Header.tsx @@ -105,7 +105,7 @@ class Header extends Component { } saveMnemonicAndClearStorage = () => { - actions.modals.open(constants.modals.SaveMnemonicModal, { + actions.modals.open(constants.modals.SaveWalletSelectMethod, { onClose: () => { this.clearLocalStorage() } diff --git a/src/front/shared/components/modals/MultisignJoinLink/MultisignJoinLink.tsx b/src/front/shared/components/modals/MultisignJoinLink/MultisignJoinLink.tsx index b2123b35bb..7c219e1733 100644 --- a/src/front/shared/components/modals/MultisignJoinLink/MultisignJoinLink.tsx +++ b/src/front/shared/components/modals/MultisignJoinLink/MultisignJoinLink.tsx @@ -96,7 +96,7 @@ class MultisignJoinLink extends React.Component { } handleBeginSaveMnemonic = async () => { - actions.modals.open(constants.modals.SaveMnemonicModal, { + actions.modals.open(constants.modals.SaveWalletSelectMethod, { onClose: () => { const mnemonic = localStorage.getItem(constants.privateKeyNames.twentywords) const mnemonicSaved = (mnemonic === `-`) diff --git a/src/front/shared/components/modals/ReceiveModal/ReceiveModal.tsx b/src/front/shared/components/modals/ReceiveModal/ReceiveModal.tsx index a6d3bcf2f4..636236680a 100644 --- a/src/front/shared/components/modals/ReceiveModal/ReceiveModal.tsx +++ b/src/front/shared/components/modals/ReceiveModal/ReceiveModal.tsx @@ -78,7 +78,7 @@ class ReceiveModal extends React.Component { } handleBeginSaveMnemonic = async () => { - actions.modals.open(constants.modals.SaveMnemonicModal, { + actions.modals.open(constants.modals.SaveWalletSelectMethod, { onClose: () => { const mnemonic = localStorage.getItem(constants.privateKeyNames.twentywords) const mnemonicSaved = (mnemonic === `-`) diff --git a/src/front/shared/components/modals/RegisterPINProtected/RegisterPINProtected.tsx b/src/front/shared/components/modals/RegisterPINProtected/RegisterPINProtected.tsx index 6ad486f478..021dadb7d0 100644 --- a/src/front/shared/components/modals/RegisterPINProtected/RegisterPINProtected.tsx +++ b/src/front/shared/components/modals/RegisterPINProtected/RegisterPINProtected.tsx @@ -345,7 +345,7 @@ class RegisterPINProtected extends React.Component { } handleBeginSaveMnemonic = async () => { - actions.modals.open(constants.modals.SaveMnemonicModal, { + actions.modals.open(constants.modals.SaveWalletSelectMethod, { onClose: () => { const mnemonic = localStorage.getItem(constants.privateKeyNames.twentywords) const mnemonicSaved = mnemonic === `-` diff --git a/src/front/shared/components/modals/SaveWalletSelectMethod/SaveWalletSelectMethod.scss b/src/front/shared/components/modals/SaveWalletSelectMethod/SaveWalletSelectMethod.scss new file mode 100644 index 0000000000..6fa1d8e54b --- /dev/null +++ b/src/front/shared/components/modals/SaveWalletSelectMethod/SaveWalletSelectMethod.scss @@ -0,0 +1,28 @@ +.content { + max-width: 800px; + margin: 0 auto; + text-align: center; + width: 100%; +} + +.text { + font-size: 1.5em; + + @media all and (max-width: 480px) { + font-size: 1em; + } +} + + +.restoreNotice { + margin-bottom: 30px; +} + +.buttonHolder { + display: block; + align-items: center; + button { + margin: 1em auto; + display: block; + } +} diff --git a/src/front/shared/components/modals/SaveWalletSelectMethod/SaveWalletSelectMethod.tsx b/src/front/shared/components/modals/SaveWalletSelectMethod/SaveWalletSelectMethod.tsx new file mode 100644 index 0000000000..86c9b946f4 --- /dev/null +++ b/src/front/shared/components/modals/SaveWalletSelectMethod/SaveWalletSelectMethod.tsx @@ -0,0 +1,105 @@ +import React from 'react' +import actions from 'redux/actions' +import cssModules from 'react-css-modules' +import styles from '../Styles/default.scss' +import ownStyle from './SaveWalletSelectMethod.scss' +import Modal from 'components/modal/Modal/Modal' +import { Button } from 'components/controls' +import { FormattedMessage, defineMessages, injectIntl } from 'react-intl' +import links from 'helpers/links' +import { constants } from 'helpers' + + +const langPrefix = `SaveWalletSelectMethod` +const langLabels = { + title: { + id: `${langPrefix}_Title`, + defaultMessage: 'Сохранение кошелька', + }, + useShamirs: { + id: `${langPrefix}_UseShamirs`, + defaultMessage: `Сохранить Shamir's Secret-Share`, + }, + useMnemonic: { + id: `${langPrefix}_UseMnemonic`, + defaultMessage: 'Сохранить 12-слов', + }, + cancel: { + id: `${langPrefix}_Cancel`, + defaultMessage: 'Отмена', + }, + selectMethod: { + id: `${langPrefix}_SelectMethod`, + defaultMessage: 'Выберите способ', + }, + +} +const translate = defineMessages(langLabels) + + +@cssModules({ ...styles, ...ownStyle }, { allowMultiple: true }) +class SaveWalletSelectMethod extends React.PureComponent { + constructor(props) { + super(props) + } + + handleCloseModal = () => { + const { name, data, onClose } = this.props + + if (typeof onClose === 'function') { + onClose() + } + + if (data && typeof data.onClose === 'function') { + data.onClose() + } else if (!(data && data.noRedirect)) { + window.location.assign(links.hashHome) + } + + actions.modals.close(name) + } + + handleUseShamirs = () => { + const { data, onClose } = this.props + actions.modals.open(constants.modals.ShamirsSecretSave, data) + } + + handleUseMnemonic = () => { + const { data, onClose } = this.props + actions.modals.open(constants.modals.SaveMnemonicModal, data) + } + + render() { + const { + name, + intl, + } = this.props + + return ( + +

+
+
+ + + +
+
+
+ + ) + } +} + +export default injectIntl(SaveWalletSelectMethod) diff --git a/src/front/shared/components/modals/ShamirsSecretSave/ShamirsSecretSave.scss b/src/front/shared/components/modals/ShamirsSecretSave/ShamirsSecretSave.scss index 04ade6efbc..d54a0484cc 100644 --- a/src/front/shared/components/modals/ShamirsSecretSave/ShamirsSecretSave.scss +++ b/src/front/shared/components/modals/ShamirsSecretSave/ShamirsSecretSave.scss @@ -139,9 +139,18 @@ display: flex; padding: 10px; justify-content: space-between; - - button { + >div { width: 30%; + button { + width: 100%; + } + } + >button, + >a { + width: 30%; + } + >a button { + width: 100%; } } } diff --git a/src/front/shared/components/modals/ShamirsSecretSave/ShamirsSecretSave.tsx b/src/front/shared/components/modals/ShamirsSecretSave/ShamirsSecretSave.tsx index a8b2e8506f..9c663419a1 100644 --- a/src/front/shared/components/modals/ShamirsSecretSave/ShamirsSecretSave.tsx +++ b/src/front/shared/components/modals/ShamirsSecretSave/ShamirsSecretSave.tsx @@ -7,7 +7,7 @@ import config from 'helpers/externalConfig' import { getActivatedCurrencies } from 'helpers/user' import cssModules from 'react-css-modules' - +import moment from 'moment/moment' import okSvg from 'shared/images/ok.svg' import Modal from 'components/modal/Modal/Modal' @@ -25,35 +25,27 @@ const langPrefix = `ShamirsSecretSave` const langLabels = defineMessages({ title: { id: `${langPrefix}_Title`, - defaultMessage: `Ваша секретная фраза`, - }, - enterMnemonicNotice: { - id: `${langPrefix}_EnterNotice`, - defaultMessage: `Нажмите слова, чтобы поместить их рядом друг с другом в правильном порядке`, + defaultMessage: `Shamir's Secret-Share`, }, shareMnemonicTitle: { id: `${langPrefix}_ShareMnemonicTitle`, - defaultMessage: `Ваша секретная фраза`, + defaultMessage: `Shamir's Secret-Share codes`, }, showMnemonicNotice: { id: `${langPrefix}_ShowMnemonicNotice`, - defaultMessage: `Запишите эти слова в правильном порядке и сохраните их в безопасном месте.`, + defaultMessage: `Сохраните эти коды. Если вы потеряете хотя-бы два из них, восстановить кошелек будет не возможно`, }, readySaveNotice: { id: `${langPrefix}_ReadySaveNotice`, - defaultMessage: `Храните бумагу в том месте, где вы не забудете`, - }, - saveMnemonicStep1: { - id: `${langPrefix}_SaveMnemonicStep1`, - defaultMessage: `1. Запишите фразу на бумагу`, + defaultMessage: `Не потеряете сохраненные коды`, }, - saveMnemonicStep2: { - id: `${langPrefix}_SaveMnemonicStep2`, - defaultMessage: `2. Обязательно подпишите что это ключ от {domain}`, + countSecretsSaved: { + id: `${langPrefix}_CountSavedSecrets`, + defaultMessage: `Сохранено {saved} из {total}`, }, mnemonicDeleted: { id: `${langPrefix}_MnemoniceDeleted`, - defaultMessage: `You have already saved your 12-words seed. {href}`, + defaultMessage: `You have already saved your Shamir's Secret-Share codes. {href}`, values: { href: ( @@ -75,6 +67,18 @@ const langLabels = defineMessages({ id: `${langPrefix}_BeginLater`, defaultMessage: `Я сохраню позже`, }, + useCopy: { + id: `${langPrefix}_UseCopy`, + defaultMessage: `Скопировать`, + }, + useSave: { + id: `${langPrefix}_UseSave`, + defaultMessage: `Сохранить`, + }, + useSend: { + id: `${langPrefix}_UseSend`, + defaultMessage: `Отправить`, + }, }) @@ -98,6 +102,9 @@ class ShamirsSecretSave extends React.Component { this.state = { step: (shamirsSecretKeys) ? `begin` : `removed`, shamirsSecretKeys, + copyUsed: false, + saveUsed: false, + sendUsed: false, sharededSecrets: { // Части ключа, сохраненные, скопированные или расшаренные 0: false, 1: false, @@ -107,6 +114,9 @@ class ShamirsSecretSave extends React.Component { } handleGoToConfirm = () => { + this.setState({ + step: `ready`, + }) } handleClose = () => { @@ -140,12 +150,46 @@ class ShamirsSecretSave extends React.Component { this.handleClose() } + createDownload = (filename, text) => { + const element = document.createElement('a') + const message = 'Check your browser downloads' + + element.setAttribute('href', `data:text/plaincharset=utf-8,${encodeURIComponent(text)}`) + element.setAttribute('download', `${filename}_${moment().format('DD.MM.YYYY')}.txt`) + + element.style.display = 'none' + document.body.appendChild(element) + element.click() + document.body.removeChild(element) + + actions.notifications.show(constants.notifications.Message, { + message, + }) + } + + markCodeShared = (secretNumber) => { + const { sharededSecrets } = this.state + this.setState({ + sharededSecrets: { + ...sharededSecrets, + [`${secretNumber}`]: true, + } + }) + } + renderShareSecret = (secretNumber) => { const { shamirsSecretKeys, sharededSecrets, + copyUsed, + saveUsed, + sendUsed, } = this.state + + const text = `Shamir's Secret-Share code #${secretNumber+1} from ${document.location.hostname}\n` + + `${shamirsSecretKeys[secretNumber]}\n` + + `Don't Lose This Code.` return (
@@ -153,9 +197,41 @@ class ShamirsSecretSave extends React.Component { {shamirsSecretKeys[secretNumber]}
- - - + {copyUsed ? ( + + ) : ( + { + this.markCodeShared(secretNumber) + /* this.setState({ copyUsed: true }) */ + }}> + + + )} + + {(sendUsed) ? ( + + ) : ( + + + + )}
) @@ -170,8 +246,12 @@ class ShamirsSecretSave extends React.Component { const { step, shamirsSecretKeys, + sharededSecrets, } = this.state + const canContinue = !(sharededSecrets[0] && sharededSecrets[1] && sharededSecrets[2]) + const totalSaved = ((sharededSecrets[0]) ? 1 : 0) + ((sharededSecrets[1]) ? 1 : 0) + ((sharededSecrets[2]) ? 1 : 0) + return ( // @ts-ignore: strictNullChecks { {this.renderShareSecret(0)} {this.renderShareSecret(1)} {this.renderShareSecret(2)} -

- -
- -

- +
+ +
- - -
-
diff --git a/src/front/shared/components/modals/index.ts b/src/front/shared/components/modals/index.ts index 7a32fe4a61..3f19f91734 100644 --- a/src/front/shared/components/modals/index.ts +++ b/src/front/shared/components/modals/index.ts @@ -39,6 +39,7 @@ import WalletConnectAccount from './WalletConnectAccount/WalletConnectAccount' import ShamirsSecretRestory from './ShamirsSecretRestory/ShamirsSecretRestory' import ShamirsSecretSave from './ShamirsSecretSave/ShamirsSecretSave' import RestoreWalletSelectMethod from './RestoreWalletSelectMethod/RestoreWalletSelectMethod' +import SaveWalletSelectMethod from './SaveWalletSelectMethod/SaveWalletSelectMethod' export default { @@ -82,6 +83,7 @@ export default { ShamirsSecretRestory, ShamirsSecretSave, RestoreWalletSelectMethod, + SaveWalletSelectMethod, SaveMnemonicModal, RestoryMnemonicWallet, } diff --git a/src/front/shared/components/ui/Copy/Copy.tsx b/src/front/shared/components/ui/Copy/Copy.tsx index 94a54be246..a2d8a4a1f9 100644 --- a/src/front/shared/components/ui/Copy/Copy.tsx +++ b/src/front/shared/components/ui/Copy/Copy.tsx @@ -15,6 +15,8 @@ class Copy extends Component { } handleCopyLink = () => { + const { onCopy } = this.props + if (onCopy) onCopy() if (this.state.showTip) { return } diff --git a/src/front/shared/helpers/constants/modals.ts b/src/front/shared/helpers/constants/modals.ts index 71c5767dd3..b7133445a1 100644 --- a/src/front/shared/helpers/constants/modals.ts +++ b/src/front/shared/helpers/constants/modals.ts @@ -34,4 +34,5 @@ export default { ShamirsSecretRestory: 'ShamirsSecretRestory', ShamirsSecretSave: 'ShamirsSecretSave', RestoreWalletSelectMethod: 'RestoreWalletSelectMethod', + SaveWalletSelectMethod: 'SaveWalletSelectMethod', } diff --git a/src/front/shared/helpers/wpLogoutModal.ts b/src/front/shared/helpers/wpLogoutModal.ts index 12b8527a2f..cb1ba8b5ab 100644 --- a/src/front/shared/helpers/wpLogoutModal.ts +++ b/src/front/shared/helpers/wpLogoutModal.ts @@ -35,7 +35,7 @@ const confirmTexts = defineMessages({ }) const handleShowMnemonic = () => { - actions.modals.open(constants.modals.SaveMnemonicModal) + actions.modals.open(constants.modals.SaveWalletSelectMethod) } const handleConfirm = () => { diff --git a/src/front/shared/pages/CreateWallet/CreateWallet.tsx b/src/front/shared/pages/CreateWallet/CreateWallet.tsx index 2b6de7bbdc..a59d79b056 100644 --- a/src/front/shared/pages/CreateWallet/CreateWallet.tsx +++ b/src/front/shared/pages/CreateWallet/CreateWallet.tsx @@ -100,7 +100,7 @@ function CreateWallet(props) { } const handleShowMnemonic = () => { - actions.modals.open(constants.modals.SaveMnemonicModal) + actions.modals.open(constants.modals.SaveWalletSelectMethod) } const handleRestoreMnemonic = () => { diff --git a/src/front/shared/pages/Exchange/QuickSwap/UserInfo.tsx b/src/front/shared/pages/Exchange/QuickSwap/UserInfo.tsx index 1f9f4020ed..7491425493 100644 --- a/src/front/shared/pages/Exchange/QuickSwap/UserInfo.tsx +++ b/src/front/shared/pages/Exchange/QuickSwap/UserInfo.tsx @@ -59,7 +59,7 @@ function UserInfo(props: ComponentProps) { const saveSecretPhrase = !mnemonicSaved && !metamask.isConnected() const saveMnemonic = () => { - actions.modals.open(constants.modals.SaveMnemonicModal, { + actions.modals.open(constants.modals.SaveWalletSelectMethod, { onClose: () => { const mnemonic = localStorage.getItem(constants.privateKeyNames.twentywords) diff --git a/src/front/shared/pages/Marketmaker/MarketmakerSettings.tsx b/src/front/shared/pages/Marketmaker/MarketmakerSettings.tsx index 9686fcfae7..9d6896592d 100644 --- a/src/front/shared/pages/Marketmaker/MarketmakerSettings.tsx +++ b/src/front/shared/pages/Marketmaker/MarketmakerSettings.tsx @@ -348,7 +348,7 @@ class MarketmakerSettings extends Component { } handleSaveMnemonic = () => { - actions.modals.open(constants.modals.SaveMnemonicModal, { + actions.modals.open(constants.modals.SaveWalletSelectMethod, { onClose: () => { const mnemonic = localStorage.getItem(constants.privateKeyNames.twentywords) const mnemonicSaved = (mnemonic === `-`) diff --git a/src/front/shared/pages/Multisign/Btc/Btc.tsx b/src/front/shared/pages/Multisign/Btc/Btc.tsx index 94e9ec0c59..bc2d99a815 100644 --- a/src/front/shared/pages/Multisign/Btc/Btc.tsx +++ b/src/front/shared/pages/Multisign/Btc/Btc.tsx @@ -250,7 +250,7 @@ class Btc extends PureComponent { handleSaveMnemonic = async () => { //@ts-ignore: strictNullChecks - actions.modals.open(constants.modals.SaveMnemonicModal, { + actions.modals.open(constants.modals.SaveWalletSelectMethod, { onClose: () => { const mnemonic = localStorage.getItem(constants.privateKeyNames.twentywords) const mnemonicSaved = mnemonic === `-` diff --git a/src/front/shared/pages/Wallet/Row/Row.tsx b/src/front/shared/pages/Wallet/Row/Row.tsx index e0b6e68313..2b47f5b4ae 100644 --- a/src/front/shared/pages/Wallet/Row/Row.tsx +++ b/src/front/shared/pages/Wallet/Row/Row.tsx @@ -412,7 +412,7 @@ class Row extends Component { } handleShowMnemonic = () => { - actions.modals.open(constants.modals.SaveMnemonicModal) + actions.modals.open(constants.modals.SaveWalletSelectMethod) } connectMetamask = () => { diff --git a/src/front/shared/pages/Wallet/WallerSlider/index.tsx b/src/front/shared/pages/Wallet/WallerSlider/index.tsx index 9feae1a006..5525470883 100644 --- a/src/front/shared/pages/Wallet/WallerSlider/index.tsx +++ b/src/front/shared/pages/Wallet/WallerSlider/index.tsx @@ -162,7 +162,7 @@ class WallerSlider extends React.Component handleShowMnemonic = () => { //@ts-ignore: strictNullChecks - actions.modals.open(constants.modals.SaveMnemonicModal, { + actions.modals.open(constants.modals.SaveWalletSelectMethod, { onClose: () => { const mnemonic = localStorage.getItem(constants.privateKeyNames.twentywords) const mnemonicDeleted = mnemonic === '-' From 75edae5a2ff5b5e68da501ffab06f2c168db5751 Mon Sep 17 00:00:00 2001 From: shendel Date: Wed, 28 Dec 2022 15:53:48 +0300 Subject: [PATCH 12/30] fix modals translate bug --- .../RestoreWalletSelectMethod.tsx | 8 +++----- .../SaveWalletSelectMethod/SaveWalletSelectMethod.tsx | 8 +++----- .../modals/ShamirsSecretRestory/ShamirsSecretRestory.tsx | 7 +++---- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/front/shared/components/modals/RestoreWalletSelectMethod/RestoreWalletSelectMethod.tsx b/src/front/shared/components/modals/RestoreWalletSelectMethod/RestoreWalletSelectMethod.tsx index 0fe178a52e..eafde80d34 100644 --- a/src/front/shared/components/modals/RestoreWalletSelectMethod/RestoreWalletSelectMethod.tsx +++ b/src/front/shared/components/modals/RestoreWalletSelectMethod/RestoreWalletSelectMethod.tsx @@ -11,7 +11,7 @@ import { constants } from 'helpers' const langPrefix = `RestoreWalletSelectMethod` -const langLabels = { +const langLabels = defineMessages({ title: { id: `${langPrefix}_Title`, defaultMessage: 'Восстановление кошелька', @@ -32,9 +32,7 @@ const langLabels = { id: `${langPrefix}_SelectMethod`, defaultMessage: 'Выберите способо восстановления', }, - -} -const translate = defineMessages(langLabels) +}) @cssModules({ ...styles, ...ownStyle }, { allowMultiple: true }) @@ -80,7 +78,7 @@ class RestoreWalletSelectMethod extends React.PureComponent { name={name} showCloseButton onClose={this.handleCloseModal} - title={intl.formatMessage(translate.title)} + title={intl.formatMessage(langLabels.title)} >
diff --git a/src/front/shared/components/modals/SaveWalletSelectMethod/SaveWalletSelectMethod.tsx b/src/front/shared/components/modals/SaveWalletSelectMethod/SaveWalletSelectMethod.tsx index 86c9b946f4..61ae2b6eee 100644 --- a/src/front/shared/components/modals/SaveWalletSelectMethod/SaveWalletSelectMethod.tsx +++ b/src/front/shared/components/modals/SaveWalletSelectMethod/SaveWalletSelectMethod.tsx @@ -11,7 +11,7 @@ import { constants } from 'helpers' const langPrefix = `SaveWalletSelectMethod` -const langLabels = { +const langLabels = defineMessages({ title: { id: `${langPrefix}_Title`, defaultMessage: 'Сохранение кошелька', @@ -32,9 +32,7 @@ const langLabels = { id: `${langPrefix}_SelectMethod`, defaultMessage: 'Выберите способ', }, - -} -const translate = defineMessages(langLabels) +}) @cssModules({ ...styles, ...ownStyle }, { allowMultiple: true }) @@ -80,7 +78,7 @@ class SaveWalletSelectMethod extends React.PureComponent { name={name} showCloseButton onClose={this.handleCloseModal} - title={intl.formatMessage(translate.title)} + title={intl.formatMessage(langLabels.title)} >
diff --git a/src/front/shared/components/modals/ShamirsSecretRestory/ShamirsSecretRestory.tsx b/src/front/shared/components/modals/ShamirsSecretRestory/ShamirsSecretRestory.tsx index 19be3b4315..6b8e83aaff 100644 --- a/src/front/shared/components/modals/ShamirsSecretRestory/ShamirsSecretRestory.tsx +++ b/src/front/shared/components/modals/ShamirsSecretRestory/ShamirsSecretRestory.tsx @@ -17,7 +17,7 @@ import feedback from 'shared/helpers/feedback' const langPrefix = `Shamirs_Restory` -const langLabels = { +const langLabels = defineMessages({ title: { id: `${langPrefix}_Title`, defaultMessage: 'Восстановление кошелька', @@ -70,8 +70,7 @@ const langLabels = { id: `${langPrefix}_CancelRestory`, defaultMessage: 'Отмена', }, -} -const translate = defineMessages(langLabels) +}) /* Какой механизм ключей использовать @@ -199,7 +198,7 @@ class ShamirsSecretRestory extends React.PureComponent { name={name} showCloseButton onClose={this.handleCloseModal} - title={intl.formatMessage(translate.title)} + title={intl.formatMessage(langLabels.title)} >
{!isRestored && ( From 6ae6a6b406c39a75ef155a3f3b38094d110fcece Mon Sep 17 00:00:00 2001 From: shendel Date: Wed, 28 Dec 2022 16:35:16 +0300 Subject: [PATCH 13/30] en translate (basic) --- src/front/client/index.html | 2 +- src/front/shared/localisation/en.json | 268 ++++++++++++++++++++++++++ src/front/shared/localisation/ru.json | 268 ++++++++++++++++++++++++++ 3 files changed, 537 insertions(+), 1 deletion(-) diff --git a/src/front/client/index.html b/src/front/client/index.html index 580ddc0453..1b5887b1fe 100644 --- a/src/front/client/index.html +++ b/src/front/client/index.html @@ -537,7 +537,7 @@

diff --git a/src/front/shared/components/modals/ShamirsSecretRestory/ShamirsSecretRestory.tsx b/src/front/shared/components/modals/ShamirsSecretRestory/ShamirsSecretRestory.tsx index 6b8e83aaff..75a12f5b6e 100644 --- a/src/front/shared/components/modals/ShamirsSecretRestory/ShamirsSecretRestory.tsx +++ b/src/front/shared/components/modals/ShamirsSecretRestory/ShamirsSecretRestory.tsx @@ -243,7 +243,7 @@ class ShamirsSecretRestory extends React.PureComponent {
{!isRestoring && (
- -
From 3cb04fb2ba3963c051867cceac282f3588a1befa Mon Sep 17 00:00:00 2001 From: shendel Date: Fri, 30 Dec 2022 15:48:59 +0300 Subject: [PATCH 25/30] routers for restore with menmonic and shamirs --- src/front/shared/helpers/links.ts | 2 ++ src/front/shared/routes/index.tsx | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/front/shared/helpers/links.ts b/src/front/shared/helpers/links.ts index e5f32c7959..09af92f6fb 100644 --- a/src/front/shared/helpers/links.ts +++ b/src/front/shared/helpers/links.ts @@ -12,6 +12,8 @@ const linksManager = { history: '/history', createWallet: '/createWallet', restoreWallet: '/restoreWallet', + restoreWalletMnemonic: '/restoreWalletMnemonic', + restoreWalletShamirs: '/restoreWalletShamirs', connectWallet: '/connectWallet', invoices: '/invoices', invoice: '/invoice', diff --git a/src/front/shared/routes/index.tsx b/src/front/shared/routes/index.tsx index b74f614351..bf4ef67bc3 100644 --- a/src/front/shared/routes/index.tsx +++ b/src/front/shared/routes/index.tsx @@ -65,6 +65,8 @@ const routes = ( + + From d221831f6ecfed3216369706abb051173cbae573 Mon Sep 17 00:00:00 2001 From: shendel Date: Fri, 30 Dec 2022 16:15:13 +0300 Subject: [PATCH 26/30] unit tests - restore wallet --- .../RestoreWalletSelectMethod/RestoreWalletSelectMethod.tsx | 4 ++-- tests/e2e/utils.ts | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/front/shared/components/modals/RestoreWalletSelectMethod/RestoreWalletSelectMethod.tsx b/src/front/shared/components/modals/RestoreWalletSelectMethod/RestoreWalletSelectMethod.tsx index 5ab35ffb28..0fca77f716 100644 --- a/src/front/shared/components/modals/RestoreWalletSelectMethod/RestoreWalletSelectMethod.tsx +++ b/src/front/shared/components/modals/RestoreWalletSelectMethod/RestoreWalletSelectMethod.tsx @@ -83,10 +83,10 @@ class RestoreWalletSelectMethod extends React.PureComponent {
- -