feat(suite): Add adding a new shamir group into an existing setup

This commit is contained in:
Peter Sanderson
2024-05-13 12:00:10 +02:00
committed by Peter Sanderson
parent 6287b16a3e
commit e9a0425183
63 changed files with 1708 additions and 5818 deletions

View File

@@ -88,13 +88,13 @@ Browser (User Agent), System and HW specifications, Suite version, instance id s
major_version: 2,
minor_version: 4,
model: T,
needs_backup: False,
backup_availability: 0,
no_backup: False,
passphrase_always_on_device: False,
passphrase_protection: True,
patch_version: 2,
pin_protection: True,
recovery_mode: False,
recovery_status: 0,
revision: 9276b1702361f70e094286e2f89e919d8a230d5c,
safety_checks: Strict,
sd_card_present: False,

View File

@@ -71,6 +71,7 @@ export type FlexProps = FrameProps & {
flex?: Flex;
flexWrap?: FlexWrap;
isReversed?: boolean;
className?: string;
};
const Flex = ({
@@ -83,6 +84,7 @@ const Flex = ({
flex = 'auto',
flexWrap = 'nowrap',
isReversed = false,
className,
}: FlexProps) => {
const frameProps = {
margin,
@@ -90,6 +92,7 @@ const Flex = ({
return (
<Container
className={className}
$gap={gap}
$justifyContent={justifyContent}
$alignItems={alignItems}

View File

@@ -45,6 +45,8 @@ export type PngImage = keyof typeof PNG_IMAGES;
export const PNG_IMAGES = {
CLOUDY: 'cloudy.png',
CLOUDY_2x: 'cloudy@2x.png',
CREATE_SHAMIR_GROUP: 'create-shamir-group.png',
CREATE_SHAMIR_GROUP_2x: 'create-shamir-group@2x.png',
BACKUP: 'backup.png',
BACKUP_2x: 'backup@2x.png',
CHECK_SHIELD: 'check-shield.png',
@@ -69,6 +71,8 @@ export const PNG_IMAGES = {
PIN_LOCKED_2x: 'pin-locked@2x.png',
RECOVERY: 'recovery.png',
RECOVERY_2x: 'recovery@2x.png',
SHAMIR_SHARES: 'shamir-shares.png',
SHAMIR_SHARES_2x: 'shamir-shares@2x.png',
UNDERSTAND: 'understand.png',
UNDERSTAND_2x: 'understand@2x.png',
WALLET: 'wallet.png',

View File

@@ -15,6 +15,7 @@ export const ICONS = {
BACK: require('../../../images/icons/back.svg'),
BACKEND: require('../../../images/icons/backend.svg'),
BACKUP: require('../../../images/icons/backup.svg'),
BACKUP_2: require('../../../images/icons/backup_2.svg'),
BINARY: require('../../../images/icons/binary.svg'),
BLOCKED: require('../../../images/icons/blocked.svg'),
BROADCAST: require('../../../images/icons/broadcast.svg'),
@@ -50,12 +51,14 @@ export const ICONS = {
ADDRESS_NEXT: require('../../../images/icons/address_next.svg'),
ADDRESS_PIXEL_CONTINUES: require('../../../images/icons/address_pixel_continues.svg'),
ADDRESS_PIXEL_NEXT: require('../../../images/icons/address_pixel_next.svg'),
CAMERA_SLASH: require('../../../images/icons/camera_slash.svg'),
DOWN: require('../../../images/icons/down.svg'),
DROPBOX: require('../../../images/icons/dropbox.svg'),
EVERSTAKE_LOGO: require('../../../images/icons/everstake_logo.svg'),
EJECT: require('../../../images/icons/eject.svg'),
EXPERIMENTAL: require('../../../images/icons/experimental.svg'),
EXTERNAL_LINK: require('../../../images/icons/external_link.svg'),
EYE_SLASH: require('../../../images/icons/eye_slash.svg'),
FEEDBACK: require('../../../images/icons/feedback.svg'),
FILE: require('../../../images/icons/file.svg'),
FINGERPRINT: require('../../../images/icons/fingerprint.svg'),
@@ -125,13 +128,10 @@ export const ICONS = {
SIGNATURE: require('../../../images/icons/signature.svg'),
SPINNER: require('../../../images/icons/spinner.svg'),
STOP: require('../../../images/icons/stop.svg'),
TREZOR_T1B1: require('../../../images/icons/trezor_t1b1.svg'),
TREZOR_T2T1: require('../../../images/icons/trezor_t2t1.svg'),
TREZOR_T2B1: require('../../../images/icons/trezor_t2b1.svg'),
TREZOR_T3T1: require('../../../images/icons/trezor_t3t1.svg'),
TABLE: require('../../../images/icons/table.svg'),
TAG_MINIMAL: require('../../../images/icons/tag_minimal.svg'),
TAG: require('../../../images/icons/tag.svg'),
TIMER: require('../../../images/icons/timer.svg'),
TOR_MINIMAL: require('../../../images/icons/tor_minimal.svg'),
TOR: require('../../../images/icons/tor.svg'),
TRANSFER: require('../../../images/icons/transfer.svg'),
@@ -139,6 +139,10 @@ export const ICONS = {
TREND_UP: require('../../../images/icons/trend_up.svg'),
TREND_UP_THIN: require('../../../images/icons/trend_up_thin.svg'),
TREZOR_LOGO: require('../../../images/icons/trezor_logo.svg'),
TREZOR_T1B1: require('../../../images/icons/trezor_t1b1.svg'),
TREZOR_T2T1: require('../../../images/icons/trezor_t2t1.svg'),
TREZOR_T2B1: require('../../../images/icons/trezor_t2b1.svg'),
TREZOR_T3T1: require('../../../images/icons/trezor_t3t1.svg'),
UNLINK: require('../../../images/icons/unlink.svg'),
UP: require('../../../images/icons/up.svg'),
USERS: require('../../../images/icons/users.svg'),

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 33 33" xmlns="http://www.w3.org/2000/svg">
<path d="M4.813 6.016c-.508.16-.979.587-1.221 1.106l-.154.33-.015 9.319c-.016 10.138-.029 9.675.286 10.173.317.502.798.829 1.377.936.457.084 22.206.084 22.663 0a2.039 2.039 0 0 0 1.377-.936c.315-.498.302-.035.286-10.173l-.014-9.319-.159-.342c-.184-.4-.588-.808-.997-1.008l-.274-.134L16.5 5.957c-9.105-.008-11.513.004-11.687.059M27.39 16.913v8.992H5.445V7.92H27.39v8.993M9.982 12.026c-.262.139-.402.293-.5.549a.983.983 0 0 0 .487 1.223l.241.117h6.207c6.132 0 6.211-.001 6.439-.112a.982.982 0 0 0 .021-1.76c-.208-.106-.348-.108-6.471-.106-5.598.002-6.276.012-6.424.089m-.086 4.049a1.152 1.152 0 0 0-.469.684c-.076.404.195.88.608 1.068.212.096.524.101 6.383.101 5.858 0 6.17-.005 6.382-.101.413-.188.684-.664.608-1.068a1.152 1.152 0 0 0-.469-.684l-.185-.125H10.081l-.185.125m.201 3.917c-.485.173-.783.716-.646 1.176.08.265.288.526.524.655.159.087.63.096 6.195.111 3.312.009 6.151.001 6.308-.017.649-.077 1.073-.671.898-1.255-.087-.29-.409-.613-.69-.691-.153-.042-2.025-.06-6.296-.058-5.063.001-6.114.015-6.293.079"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path d="M5.661 4.054c-.538.229-.79.821-.572 1.339.042.099.367.498.723.887l.647.707-.256.001c-.971.007-1.708.293-2.315.9a2.852 2.852 0 0 0-.85 1.712c-.036.297-.05 2.953-.04 7.707l.015 7.253.12.351a3.13 3.13 0 0 0 1.534 1.759c.234.116.556.235.714.264.183.034 3.714.053 9.763.053h9.477l.426.448c.464.488.694.603 1.102.548.615-.082 1.008-.793.762-1.376-.088-.208-20.036-22.184-20.341-22.41-.224-.165-.686-.238-.909-.143m5.996.025c-.259.093-.449.282-.694.691-.305.51-.251.998.15 1.35.236.207.375.247.756.215a.774.774 0 0 0 .501-.193l.21-.169h6.87l.822 1.238c.452.681.891 1.318.976 1.416.318.367.242.357 2.779.386 2.386.028 2.394.029 2.658.26.055.048.139.159.187.247.08.146.09.786.115 7.093l.026 6.934.127.203c.201.324.474.474.86.474s.659-.15.86-.474l.127-.203.015-6.747c.011-4.588-.003-6.892-.042-7.2a2.937 2.937 0 0 0-1.653-2.283c-.611-.297-.918-.33-3.036-.33h-1.748l-.873-1.312c-.48-.721-.94-1.368-1.023-1.438a1.114 1.114 0 0 0-.349-.181c-.128-.036-1.637-.055-4.305-.053-3.377.001-4.143.015-4.316.076m-1.616 6.853c.96 1.056 1.746 1.944 1.746 1.974 0 .029-.109.202-.242.382a5.819 5.819 0 0 0-.855 1.832c-.758 3.003 1.012 5.958 4.03 6.725.551.14 2.047.142 2.56.003a6.079 6.079 0 0 0 1.464-.595c.232-.132.435-.238.452-.236.017.002.839.896 1.826 1.987l1.795 1.983-8.462.014c-9.444.016-8.729.045-9.095-.361-.103-.114-.202-.286-.219-.383-.017-.098-.025-3.433-.016-7.412L5.04 9.61l.161-.199c.311-.384.372-.398 1.813-.398h1.282l1.745 1.919m5.505 6.055 2.277 2.506-.368.174a3.466 3.466 0 0 1-3.935-.71c-.999-1.019-1.293-2.54-.736-3.81.191-.437.394-.764.443-.712l2.319 2.552"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path d="M5.661 4.054c-.538.229-.791.822-.571 1.339.042.099.63.789 1.306 1.533.677.743 1.231 1.367 1.231 1.386 0 .019-.114.106-.254.193-2.068 1.29-4.182 3.499-5.615 5.865-.865 1.43-.897 1.598-.463 2.442 1.268 2.468 3.459 4.919 5.799 6.489 2.068 1.388 4.061 2.149 6.666 2.547.722.11 2.975.155 3.822.076a15.598 15.598 0 0 0 4.429-1.075l.496-.202 1.388 1.53c.763.841 1.467 1.584 1.564 1.651.613.421 1.455.003 1.514-.751a1.042 1.042 0 0 0-.058-.464c-.045-.102-.518-.666-1.053-1.253A3497.39 3497.39 0 0 1 9.794 7.68C8.132 5.847 6.68 4.279 6.569 4.197c-.223-.165-.685-.238-.908-.143m8.698 2.025c-.881.1-1.288.175-1.44.268a1 1 0 0 0-.186 1.575c.292.284.494.315 1.286.202 1.012-.146 3.123-.132 4.141.026 2.9.452 5.334 1.657 7.562 3.743 1.048.98 2.444 2.761 3.028 3.862l.13.245-.133.253c-.233.443-1.008 1.564-1.538 2.227-.281.352-.853.981-1.269 1.397-.718.718-.761.773-.824 1.052-.057.255-.053.325.027.535.188.492.722.766 1.21.62.252-.075.889-.632 1.571-1.372 1.523-1.655 3.063-4.028 3.063-4.721 0-.294-.319-.961-.909-1.901-1.849-2.948-4.574-5.384-7.49-6.694-1.38-.619-2.647-.982-4.348-1.245-.589-.091-3.277-.141-3.881-.072m-4.182 4.998c.592.651 1.076 1.198 1.076 1.216 0 .018-.099.179-.22.357-.323.475-.672 1.248-.833 1.842-.129.476-.141.608-.141 1.508s.012 1.032.141 1.508c.171.63.523 1.391.89 1.921.324.47 1.075 1.213 1.543 1.529a7.016 7.016 0 0 0 1.859.841c.469.127.617.141 1.481.144 1.247.005 1.793-.121 2.8-.642l.434-.223c.026-.013 1.128 1.165 1.768 1.891l.102.115-.739.241c-1.525.497-2.559.649-4.365.645-1.149-.003-1.514-.024-2.133-.12-3.043-.475-5.588-1.78-7.815-4.008a16.473 16.473 0 0 1-2.545-3.213c-.183-.299-.333-.583-.333-.63 0-.126.777-1.35 1.295-2.037a15.97 15.97 0 0 1 3.208-3.21c.703-.524 1.377-.949 1.42-.897.017.021.516.571 1.107 1.222m6.436-.918c-.256.09-.586.442-.633.673-.113.569.188 1.083.716 1.22l.437.112c.375.095.974.408 1.328.692a3.97 3.97 0 0 1 1.492 2.564c.08.501.21.758.467.929.43.284.928.22 1.283-.164.242-.263.299-.474.247-.919-.089-.763-.384-1.646-.758-2.27-.885-1.473-2.22-2.46-3.831-2.834-.425-.098-.474-.099-.748-.003m-1.311 6.559c2.92 3.215 2.689 2.837 1.898 3.104-.377.128-.51.145-1.147.147-.841.004-1.179-.069-1.866-.403-.383-.186-.538-.305-.988-.76-.586-.591-.826-.977-1.047-1.686-.163-.52-.177-1.59-.029-2.133.157-.575.485-1.23.57-1.139.024.025 1.198 1.316 2.609 2.87"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 12 13" xmlns="http://www.w3.org/2000/svg">
<path d="M4.596.688a.738.738 0 0 0-.417.429.852.852 0 0 0 .022.571c.065.13.183.261.293.325.156.092.27.106.866.11.448.002.51.006.336.018-1.735.125-3.237 1.104-4.012 2.615a4.873 4.873 0 0 0-.498 1.505c-.051.326-.051 1.152 0 1.478.185 1.165.798 2.255 1.679 2.987.688.571 1.522.95 2.396 1.088.325.051 1.153.051 1.478 0a4.987 4.987 0 0 0 2.396-1.088c.881-.732 1.495-1.823 1.679-2.987.051-.325.051-1.153 0-1.478-.184-1.164-.798-2.255-1.679-2.987a4.917 4.917 0 0 0-2.831-1.133c-.174-.012-.112-.016.336-.018.596-.004.71-.018.866-.11a.847.847 0 0 0 .294-.325c.044-.086.05-.124.05-.308s-.006-.224-.052-.32A.825.825 0 0 0 7.44.702L7.33.65 6.02.645 4.71.641l-.114.047M6.42 3.649c.484.064.949.227 1.339.469.184.114.461.325.541.412.046.049.04.048-.057-.016a.741.741 0 0 0-.713-.034c-.104.05-.255.194-1.142 1.084C5.219 6.738 5.271 6.672 5.27 7c0 .164.007.205.053.304a.765.765 0 0 0 .354.361c.121.06.148.065.323.065.328-.001.261.052 1.436-1.118.89-.887 1.034-1.038 1.084-1.142a.741.741 0 0 0-.034-.713c-.064-.097-.065-.103-.016-.057.162.149.462.591.585.86.329.72.407 1.464.232 2.208a3.265 3.265 0 0 1-.866 1.579 3.362 3.362 0 0 1-4.51.304A3.398 3.398 0 0 1 2.819 8.13c-.537-1.503.052-3.162 1.425-4.014a3.396 3.396 0 0 1 2.176-.467"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -22,253 +22,268 @@ const customFirmwareBuild =
// integration tests in trezor-firmware repo use 2.99.99 version
process.env.TESTS_FIRMWARE === '2.99.99';
const tests = [
{
description: 'T2T1/T2B1 features',
skip: ['1'],
params: {},
result: {
device_id: expect.any(String),
vendor: 'trezor.io',
major_version: expect.any(Number),
minor_version: expect.any(Number),
patch_version: expect.any(Number),
bootloader_mode: null,
pin_protection: expect.any(Boolean),
passphrase_protection: expect.any(Boolean),
language: 'en-US',
label: expect.any(String),
initialized: true,
revision: expect.any(String),
bootloader_hash: null,
imported: null,
unlocked: expect.any(Boolean),
firmware_present: null,
backup_availability: expect.any(String),
// flags: expect.any(Number), // flags may be changed by applyFlags test
model: expect.any(String), // "T" | "R"
internal_model: expect.any(String),
fw_major: null,
fw_minor: null,
fw_patch: null,
// fw_vendor: null, // "EMULATOR" since 2.5.1
unfinished_backup: expect.any(Boolean),
no_backup: expect.any(Boolean),
recovery_status: 'Nothing',
capabilities: expect.arrayContaining([
'Capability_Bitcoin',
'Capability_Bitcoin_like',
'Capability_Binance',
'Capability_Cardano',
'Capability_Crypto',
'Capability_Ethereum',
'Capability_Monero',
'Capability_Ripple',
'Capability_Stellar',
'Capability_Tezos',
'Capability_U2F',
'Capability_Shamir',
'Capability_ShamirGroups',
'Capability_PassphraseEntry',
'Capability_NEM',
'Capability_EOS',
]),
backup_type: 'Bip39',
sd_card_present: expect.any(Boolean), // T2T1 true, T2B1 false
sd_protection: false,
wipe_code_protection: false,
session_id: expect.any(String),
passphrase_always_on_device: false,
flags: expect.any(Number),
fw_vendor: expect.any(String),
safety_checks: expect.any(String),
auto_lock_delay_ms: expect.any(Number),
display_rotation: expect.any(Number),
experimental_features: expect.any(Boolean),
},
legacyResults: [
{
rules: ['<2.4.2'], // 2.4.2 removed Lisk capability
success: true,
payload: {
capabilities: expect.arrayContaining([
'Capability_Bitcoin',
'Capability_Bitcoin_like',
'Capability_Binance',
'Capability_Cardano',
'Capability_Crypto',
'Capability_EOS',
'Capability_Ethereum',
'Capability_Lisk',
'Capability_Monero',
'Capability_NEM',
'Capability_Ripple',
'Capability_Stellar',
'Capability_Tezos',
'Capability_U2F',
'Capability_Shamir',
'Capability_ShamirGroups',
'Capability_PassphraseEntry',
]),
session_id: expect.any(String),
passphrase_always_on_device: false,
},
},
{
rules: ['<2.2.1'], // 2.2.1 added PassphraseEntry capability
success: true,
payload: {
capabilities: expect.arrayContaining([
'Capability_Bitcoin',
'Capability_Bitcoin_like',
'Capability_Binance',
'Capability_Cardano',
'Capability_Crypto',
'Capability_EOS',
'Capability_Ethereum',
'Capability_Lisk',
'Capability_Monero',
'Capability_NEM',
'Capability_Ripple',
'Capability_Stellar',
'Capability_Tezos',
'Capability_U2F',
'Capability_Shamir',
'Capability_ShamirGroups',
]),
backup_type: 'Bip39',
session_id: null,
passphrase_always_on_device: null,
},
},
{
rules: ['<2.1.6'], // 2.1.6 added ShamirGroups capability
success: true,
payload: {
capabilities: expect.arrayContaining([
'Capability_Bitcoin',
'Capability_Bitcoin_like',
'Capability_Binance',
'Capability_Cardano',
'Capability_Crypto',
'Capability_EOS',
'Capability_Ethereum',
'Capability_Lisk',
'Capability_Monero',
'Capability_NEM',
'Capability_Ripple',
'Capability_Stellar',
'Capability_Tezos',
'Capability_U2F',
'Capability_Shamir',
]),
backup_type: undefined,
},
},
{
rules: ['<2.1.5'], // 2.1.5 added Shamir and Lisk capability
success: true,
payload: {
capabilities: expect.arrayContaining([
'Capability_Bitcoin',
'Capability_Bitcoin_like',
'Capability_Binance',
'Capability_Cardano',
'Capability_Crypto',
'Capability_EOS',
'Capability_Ethereum',
'Capability_Monero',
'Capability_NEM',
'Capability_Ripple',
'Capability_Stellar',
'Capability_Tezos',
'Capability_U2F',
]),
backup_type: undefined,
},
},
],
},
{
description: 'T1B1 features',
skip: ['2'],
params: {},
result: {
device_id: expect.any(String),
vendor: 'trezor.io',
major_version: customFirmwareBuild ? expect.any(Number) : Number(major),
minor_version: customFirmwareBuild ? expect.any(Number) : Number(minor),
patch_version: customFirmwareBuild ? expect.any(Number) : Number(patch),
bootloader_mode: null,
pin_protection: expect.any(Boolean),
passphrase_protection: expect.any(Boolean),
language: 'en-US',
label: expect.any(String),
initialized: true,
revision: expect.any(String),
bootloader_hash: expect.any(String), // difference between T2T1
imported: true, // difference between T2T1
unlocked: expect.any(Boolean),
firmware_present: null,
backup_availability: expect.any(String),
// flags: null, // flags may be changed by applyFlags test
model: '1',
internal_model: DeviceModelInternal.T1B1,
fw_major: null,
fw_minor: null,
fw_patch: null,
// fw_vendor: null, // "EMULATOR" since 1.11.1
unfinished_backup: expect.any(Boolean),
no_backup: expect.any(Boolean),
recovery_status: 'Nothing', // difference between T2T1
capabilities: expect.arrayContaining([
'Capability_Bitcoin',
'Capability_Bitcoin_like',
'Capability_Crypto',
'Capability_Ethereum',
'Capability_NEM',
'Capability_Stellar',
'Capability_U2F',
]),
recovery_type: undefined, // difference between T2T1
sd_card_present: null, // no sdcard in T1B1
sd_protection: null, // no sdcard in T1B1
wipe_code_protection: false,
session_id: expect.any(String),
passphrase_always_on_device: null,
flags: expect.any(Number),
fw_vendor: expect.any(String),
safety_checks: expect.any(String),
auto_lock_delay_ms: expect.any(Number),
display_rotation: expect.any(Number),
experimental_features: expect.any(Boolean),
backup_type: 'Bip39',
},
legacyResults: [
{
rules: ['<1.10.3'], // 1.10.3 removed Lisk capability
success: true,
payload: {
capabilities: expect.arrayContaining([
'Capability_Bitcoin',
'Capability_Bitcoin_like',
'Capability_Crypto',
'Capability_Ethereum',
'Capability_Lisk',
'Capability_NEM',
'Capability_Stellar',
'Capability_U2F',
]),
},
},
{
rules: ['<1.8.3'], // 1.8.3 added Lisk capability
success: true,
payload: {
capabilities: expect.arrayContaining([
'Capability_Bitcoin',
'Capability_Bitcoin_like',
'Capability_Crypto',
'Capability_Ethereum',
'Capability_NEM',
'Capability_Stellar',
'Capability_U2F',
]),
},
},
],
},
];
export default {
method: 'getFeatures',
setup: {
mnemonic: 'mnemonic_12',
},
tests: [
{
description: 'T2T1/T2B1 features',
skip: ['1'],
params: {},
result: {
device_id: expect.any(String),
vendor: 'trezor.io',
major_version: expect.any(Number),
minor_version: expect.any(Number),
patch_version: expect.any(Number),
bootloader_mode: null,
pin_protection: expect.any(Boolean),
passphrase_protection: expect.any(Boolean),
language: 'en-US',
label: expect.any(String),
initialized: true,
revision: expect.any(String),
bootloader_hash: null,
imported: null,
unlocked: expect.any(Boolean),
firmware_present: null,
needs_backup: expect.any(Boolean),
// flags: expect.any(Number), // flags may be changed by applyFlags test
model: expect.any(String), // "T" | "R"
internal_model: expect.any(String),
fw_major: null,
fw_minor: null,
fw_patch: null,
// fw_vendor: null, // "EMULATOR" since 2.5.1
unfinished_backup: expect.any(Boolean),
no_backup: expect.any(Boolean),
recovery_mode: false,
capabilities: expect.arrayContaining([
'Capability_Bitcoin',
'Capability_Bitcoin_like',
'Capability_Binance',
'Capability_Cardano',
'Capability_Crypto',
'Capability_Ethereum',
'Capability_Monero',
'Capability_Ripple',
'Capability_Stellar',
'Capability_Tezos',
'Capability_U2F',
'Capability_Shamir',
'Capability_ShamirGroups',
'Capability_PassphraseEntry',
'Capability_NEM',
'Capability_EOS',
]),
backup_type: 'Bip39',
sd_card_present: expect.any(Boolean), // T2T1 true, T2B1 false
sd_protection: false,
wipe_code_protection: false,
session_id: expect.any(String),
passphrase_always_on_device: false,
},
legacyResults: [
{
rules: ['<2.4.2'], // 2.4.2 removed Lisk capability
success: true,
payload: {
capabilities: expect.arrayContaining([
'Capability_Bitcoin',
'Capability_Bitcoin_like',
'Capability_Binance',
'Capability_Cardano',
'Capability_Crypto',
'Capability_EOS',
'Capability_Ethereum',
'Capability_Lisk',
'Capability_Monero',
'Capability_NEM',
'Capability_Ripple',
'Capability_Stellar',
'Capability_Tezos',
'Capability_U2F',
'Capability_Shamir',
'Capability_ShamirGroups',
'Capability_PassphraseEntry',
]),
session_id: expect.any(String),
passphrase_always_on_device: false,
},
},
{
rules: ['<2.2.1'], // 2.2.1 added PassphraseEntry capability
success: true,
payload: {
capabilities: expect.arrayContaining([
'Capability_Bitcoin',
'Capability_Bitcoin_like',
'Capability_Binance',
'Capability_Cardano',
'Capability_Crypto',
'Capability_EOS',
'Capability_Ethereum',
'Capability_Lisk',
'Capability_Monero',
'Capability_NEM',
'Capability_Ripple',
'Capability_Stellar',
'Capability_Tezos',
'Capability_U2F',
'Capability_Shamir',
'Capability_ShamirGroups',
]),
backup_type: 'Bip39',
session_id: null,
passphrase_always_on_device: null,
},
},
{
rules: ['<2.1.6'], // 2.1.6 added ShamirGroups capability
success: true,
payload: {
capabilities: expect.arrayContaining([
'Capability_Bitcoin',
'Capability_Bitcoin_like',
'Capability_Binance',
'Capability_Cardano',
'Capability_Crypto',
'Capability_EOS',
'Capability_Ethereum',
'Capability_Lisk',
'Capability_Monero',
'Capability_NEM',
'Capability_Ripple',
'Capability_Stellar',
'Capability_Tezos',
'Capability_U2F',
'Capability_Shamir',
]),
backup_type: undefined,
},
},
{
rules: ['<2.1.5'], // 2.1.5 added Shamir and Lisk capability
success: true,
payload: {
capabilities: expect.arrayContaining([
'Capability_Bitcoin',
'Capability_Bitcoin_like',
'Capability_Binance',
'Capability_Cardano',
'Capability_Crypto',
'Capability_EOS',
'Capability_Ethereum',
'Capability_Monero',
'Capability_NEM',
'Capability_Ripple',
'Capability_Stellar',
'Capability_Tezos',
'Capability_U2F',
]),
backup_type: undefined,
},
},
],
},
{
description: 'T1B1 features',
skip: ['2'],
params: {},
result: {
device_id: expect.any(String),
vendor: 'trezor.io',
major_version: customFirmwareBuild ? expect.any(Number) : Number(major),
minor_version: customFirmwareBuild ? expect.any(Number) : Number(minor),
patch_version: customFirmwareBuild ? expect.any(Number) : Number(patch),
bootloader_mode: null,
pin_protection: expect.any(Boolean),
passphrase_protection: expect.any(Boolean),
language: 'en-US',
label: expect.any(String),
initialized: true,
revision: expect.any(String),
bootloader_hash: expect.any(String), // difference between T2T1
imported: true, // difference between T2T1
unlocked: expect.any(Boolean),
firmware_present: null,
needs_backup: expect.any(Boolean),
// flags: null, // flags may be changed by applyFlags test
model: '1',
internal_model: DeviceModelInternal.T1B1,
fw_major: null,
fw_minor: null,
fw_patch: null,
// fw_vendor: null, // "EMULATOR" since 1.11.1
unfinished_backup: expect.any(Boolean),
no_backup: expect.any(Boolean),
recovery_mode: null, // difference between T2T1
capabilities: expect.arrayContaining([
'Capability_Bitcoin',
'Capability_Bitcoin_like',
'Capability_Crypto',
'Capability_Ethereum',
'Capability_NEM',
'Capability_Stellar',
'Capability_U2F',
]),
backup_type: undefined, // difference between T2T1
sd_card_present: null, // no sdcard in T1B1
sd_protection: null, // no sdcard in T1B1
wipe_code_protection: false,
session_id: expect.any(String),
passphrase_always_on_device: null, // no passphrase input on T1B1 device
},
legacyResults: [
{
rules: ['<1.10.3'], // 1.10.3 removed Lisk capability
success: true,
payload: {
capabilities: expect.arrayContaining([
'Capability_Bitcoin',
'Capability_Bitcoin_like',
'Capability_Crypto',
'Capability_Ethereum',
'Capability_Lisk',
'Capability_NEM',
'Capability_Stellar',
'Capability_U2F',
]),
},
},
{
rules: ['<1.8.3'], // 1.8.3 added Lisk capability
success: true,
payload: {
capabilities: expect.arrayContaining([
'Capability_Bitcoin',
'Capability_Bitcoin_like',
'Capability_Crypto',
'Capability_Ethereum',
'Capability_NEM',
'Capability_Stellar',
'Capability_U2F',
]),
},
},
],
},
],
tests,
};

View File

@@ -19,7 +19,7 @@ export const getDeviceFeatures = (feat?: Partial<Features>): Features => ({
imported: null,
unlocked: true,
firmware_present: null,
needs_backup: false,
backup_availability: 'NotAvailable',
flags: 0,
model: 'T',
internal_model: DeviceModelInternal.T2T1,
@@ -29,7 +29,7 @@ export const getDeviceFeatures = (feat?: Partial<Features>): Features => ({
fw_vendor: null,
unfinished_backup: false,
no_backup: false,
recovery_mode: false,
recovery_status: 'Nothing',
capabilities: [],
backup_type: 'Bip39',
sd_card_present: false,

View File

@@ -21,10 +21,11 @@ export default class RecoveryDevice extends AbstractMethod<'recoveryDevice', PRO
language: payload.language,
label: payload.label,
enforce_wordlist: payload.enforce_wordlist,
input_method: payload.input_method,
type: payload.type,
u2f_counter: payload.u2f_counter,
dry_run: payload.dry_run,
};
this.allowDeviceMode = [...this.allowDeviceMode, UI.INITIALIZE];
this.useDeviceState = false;
}

View File

@@ -317,7 +317,7 @@ const inner = async (method: AbstractMethod<any>, device: Device) => {
}
}
const deviceNeedsBackup = device.features.needs_backup;
const deviceNeedsBackup = device.features.backup_availability === 'Required';
if (deviceNeedsBackup) {
if (
method.noBackupConfirmationMode === 'always' ||

View File

@@ -1,4 +1,4 @@
import { TrezorConnect } from '../../..';
import { PROTO, TrezorConnect } from '../../..';
export const management = async (api: TrezorConnect) => {
const reset = await api.resetDevice({
@@ -65,8 +65,8 @@ export const management = async (api: TrezorConnect) => {
passphrase_protection: true,
pin_protection: true,
label: 'My Trezor',
type: 1,
dry_run: true,
input_method: PROTO.RecoveryDeviceInputMethod.Matrix,
type: PROTO.RecoveryType.DryRun,
word_count: 24,
});
if (recovery.success) recovery.payload.message.toLowerCase();

View File

@@ -1,5 +1,6 @@
module.exports = {
rules: {
'@typescript-eslint/ban-types': 'off', // allow {} in protobuf.d.ts
'@typescript-eslint/no-shadow': 'off',
},
};

View File

@@ -4450,8 +4450,8 @@
"type": "bool",
"id": 18
},
"needs_backup": {
"type": "bool",
"backup_availability": {
"type": "BackupAvailability",
"id": 19
},
"flags": {
@@ -4486,8 +4486,8 @@
"type": "bool",
"id": 28
},
"recovery_mode": {
"type": "bool",
"recovery_status": {
"type": "RecoveryStatus",
"id": 29
},
"capabilities": {
@@ -4584,9 +4584,31 @@
"unit_packaging": {
"type": "uint32",
"id": 51
},
"haptic_feedback": {
"type": "bool",
"id": 52
},
"recovery_type": {
"type": "RecoveryType",
"id": 53
}
},
"nested": {
"BackupAvailability": {
"values": {
"NotAvailable": 0,
"Required": 1,
"Available": 2
}
},
"RecoveryStatus": {
"values": {
"Nothing": 0,
"Recovery": 1,
"Backup": 2
}
},
"Capability": {
"options": {
"(has_bitcoin_only_values)": true
@@ -4612,6 +4634,12 @@
},
"Capability_Translations": {
"(bitcoin_only)": true
},
"Capability_Brightness": {
"(bitcoin_only)": true
},
"Capability_Haptic": {
"(bitcoin_only)": true
}
},
"values": {
@@ -4633,7 +4661,9 @@
"Capability_ShamirGroups": 16,
"Capability_PassphraseEntry": 17,
"Capability_Solana": 18,
"Capability_Translations": 19
"Capability_Translations": 19,
"Capability_Brightness": 20,
"Capability_Haptic": 21
}
}
}
@@ -4703,6 +4733,10 @@
"hide_passphrase_from_host": {
"type": "bool",
"id": 11
},
"haptic_feedback": {
"type": "bool",
"id": 13
}
}
},
@@ -5031,28 +5065,38 @@
"type": "bool",
"id": 6
},
"type": {
"type": "RecoveryDeviceType",
"input_method": {
"type": "RecoveryDeviceInputMethod",
"id": 8
},
"u2f_counter": {
"type": "uint32",
"id": 9
},
"dry_run": {
"type": "bool",
"id": 10
"type": {
"type": "RecoveryType",
"id": 10,
"options": {
"default": "NormalRecovery"
}
}
},
"nested": {
"RecoveryDeviceType": {
"RecoveryDeviceInputMethod": {
"values": {
"RecoveryDeviceType_ScrambledWords": 0,
"RecoveryDeviceType_Matrix": 1
"ScrambledWords": 0,
"Matrix": 1
}
}
}
},
"RecoveryType": {
"values": {
"NormalRecovery": 0,
"DryRun": 1,
"UnlockRepeatedBackup": 2
}
},
"WordRequest": {
"fields": {
"type": {
@@ -5188,6 +5232,14 @@
"UnlockBootloader": {
"fields": {}
},
"SetBrightness": {
"fields": {
"value": {
"type": "uint32",
"id": 1
}
}
},
"MoneroNetworkType": {
"values": {
"MAINNET": 0,
@@ -8081,6 +8133,10 @@
"(bitcoin_only)": true,
"(wire_in)": true
},
"MessageType_SetBrightness": {
"(bitcoin_only)": true,
"(wire_in)": true
},
"MessageType_SetU2FCounter": {
"(wire_in)": true
},
@@ -8765,6 +8821,7 @@
"MessageType_ChangeLanguage": 990,
"MessageType_TranslationDataRequest": 991,
"MessageType_TranslationDataAck": 992,
"MessageType_SetBrightness": 993,
"MessageType_SetU2FCounter": 63,
"MessageType_GetNextU2FCounter": 80,
"MessageType_NextU2FCounter": 81,

View File

@@ -48,7 +48,7 @@ const RULE_PATCH = {
'Features.imported': 'required',
'Features.unlocked': 'required',
'Features.firmware_present': 'required',
'Features.needs_backup': 'required',
'Features.backup_availability': 'required',
'Features.flags': 'required',
'Features.fw_major': 'required',
'Features.fw_minor': 'required',
@@ -58,7 +58,7 @@ const RULE_PATCH = {
'Features.internal_model': 'required',
'Features.unfinished_backup': 'required',
'Features.no_backup': 'required',
'Features.recovery_mode': 'required',
'Features.recovery_status': 'required',
'Features.backup_type': 'required',
'Features.sd_card_present': 'required',
'Features.sd_protection': 'required',
@@ -95,7 +95,7 @@ const TYPE_PATCH = {
'Features.imported': 'boolean | null',
'Features.unlocked': 'boolean | null',
'Features.firmware_present': 'boolean | null',
'Features.needs_backup': 'boolean | null',
'Features.backup_availability': 'BackupAvailability | null',
'Features.flags': 'number | null',
'Features.fw_major': 'number | null',
'Features.fw_minor': 'number | null',
@@ -103,7 +103,7 @@ const TYPE_PATCH = {
'Features.fw_vendor': 'string | null',
'Features.unfinished_backup': 'boolean | null',
'Features.no_backup': 'boolean | null',
'Features.recovery_mode': 'boolean | null',
'Features.recovery_status': 'RecoveryStatus | null',
'Features.backup_type': 'BackupType | null',
'Features.sd_card_present': 'boolean | null',
'Features.sd_protection': 'boolean | null',
@@ -204,6 +204,7 @@ const TYPE_PATCH = {
'TezosDelegationOp.source': 'Uint8Array',
'TezosDelegationOp.delegate': 'Uint8Array',
'TezosSignTx.branch': 'Uint8Array',
'Features.recovery_type': 'RecoveryType',
};
const DEFINITION_PATCH = {

View File

@@ -11,6 +11,13 @@ const {
SINT_TYPE,
} = require('./protobuf-patches');
CONSOLE_RED = "\x1b[31m"
CONSOLE_RESET = "\x1b[0m"
const logError = (text) => {
console.error(`Error: ${CONSOLE_RED}${text}${CONSOLE_RESET}`);
}
const INDENT = ' '.repeat(4);
// proto types to javascript types
@@ -38,6 +45,9 @@ const ENUM_KEYS = [
'PinMatrixRequestType',
'WordRequestType',
'HomescreenFormat',
"RecoveryStatus",
"BackupAvailability",
"RecoveryType",
];
const parseEnum = (itemName, item) => {
@@ -131,26 +141,40 @@ const parseMessage = (messageName, message, depth = 0) => {
// top level messages and nested messages
Object.keys(json.nested).map(e => parseMessage(e, json.nested[e]));
// types needs reordering (used before defined)
// Types needs reordering (used before defined).
// The Type in the Value NEEDs (depends on) the Type in the Key.
const ORDER = {
BinanceCoin: 'BinanceInputOutput',
HDNodeType: 'HDNodePathType',
CardanoAssetGroupType: 'CardanoTxOutputType',
CardanoTokenType: 'CardanoAssetGroupType',
TxAck: 'TxAckInputWrapper',
EthereumFieldType: 'EthereumStructMember',
EthereumDataType: 'EthereumFieldType',
PaymentRequestMemo: 'TxAckPaymentRequest',
RecoveryDevice: 'Features',
RecoveryType: 'RecoveryDevice',
RecoveryDeviceInputMethod: 'RecoveryType',
};
Object.keys(ORDER).forEach(key => {
if (key === ORDER[key]) {
logError(`ORDER map cannot have key=value`)
}
// find indexes
const indexA = types.findIndex(t => t && t.name === key);
const indexB = types.findIndex(t => t && t.name === ORDER[key]);
const prevA = types[indexA];
const indexOfDependency = types.findIndex(t => t && t.name === key);
if (indexOfDependency === -1) {
logError(`Type from key: '${key}' not found in the 'types' variable!`)
}
const indexOfDependant = types.findIndex(t => t && t.name === ORDER[key]);
if (indexOfDependant === -1) {
logError(`Type from value: '${ORDER[key]}' not found in the 'types' variable!`)
}
const dependency = types[indexOfDependency];
// replace values
delete types[indexA];
types.splice(indexB, 0, prevA);
delete types[indexOfDependency];
types.splice(indexOfDependant, 0, dependency);
});
// skip not needed types

View File

@@ -2200,6 +2200,32 @@ export const Initialize = Type.Object(
export type GetFeatures = Static<typeof GetFeatures>;
export const GetFeatures = Type.Object({}, { $id: 'GetFeatures' });
export enum Enum_BackupAvailability {
NotAvailable = 0,
Required = 1,
Available = 2,
}
export type EnumEnum_BackupAvailability = Static<typeof EnumEnum_BackupAvailability>;
export const EnumEnum_BackupAvailability = Type.Enum(Enum_BackupAvailability);
export type BackupAvailability = Static<typeof BackupAvailability>;
export const BackupAvailability = Type.KeyOfEnum(Enum_BackupAvailability, {
$id: 'BackupAvailability',
});
export enum Enum_RecoveryStatus {
Nothing = 0,
Recovery = 1,
Backup = 2,
}
export type EnumEnum_RecoveryStatus = Static<typeof EnumEnum_RecoveryStatus>;
export const EnumEnum_RecoveryStatus = Type.Enum(Enum_RecoveryStatus);
export type RecoveryStatus = Static<typeof RecoveryStatus>;
export const RecoveryStatus = Type.KeyOfEnum(Enum_RecoveryStatus, { $id: 'RecoveryStatus' });
export enum Enum_Capability {
Capability_Bitcoin = 1,
Capability_Bitcoin_like = 2,
@@ -2220,6 +2246,8 @@ export enum Enum_Capability {
Capability_PassphraseEntry = 17,
Capability_Solana = 18,
Capability_Translations = 19,
Capability_Brightness = 20,
Capability_Haptic = 21,
}
export type EnumEnum_Capability = Static<typeof EnumEnum_Capability>;
@@ -2228,6 +2256,42 @@ export const EnumEnum_Capability = Type.Enum(Enum_Capability);
export type Capability = Static<typeof Capability>;
export const Capability = Type.KeyOfEnum(Enum_Capability, { $id: 'Capability' });
export enum RecoveryDeviceInputMethod {
ScrambledWords = 0,
Matrix = 1,
}
export type EnumRecoveryDeviceInputMethod = Static<typeof EnumRecoveryDeviceInputMethod>;
export const EnumRecoveryDeviceInputMethod = Type.Enum(RecoveryDeviceInputMethod);
export enum Enum_RecoveryType {
NormalRecovery = 0,
DryRun = 1,
UnlockRepeatedBackup = 2,
}
export type EnumEnum_RecoveryType = Static<typeof EnumEnum_RecoveryType>;
export const EnumEnum_RecoveryType = Type.Enum(Enum_RecoveryType);
export type RecoveryType = Static<typeof RecoveryType>;
export const RecoveryType = Type.KeyOfEnum(Enum_RecoveryType, { $id: 'RecoveryType' });
export type RecoveryDevice = Static<typeof RecoveryDevice>;
export const RecoveryDevice = Type.Object(
{
word_count: Type.Optional(Type.Number()),
passphrase_protection: Type.Optional(Type.Boolean()),
pin_protection: Type.Optional(Type.Boolean()),
language: Type.Optional(Type.String()),
label: Type.Optional(Type.String()),
enforce_wordlist: Type.Optional(Type.Boolean()),
input_method: Type.Optional(EnumRecoveryDeviceInputMethod),
u2f_counter: Type.Optional(Type.Number()),
type: Type.Optional(RecoveryType),
},
{ $id: 'RecoveryDevice' },
);
export type Features = Static<typeof Features>;
export const Features = Type.Object(
{
@@ -2248,7 +2312,7 @@ export const Features = Type.Object(
unlocked: Type.Union([Type.Boolean(), Type.Null()]),
_passphrase_cached: Type.Optional(Type.Boolean()),
firmware_present: Type.Union([Type.Boolean(), Type.Null()]),
needs_backup: Type.Union([Type.Boolean(), Type.Null()]),
backup_availability: Type.Union([BackupAvailability, Type.Null()]),
flags: Type.Union([Type.Number(), Type.Null()]),
model: Type.String(),
fw_major: Type.Union([Type.Number(), Type.Null()]),
@@ -2257,7 +2321,7 @@ export const Features = Type.Object(
fw_vendor: Type.Union([Type.String(), Type.Null()]),
unfinished_backup: Type.Union([Type.Boolean(), Type.Null()]),
no_backup: Type.Union([Type.Boolean(), Type.Null()]),
recovery_mode: Type.Union([Type.Boolean(), Type.Null()]),
recovery_status: Type.Union([RecoveryStatus, Type.Null()]),
capabilities: Type.Array(Capability),
backup_type: Type.Union([BackupType, Type.Null()]),
sd_card_present: Type.Union([Type.Boolean(), Type.Null()]),
@@ -2280,6 +2344,8 @@ export const Features = Type.Object(
bootloader_locked: Type.Optional(Type.Boolean()),
language_version_matches: Type.Optional(Type.Boolean()),
unit_packaging: Type.Optional(Type.Number()),
haptic_feedback: Type.Optional(Type.Boolean()),
recovery_type: Type.Optional(RecoveryType),
},
{ $id: 'Features' },
);
@@ -2312,6 +2378,7 @@ export const ApplySettings = Type.Object(
safety_checks: Type.Optional(SafetyCheckLevel),
experimental_features: Type.Optional(Type.Boolean()),
hide_passphrase_from_host: Type.Optional(Type.Boolean()),
haptic_feedback: Type.Optional(Type.Boolean()),
},
{ $id: 'ApplySettings' },
);
@@ -2493,30 +2560,6 @@ export const EntropyAck = Type.Object(
{ $id: 'EntropyAck' },
);
export enum RecoveryDeviceType {
RecoveryDeviceType_ScrambledWords = 0,
RecoveryDeviceType_Matrix = 1,
}
export type EnumRecoveryDeviceType = Static<typeof EnumRecoveryDeviceType>;
export const EnumRecoveryDeviceType = Type.Enum(RecoveryDeviceType);
export type RecoveryDevice = Static<typeof RecoveryDevice>;
export const RecoveryDevice = Type.Object(
{
word_count: Type.Optional(Type.Number()),
passphrase_protection: Type.Optional(Type.Boolean()),
pin_protection: Type.Optional(Type.Boolean()),
language: Type.Optional(Type.String()),
label: Type.Optional(Type.String()),
enforce_wordlist: Type.Optional(Type.Boolean()),
type: Type.Optional(EnumRecoveryDeviceType),
u2f_counter: Type.Optional(Type.Number()),
dry_run: Type.Optional(Type.Boolean()),
},
{ $id: 'RecoveryDevice' },
);
export enum Enum_WordRequestType {
WordRequestType_Plain = 0,
WordRequestType_Matrix9 = 1,
@@ -2625,6 +2668,14 @@ export const ShowDeviceTutorial = Type.Object({}, { $id: 'ShowDeviceTutorial' })
export type UnlockBootloader = Static<typeof UnlockBootloader>;
export const UnlockBootloader = Type.Object({}, { $id: 'UnlockBootloader' });
export type SetBrightness = Static<typeof SetBrightness>;
export const SetBrightness = Type.Object(
{
value: Type.Optional(Type.Number()),
},
{ $id: 'SetBrightness' },
);
export enum MoneroNetworkType {
MAINNET = 0,
TESTNET = 1,
@@ -3598,6 +3649,7 @@ export const MessageType = Type.Object(
EthereumTypedDataSignature,
Initialize,
GetFeatures,
RecoveryDevice,
Features,
LockDevice,
SetBusy,
@@ -3624,7 +3676,6 @@ export const MessageType = Type.Object(
BackupDevice,
EntropyRequest,
EntropyAck,
RecoveryDevice,
WordRequest,
WordAck,
SetU2FCounter,
@@ -3640,6 +3691,7 @@ export const MessageType = Type.Object(
UnlockedPathRequest,
ShowDeviceTutorial,
UnlockBootloader,
SetBrightness,
NEMGetAddress,
NEMAddress,
NEMTransactionCommon,

View File

@@ -1554,6 +1554,22 @@ export type Initialize = {
// GetFeatures
export type GetFeatures = {};
export enum Enum_BackupAvailability {
NotAvailable = 0,
Required = 1,
Available = 2,
}
export type BackupAvailability = keyof typeof Enum_BackupAvailability;
export enum Enum_RecoveryStatus {
Nothing = 0,
Recovery = 1,
Backup = 2,
}
export type RecoveryStatus = keyof typeof Enum_RecoveryStatus;
export enum Enum_Capability {
Capability_Bitcoin = 1,
Capability_Bitcoin_like = 2,
@@ -1574,10 +1590,38 @@ export enum Enum_Capability {
Capability_PassphraseEntry = 17,
Capability_Solana = 18,
Capability_Translations = 19,
Capability_Brightness = 20,
Capability_Haptic = 21,
}
export type Capability = keyof typeof Enum_Capability;
export enum RecoveryDeviceInputMethod {
ScrambledWords = 0,
Matrix = 1,
}
export enum Enum_RecoveryType {
NormalRecovery = 0,
DryRun = 1,
UnlockRepeatedBackup = 2,
}
export type RecoveryType = keyof typeof Enum_RecoveryType;
// RecoveryDevice
export type RecoveryDevice = {
word_count?: number;
passphrase_protection?: boolean;
pin_protection?: boolean;
language?: string;
label?: string;
enforce_wordlist?: boolean;
input_method?: RecoveryDeviceInputMethod;
u2f_counter?: number;
type?: RecoveryType;
};
// Features
export type Features = {
vendor: string;
@@ -1597,7 +1641,7 @@ export type Features = {
unlocked: boolean | null;
_passphrase_cached?: boolean;
firmware_present: boolean | null;
needs_backup: boolean | null;
backup_availability: BackupAvailability | null;
flags: number | null;
model: string;
fw_major: number | null;
@@ -1606,7 +1650,7 @@ export type Features = {
fw_vendor: string | null;
unfinished_backup: boolean | null;
no_backup: boolean | null;
recovery_mode: boolean | null;
recovery_status: RecoveryStatus | null;
capabilities: Capability[];
backup_type: BackupType | null;
sd_card_present: boolean | null;
@@ -1629,6 +1673,8 @@ export type Features = {
bootloader_locked?: boolean;
language_version_matches?: boolean;
unit_packaging?: number;
haptic_feedback?: boolean;
recovery_type?: RecoveryType;
};
// LockDevice
@@ -1655,6 +1701,7 @@ export type ApplySettings = {
safety_checks?: SafetyCheckLevel;
experimental_features?: boolean;
hide_passphrase_from_host?: boolean;
haptic_feedback?: boolean;
};
// ChangeLanguage
@@ -1776,24 +1823,6 @@ export type EntropyAck = {
entropy: string;
};
export enum RecoveryDeviceType {
RecoveryDeviceType_ScrambledWords = 0,
RecoveryDeviceType_Matrix = 1,
}
// RecoveryDevice
export type RecoveryDevice = {
word_count?: number;
passphrase_protection?: boolean;
pin_protection?: boolean;
language?: string;
label?: string;
enforce_wordlist?: boolean;
type?: RecoveryDeviceType;
u2f_counter?: number;
dry_run?: boolean;
};
export enum Enum_WordRequestType {
WordRequestType_Plain = 0,
WordRequestType_Matrix9 = 1,
@@ -1871,6 +1900,11 @@ export type ShowDeviceTutorial = {};
// UnlockBootloader
export type UnlockBootloader = {};
// SetBrightness
export type SetBrightness = {
value?: number;
};
export enum MoneroNetworkType {
MAINNET = 0,
TESTNET = 1,
@@ -2597,6 +2631,7 @@ export type MessageType = {
EthereumTypedDataSignature: EthereumTypedDataSignature;
Initialize: Initialize;
GetFeatures: GetFeatures;
RecoveryDevice: RecoveryDevice;
Features: Features;
LockDevice: LockDevice;
SetBusy: SetBusy;
@@ -2623,7 +2658,6 @@ export type MessageType = {
BackupDevice: BackupDevice;
EntropyRequest: EntropyRequest;
EntropyAck: EntropyAck;
RecoveryDevice: RecoveryDevice;
WordRequest: WordRequest;
WordAck: WordAck;
SetU2FCounter: SetU2FCounter;
@@ -2639,6 +2673,7 @@ export type MessageType = {
UnlockedPathRequest: UnlockedPathRequest;
ShowDeviceTutorial: ShowDeviceTutorial;
UnlockBootloader: UnlockBootloader;
SetBrightness: SetBrightness;
NEMGetAddress: NEMGetAddress;
NEMAddress: NEMAddress;
NEMTransactionCommon: NEMTransactionCommon;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +0,0 @@
import { generateForFile } from '../src/codegen';
describe('codegen', () => {
it('should generate code for protobuf messages example', () => {
const output = generateForFile(`${__dirname}/__snapshots__/codegen.input.ts`);
expect(output).toMatchSnapshot();
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -2033,6 +2033,7 @@
"TR_VIEW_ONLY_RADIOS_ENABLED_TITLE": "Enabled",
"TR_VIEW_ONLY_SEND_COINS_INFO": "You always need to connect Trezor to move coins.",
"TR_VIEW_ONLY_TOOLTIP_BUTTON": "Got it",
"TR_GOT_IT_BUTTON": "Got it",
"TR_VIEW_ONLY_TOOLTIP_CHANGE_INFO": "You can change it here",
"TR_VIEW_ONLY_TOOLTIP_DESCRIPTION": "You'll see wallet balances after disconnecting Trezor",
"TR_VIEW_ONLY_TOOLTIP_TITLE": "View-only enabled",

View File

@@ -1,5 +1,5 @@
import { deviceActions, selectDevice } from '@suite-common/wallet-core';
import TrezorConnect, { UI, RecoveryDevice, DeviceModelInternal } from '@trezor/connect';
import TrezorConnect, { UI, RecoveryDevice, DeviceModelInternal, PROTO } from '@trezor/connect';
import { analytics, EventType } from '@trezor/suite-analytics';
import { RECOVERY } from 'src/actions/recovery/constants';
@@ -8,6 +8,7 @@ import * as routerActions from 'src/actions/suite/routerActions';
import { Dispatch, GetState } from 'src/types/suite';
import { WordCount } from 'src/types/recovery';
import { DEFAULT_PASSPHRASE_PROTECTION } from 'src/constants/suite/device';
import { isRecoveryInProgress } from '../../utils/device/isRecoveryInProgress';
export type SeedInputStatus =
| 'initial'
@@ -67,8 +68,10 @@ const checkSeed = () => async (dispatch: Dispatch, getState: GetState) => {
}
const response = await TrezorConnect.recoveryDevice({
dry_run: true,
type: advancedRecovery ? 1 : 0,
type: device.features.recovery_type ?? 'DryRun', // For old firmware, we assume DryRun as it was the only option before
input_method: advancedRecovery
? PROTO.RecoveryDeviceInputMethod.Matrix
: PROTO.RecoveryDeviceInputMethod.ScrambledWords,
word_count: wordsCount,
enforce_wordlist: true,
device: {
@@ -105,7 +108,10 @@ const recoverDevice = () => async (dispatch: Dispatch, getState: GetState) => {
}
const params: RecoveryDevice = {
type: advancedRecovery ? 1 : 0,
type: device.features.recovery_type ?? 'NormalRecovery', // For old firmware, we assume NormalRecovery as it was the only option before
input_method: advancedRecovery
? PROTO.RecoveryDeviceInputMethod.Matrix
: PROTO.RecoveryDeviceInputMethod.ScrambledWords,
word_count: wordsCount,
passphrase_protection: DEFAULT_PASSPHRASE_PROTECTION,
enforce_wordlist: true,
@@ -167,7 +173,7 @@ const rerun = () => async (dispatch: Dispatch, getState: GetState) => {
const features = response.payload;
if (!features.recovery_mode) {
if (!isRecoveryInProgress(features)) {
return;
}

View File

@@ -49,7 +49,9 @@ export const CheckSeedStep = ({ onClose, onSuccess, willBeWiped }: CheckSeedStep
const handleCheckboxClick = () => setIsChecked(prev => !prev);
const getContent = () => {
const isBackedUp = !device?.features?.needs_backup && !device?.features?.unfinished_backup;
const isBackedUp =
device?.features?.backup_availability !== 'Required' &&
!device?.features?.unfinished_backup;
const noBackupHeading = (
<Translation

View File

@@ -18,14 +18,15 @@ interface LearnMoreButtonProps extends Omit<ButtonProps, 'children'> {
export const LearnMoreButton = ({
children,
url,
className,
size = 'tiny',
url,
...buttonProps
}: LearnMoreButtonProps) => (
<StyledTrezorLink variant="nostyle" href={url} className={className}>
<Button
variant="tertiary"
size="tiny"
size={size}
icon="EXTERNAL_LINK"
iconAlignment="right"
{...buttonProps}

View File

@@ -4,6 +4,8 @@ import { configureStore } from 'src/support/tests/configureStore';
import { renderWithProviders, findByTestId } from 'src/support/tests/hooksHelper';
import { Preloader } from '../Preloader';
import { AppState } from '../../../../reducers/store';
import { DeepPartial } from '@trezor/type-utils';
// render only Translation.id in data-test attribute
jest.mock('src/components/suite/Translation', () => ({
@@ -186,14 +188,16 @@ describe('Preloader component', () => {
});
it('Unacquired device', () => {
const device: DeepPartial<AppState['device']> = {
selectedDevice: { type: 'unacquired' },
};
const store = initStore(
getInitialState({
suite: {
transport: { type: 'BridgeTransport' },
},
device: {
selectedDevice: { type: 'unacquired' },
},
device,
}),
);
const { unmount } = renderWithProviders(store, <Index app={store.getState().router.app} />);
@@ -205,14 +209,16 @@ describe('Preloader component', () => {
});
it('Unreadable device: webusb HID', () => {
const device: DeepPartial<AppState['device']> = {
selectedDevice: { type: 'unreadable', error: 'LIBUSB_ERROR_ACCESS' },
};
const store = initStore(
getInitialState({
suite: {
transport: { type: 'WebUsbTransport' },
},
device: {
selectedDevice: { type: 'unreadable', error: 'LIBUSB_ERROR_ACCESS' },
},
device,
}),
);
const { unmount } = renderWithProviders(store, <Index app={store.getState().router.app} />);
@@ -226,14 +232,16 @@ describe('Preloader component', () => {
it('Unreadable device: missing udev on Linux', () => {
jest.spyOn(envUtils, 'isLinux').mockImplementation(() => true);
const device: DeepPartial<AppState['device']> = {
selectedDevice: { type: 'unreadable', error: 'LIBUSB_ERROR_ACCESS' },
};
const store = initStore(
getInitialState({
suite: {
transport: { type: 'BridgeTransport' },
},
device: {
selectedDevice: { type: 'unreadable', error: 'LIBUSB_ERROR_ACCESS' },
},
device,
}),
);
const { unmount } = renderWithProviders(store, <Index app={store.getState().router.app} />);
@@ -247,14 +255,16 @@ describe('Preloader component', () => {
it('Unreadable device: missing udev on non-Linux os (should never happen)', () => {
jest.spyOn(envUtils, 'isLinux').mockImplementation(() => false);
const device: DeepPartial<AppState['device']> = {
selectedDevice: { type: 'unreadable', error: 'LIBUSB_ERROR_ACCESS' },
};
const store = initStore(
getInitialState({
suite: {
transport: { type: 'BridgeTransport' },
},
device: {
selectedDevice: { type: 'unreadable', error: 'LIBUSB_ERROR_ACCESS' },
},
device,
}),
);
const { unmount } = renderWithProviders(store, <Index app={store.getState().router.app} />);
@@ -266,14 +276,16 @@ describe('Preloader component', () => {
});
it('Unreadable device: unknown error', () => {
const device: DeepPartial<AppState['device']> = {
selectedDevice: { type: 'unreadable', error: 'Unexpected error' },
};
const store = initStore(
getInitialState({
suite: {
transport: { type: 'BridgeTransport' },
},
device: {
selectedDevice: { type: 'unreadable', error: 'Unexpected error' },
},
device,
}),
);
const { unmount } = renderWithProviders(store, <Index app={store.getState().router.app} />);
@@ -285,14 +297,16 @@ describe('Preloader component', () => {
});
it('Unknown device (should never happen)', () => {
const device: DeepPartial<AppState['device']> = {
selectedDevice: { features: undefined },
};
const store = initStore(
getInitialState({
suite: {
transport: { type: 'BridgeTransport' },
},
device: {
selectedDevice: { features: null },
},
device,
}),
);
const { unmount } = renderWithProviders(store, <Index app={store.getState().router.app} />);
@@ -304,14 +318,16 @@ describe('Preloader component', () => {
});
it('Seedless device', () => {
const device: DeepPartial<AppState['device']> = {
selectedDevice: { mode: 'seedless', features: {} },
};
const store = initStore(
getInitialState({
suite: {
transport: { type: 'BridgeTransport' },
},
device: {
selectedDevice: { mode: 'seedless', features: {} },
},
device,
}),
);
const { unmount } = renderWithProviders(store, <Index app={store.getState().router.app} />);
@@ -324,14 +340,18 @@ describe('Preloader component', () => {
});
it('Recovery mode device', () => {
const device: DeepPartial<AppState['device']> = {
selectedDevice: {
features: { recovery_status: 'Recovery' },
},
};
const store = initStore(
getInitialState({
suite: {
transport: { type: 'BridgeTransport' },
},
device: {
selectedDevice: { features: { recovery_mode: true } },
},
device,
}),
);
const { unmount } = renderWithProviders(store, <Index app={store.getState().router.app} />);
@@ -344,14 +364,16 @@ describe('Preloader component', () => {
});
it('Not initialized device', () => {
const device: DeepPartial<AppState['device']> = {
selectedDevice: { mode: 'initialize', features: {} },
};
const store = initStore(
getInitialState({
suite: {
transport: { type: 'BridgeTransport' },
},
device: {
selectedDevice: { mode: 'initialize', features: {} },
},
device,
}),
);
const { unmount } = renderWithProviders(store, <Index app={store.getState().router.app} />);
@@ -364,14 +386,16 @@ describe('Preloader component', () => {
});
it('Bootloader device with installed firmware', () => {
const device: DeepPartial<AppState['device']> = {
selectedDevice: { mode: 'bootloader', features: { firmware_present: true } },
};
const store = initStore(
getInitialState({
suite: {
transport: { type: 'BridgeTransport' },
},
device: {
selectedDevice: { mode: 'bootloader', features: { firmware_present: true } },
},
device,
}),
);
const { unmount } = renderWithProviders(store, <Index app={store.getState().router.app} />);
@@ -384,14 +408,16 @@ describe('Preloader component', () => {
});
it('Bootloader device without firmware', () => {
const device: DeepPartial<AppState['device']> = {
selectedDevice: { mode: 'bootloader', features: { firmware_present: false } },
};
const store = initStore(
getInitialState({
suite: {
transport: { type: 'BridgeTransport' },
},
device: {
selectedDevice: { mode: 'bootloader', features: { firmware_present: false } },
},
device,
}),
);
const { unmount } = renderWithProviders(store, <Index app={store.getState().router.app} />);
@@ -404,14 +430,16 @@ describe('Preloader component', () => {
});
it('Required FW update device', () => {
const device: DeepPartial<AppState['device']> = {
selectedDevice: { firmware: 'required', features: {} },
};
const store = initStore(
getInitialState({
suite: {
transport: { type: 'BridgeTransport' },
},
device: {
selectedDevice: { firmware: 'required', features: {} },
},
device,
}),
);
const { unmount } = renderWithProviders(store, <Index app={store.getState().router.app} />);

View File

@@ -0,0 +1,16 @@
import { Translation, TroubleshootingTips } from 'src/components/suite';
export const MultiShareBackupInProgress = () => {
return (
<TroubleshootingTips
label={<Translation id="TR_MULTI_SHARE_BACKUP_IN_PROGRESS" />}
items={[
{
key: 'multi-share-backup-in-progress',
heading: <Translation id="TR_MULTI_SHARE_BACKUP_IN_PROGRESS_HEADING" />,
description: <Translation id="TR_MULTI_SHARE_BACKUP_IN_PROGRESS_DESCRIPTION" />,
},
]}
/>
);
};

View File

@@ -24,6 +24,7 @@ import { DeviceNoFirmware } from './DeviceNoFirmware';
import { DeviceUpdateRequired } from './DeviceUpdateRequired';
import { DeviceDisconnectRequired } from './DeviceDisconnectRequired';
import { selectPrerequisite } from 'src/reducers/suite/suiteReducer';
import { MultiShareBackupInProgress } from './MultiShareBackupInProgress';
const Wrapper = styled.div`
display: flex;
@@ -49,7 +50,7 @@ export const PrerequisitesGuide = ({ allowSwitchDevice }: PrerequisitesGuideProp
const isWebUsbTransport = isWebUsb(transport);
const TipComponent = useMemo(
() => () => {
() => (): React.JSX.Element => {
switch (prerequisite) {
case 'transport-bridge':
return <Transport />;
@@ -77,9 +78,11 @@ export const PrerequisitesGuide = ({ allowSwitchDevice }: PrerequisitesGuideProp
return <DeviceNoFirmware />;
case 'firmware-required':
return <DeviceUpdateRequired />;
case 'multi-share-backup-in-progress':
return <MultiShareBackupInProgress />;
default:
return null;
case undefined:
return <></>;
}
},
[prerequisite, isWebUsbTransport, device],

View File

@@ -57,7 +57,7 @@ export const SuiteBanners = () => {
} else if (device?.features?.unfinished_backup) {
banner = <FailedBackup />;
priority = 90;
} else if (device?.features?.needs_backup) {
} else if (device?.features?.backup_availability === 'Required') {
banner = <NoBackup />;
priority = 70;
} else if (device?.connected && device?.features?.safety_checks === 'PromptAlways') {

View File

@@ -11,33 +11,27 @@ import { SwitchDeviceLegacy } from 'src/views/suite/SwitchDevice/SwitchDeviceLeg
import { SwitchDevice } from 'src/views/suite/SwitchDevice/SwitchDevice';
import type { ForegroundAppRoute } from 'src/types/suite';
import { selectSuiteFlags } from 'src/reducers/suite/suiteReducer';
import { FunctionComponent } from 'react';
import { MultiShareBackupModal } from '../ReduxModal/UserContextModal/MultiShareBackupModal/MultiShareBackupModal';
// would not work if defined directly in the switch
const FirmwareType = () => <FirmwareUpdate shouldSwitchFirmwareType />;
const getForegroundApp = (app: ForegroundAppRoute['app'], isViewOnlyModeVisible: boolean) => {
switch (app) {
case 'firmware':
return FirmwareUpdate;
case 'firmware-type':
return FirmwareType;
case 'firmware-custom':
return FirmwareCustom;
case 'bridge':
return InstallBridge;
case 'udev':
return UdevRules;
case 'version':
return Version;
case 'switch-device':
return isViewOnlyModeVisible ? SwitchDevice : SwitchDeviceLegacy;
case 'recovery':
return Recovery;
case 'backup':
return Backup;
default:
return null;
}
const map: Record<ForegroundAppRoute['app'], FunctionComponent<any>> = {
firmware: FirmwareUpdate,
'firmware-type': FirmwareType,
'firmware-custom': FirmwareCustom,
version: Version,
bridge: InstallBridge,
udev: UdevRules,
'switch-device': isViewOnlyModeVisible ? SwitchDevice : SwitchDeviceLegacy,
recovery: Recovery,
backup: Backup,
'create-multi-share-backup': MultiShareBackupModal,
};
return map[app];
};
type ForegroundAppModalProps = {

View File

@@ -0,0 +1,46 @@
import { ReactNode } from 'react';
import styled, { css } from 'styled-components';
import { Card, Icon, IconType } from '@trezor/components';
import { borders, spacingsPx } from '@trezor/theme';
const StyledCard = styled(Card)<{ $isHorizontal: boolean }>`
align-items: center;
gap: ${spacingsPx.md};
${({ $isHorizontal }) =>
$isHorizontal
? css`
flex-direction: row;
`
: css`
text-align: center;
`}
`;
const StyledIcon = styled(Icon)<{ $isCircled: boolean }>`
${({ $isCircled }) =>
$isCircled &&
css`
border: ${borders.widths.large} solid ${({ theme }) => theme.iconDefault};
border-radius: ${borders.radii.full};
padding: ${spacingsPx.xl};
`}
`;
interface BackupInstructionsCardProps {
children: ReactNode;
icon: IconType;
isHorizontal?: boolean;
}
export const BackupInstructionsCard = ({
children,
icon,
isHorizontal,
}: BackupInstructionsCardProps) => (
<StyledCard $isHorizontal={!!isHorizontal}>
<StyledIcon icon={icon} size={32} $isCircled={!isHorizontal} />
{children}
</StyledCard>
);

View File

@@ -0,0 +1,118 @@
import { ReactNode } from 'react';
import styled, { css } from 'styled-components';
import { TranslationKey } from '@suite-common/intl-types';
import { H3, Icon, IconVariant, Paragraph, Row, Text, TextVariant } from '@trezor/components';
import { borders, spacings, spacingsPx, typographyStylesBase } from '@trezor/theme';
import { Translation } from 'src/components/suite';
const Step = styled.div`
display: grid;
align-items: center;
gap: 0 ${spacingsPx.sm};
grid-template: auto auto / min-content auto;
`;
const StepNumber = styled.div<{ $subdued?: boolean; $isDone?: boolean }>`
border: ${borders.widths.large} solid ${({ theme }) => theme.borderElevation2};
border-radius: ${borders.radii.full};
padding: ${spacingsPx.xxs};
background-color: ${({ theme, $isDone }) =>
$isDone === true ? theme.backgroundPrimarySubtleOnElevation1 : undefined};
color: ${({ theme, $subdued }) => ($subdued === true ? theme.textSubdued : undefined)};
`;
const StyledParagraph = styled(Paragraph)`
font-variant-numeric: tabular-nums;
text-align: center;
width: ${typographyStylesBase.highlight.lineHeight}px;
`;
const Line = styled.div`
border-left: ${borders.widths.large} dashed ${({ theme }) => theme.borderElevation2};
place-self: stretch center;
`;
const Body = styled.div<{ $isLast: boolean }>`
display: flex;
flex-direction: column;
gap: ${spacingsPx.md};
${({ $isLast }) =>
$isLast
? css`
/* This is necessary when Line is not rendered and its cell must be empty */
grid-column-start: 2;
grid-row-start: 2;
`
: css`
/* This cannot be done with gap because Line needs to reach the following step */
margin-bottom: ${spacingsPx.xxxl};
`}
`;
export interface BackupInstructionsStepProps {
children: ReactNode;
description?: TranslationKey;
heading: TranslationKey;
isLast: boolean;
stepNumber: number;
time: number;
completeness?: 'todo' | 'done';
}
export const BackupInstructionsStep = ({
children,
description,
heading,
isLast,
stepNumber,
time,
completeness,
}: BackupInstructionsStepProps) => {
const variantMap: Record<'todo' | 'done', TextVariant & IconVariant> = {
todo: 'tertiary',
done: 'primary',
};
const variant = completeness !== undefined ? variantMap[completeness] : undefined;
return (
<Step>
<StepNumber $subdued={completeness === 'todo'} $isDone={completeness === 'done'}>
{completeness === 'done' ? (
<Icon icon="CHECK" variant="primary" size={24} />
) : (
<StyledParagraph typographyStyle="highlight">{stepNumber}</StyledParagraph>
)}
</StepNumber>
<Row gap={spacings.sm}>
<H3>
<Text variant={variant}>
<Translation id={heading} />
</Text>
</H3>
{completeness === 'done' ? null : (
<Row gap={spacings.xxs}>
<Icon icon="TIMER" variant={variant ?? 'tertiary'} size={16} />
<Text typographyStyle="hint" variant={variant ?? 'tertiary'}>
<Translation id="TR_N_MIN" values={{ n: time }} />
</Text>
</Row>
)}
</Row>
{!isLast && <Line />}
<Body $isLast={isLast}>
{description ? (
<Text typographyStyle="hint" variant="tertiary">
<Translation id={description} />
</Text>
) : null}
{children}
</Body>
</Step>
);
};

View File

@@ -0,0 +1,152 @@
import { useState } from 'react';
import { selectDevice } from '@suite-common/wallet-core';
import { Button, ModalProps } from '@trezor/components';
import {
HELP_CENTER_KEEPING_SEED_SAFE_URL,
HELP_CENTER_MULTI_SHARE_BACKUP_URL,
HELP_CENTER_SEED_CARD_URL,
} from '@trezor/urls';
import { useSelector } from 'src/hooks/suite';
import { Modal, Translation } from 'src/components/suite';
import { LearnMoreButton } from 'src/components/suite/LearnMoreButton';
import { MultiShareBackupStep1FirstInfo } from './MultiShareBackupStep1FirstInfo';
import { MultiShareBackupStep2SecondInfo } from './MultiShareBackupStep2SecondInfo';
import TrezorConnect, { PROTO } from '@trezor/connect';
import { MultiShareBackupStep5Done } from './MultiShareBackupStep5Done';
import { isAdditionalShamirBackupInProgress } from '../../../../../../utils/device/isRecoveryInProgress';
import { MultiShareBackupStep3VerifyOwnership } from './MultiShareBackupStep3VerifyOwnership';
import { MultiShareBackupStep4BackupSeed } from './MultiShareBackupStep4BackupSeed';
type Steps = 'first-info' | 'second-info' | 'verify-ownership' | 'backup-seed' | 'done';
export const MultiShareBackupModal = ({ onCancel }: { onCancel: () => void }) => {
const device = useSelector(selectDevice);
const isInBackupMode =
device?.features !== undefined && isAdditionalShamirBackupInProgress(device.features);
const [step, setStep] = useState<Steps>(isInBackupMode ? 'backup-seed' : 'first-info');
const [isChecked1, setIsChecked1] = useState(false);
const [isChecked2, setIsChecked2] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false);
const closeWithCancelOnDevice = () => {
TrezorConnect.cancel('cancel');
onCancel();
};
if (device === undefined) {
return;
}
const getStepConfig = (): Partial<ModalProps> => {
switch (step) {
case 'first-info':
const goToStepNextStep = () => {
setIsSubmitted(true);
if (isChecked1 && isChecked2) {
setStep('second-info');
}
};
return {
heading: <Translation id="TR_CREATE_MULTI_SHARE_BACKUP" />,
children: (
<MultiShareBackupStep1FirstInfo
isChecked1={isChecked1}
isChecked2={isChecked2}
isSubmitted={isSubmitted}
setIsChecked1={setIsChecked1}
setIsChecked2={setIsChecked2}
/>
),
bottomBarComponents: (
<>
<Button onClick={goToStepNextStep}>
<Translation id="TR_CREATE_MULTI_SHARE_BACKUP" />
</Button>
<LearnMoreButton url={HELP_CENTER_SEED_CARD_URL} size="medium" />
</>
),
};
case 'second-info':
const enterBackup = async () => {
setStep('verify-ownership');
const response = await TrezorConnect.recoveryDevice({
type: 'UnlockRepeatedBackup',
input_method: PROTO.RecoveryDeviceInputMethod.Matrix,
enforce_wordlist: true,
device: {
path: device.path,
},
});
if (response.success) {
setStep('backup-seed');
TrezorConnect.backupDevice().then(response => {
if (response.success) {
setStep('done');
} else {
onCancel();
}
});
} else {
onCancel();
}
};
return {
heading: <Translation id="TR_NEXT_UP" />,
children: <MultiShareBackupStep2SecondInfo />,
bottomBarComponents: (
<>
<Button onClick={enterBackup}>
<Translation id="TR_ENTER_EXISTING_BACKUP" />
</Button>
<LearnMoreButton url={HELP_CENTER_MULTI_SHARE_BACKUP_URL} size="medium">
<Translation id="TR_DONT_HAVE_BACKUP" />
</LearnMoreButton>
</>
),
onBackClick: () => setStep('first-info'),
};
case 'verify-ownership':
return {
children: <MultiShareBackupStep3VerifyOwnership />,
heading: <Translation id="TR_CONFIRM_ON_TREZOR" />,
onCancel: closeWithCancelOnDevice,
};
case 'backup-seed':
return {
children: <MultiShareBackupStep4BackupSeed />,
heading: <Translation id="TR_CONFIRM_ON_TREZOR" />,
onCancel: closeWithCancelOnDevice,
};
case 'done':
return {
heading: <Translation id="TR_CREATE_MULTI_SHARE_BACKUP_CREATED" />,
children: <MultiShareBackupStep5Done />,
bottomBarComponents: (
<>
<Button onClick={onCancel}>
<Translation id="TR_GOT_IT_BUTTON" />
</Button>
<LearnMoreButton url={HELP_CENTER_KEEPING_SEED_SAFE_URL} size="medium">
<Translation id="TR_MULTI_SHARE_TIPS_ON_STORING_BACKUP" />
</LearnMoreButton>
</>
),
};
}
};
return <Modal isCancelable onCancel={onCancel} {...getStepConfig()}></Modal>;
};

View File

@@ -0,0 +1,76 @@
import { Dispatch, SetStateAction } from 'react';
import styled from 'styled-components';
import { Checkbox, Image, Paragraph } from '@trezor/components';
import { spacingsPx } from '@trezor/theme';
import { Translation } from 'src/components/suite';
import { Body, Section } from './multiShareModalLayout';
const StyledImage = styled(Image)`
margin-bottom: ${spacingsPx.md};
`;
type MultiShareBackupStep1Props = {
isChecked1: boolean;
isChecked2: boolean;
isSubmitted: boolean;
setIsChecked1: Dispatch<SetStateAction<boolean>>;
setIsChecked2: Dispatch<SetStateAction<boolean>>;
};
export const MultiShareBackupStep1FirstInfo = ({
isChecked1,
isChecked2,
isSubmitted,
setIsChecked1,
setIsChecked2,
}: MultiShareBackupStep1Props) => {
const getCheckboxVariant = (isChecked: boolean) =>
isSubmitted && !isChecked ? 'destructive' : undefined;
const checkboxVariant1 = getCheckboxVariant(isChecked1);
const checkboxVariant2 = getCheckboxVariant(isChecked2);
const toggleCheckbox1 = () => setIsChecked1(prev => !prev);
const toggleCheckbox2 = () => setIsChecked2(prev => !prev);
return (
<>
<StyledImage image="CREATE_SHAMIR_GROUP" />
<Body>
<Section>
<Paragraph typographyStyle="callout">
<Translation id="TR_MULTI_SHARE_BACKUP_CALLOUT_1" />
</Paragraph>
<Translation id="TR_MULTI_SHARE_BACKUP_EXPLANATION_1" />
</Section>
<Section>
<Paragraph typographyStyle="callout">
<Translation id="TR_MULTI_SHARE_BACKUP_CALLOUT_2" />
</Paragraph>
<Translation id="TR_MULTI_SHARE_BACKUP_EXPLANATION_2" />
</Section>
<Section>
<Paragraph typographyStyle="callout">
<Translation id="TR_MULTI_SHARE_BACKUP_CALLOUT_3" />
</Paragraph>
<Checkbox
isChecked={isChecked1}
onClick={toggleCheckbox1}
variant={checkboxVariant1}
>
<Translation id="TR_MULTI_SHARE_BACKUP_CHECKBOX_1" />
</Checkbox>
<Checkbox
isChecked={isChecked2}
onClick={toggleCheckbox2}
variant={checkboxVariant2}
>
<Translation id="TR_MULTI_SHARE_BACKUP_CHECKBOX_2" />
</Checkbox>
</Section>
</Body>
</>
);
};

View File

@@ -0,0 +1,22 @@
import { BackupInstructionsStep } from './BackupInstructionsStep';
import {
InstructionBaseConfig,
createSharesInstruction,
verifyTrezorOwnershipInstruction,
} from './instructionSteps';
export const MultiShareBackupStep2SecondInfo = () => {
const instructions: InstructionBaseConfig[] = [
verifyTrezorOwnershipInstruction,
createSharesInstruction,
];
return instructions.map((content, i) => (
<BackupInstructionsStep
key={i}
stepNumber={i + 1}
isLast={instructions.length === i + 1}
{...content}
/>
));
};

View File

@@ -0,0 +1,27 @@
import { BackupInstructionsStep } from './BackupInstructionsStep';
import {
InstructionBaseConfig,
createSharesInstruction,
verifyTrezorOwnershipInstruction,
} from './instructionSteps';
export const MultiShareBackupStep3VerifyOwnership = () => {
const instructions: InstructionBaseConfig[] = [
verifyTrezorOwnershipInstruction,
{
...createSharesInstruction,
description: undefined,
children: null,
completeness: 'todo',
},
];
return instructions.map((content, i) => (
<BackupInstructionsStep
key={i}
stepNumber={i + 1}
isLast={instructions.length === i + 1}
{...content}
/>
));
};

View File

@@ -0,0 +1,27 @@
import { BackupInstructionsStep } from './BackupInstructionsStep';
import {
InstructionBaseConfig,
createSharesInstruction,
verifyTrezorOwnershipInstruction,
} from './instructionSteps';
export const MultiShareBackupStep4BackupSeed = () => {
const instructions: InstructionBaseConfig[] = [
{
...verifyTrezorOwnershipInstruction,
description: undefined,
children: null,
completeness: 'done',
},
createSharesInstruction,
];
return instructions.map((content, i) => (
<BackupInstructionsStep
key={i}
stepNumber={i + 1}
isLast={instructions.length === i + 1}
{...content}
/>
));
};

View File

@@ -0,0 +1,135 @@
import { Card, Row, Icon, Column, Text } from '@trezor/components';
import { Translation } from 'src/components/suite';
import { Body, Section } from './multiShareModalLayout';
import { borders, spacings, spacingsPx } from '@trezor/theme';
import { ReactNode } from 'react';
import { TranslationKey } from '@suite-common/intl-types';
import styled from 'styled-components';
const GradientCallout = styled.div`
background-image: linear-gradient(
to right,
${({ theme }) => theme.backgroundPrimarySubtleOnElevation1},
${({ theme }) => theme.backgroundAlertYellowSubtleOnElevation1}
);
border-radius: ${borders.radii.xxs};
width: 100%;
`;
const GradientCalloutCard = styled.div`
flex: 1;
padding: ${spacingsPx.lg} ${spacingsPx.md};
`;
const IconQuestionMarkWrapper = styled.div`
position: relative;
margin-bottom: ${spacingsPx.xxxl};
`;
const IconQuestionMark = styled(Icon)`
position: absolute;
top: -7px;
left: 25px;
`;
const TextDiv = styled(Text)`
display: block;
`;
const Callout = ({ items, header }: { items: ReactNode[]; header: TranslationKey }) => (
<Card>
<Column alignItems="start" gap={spacings.xs}>
<Text typographyStyle="highlight">
<Translation id={header} />
</Text>
{items.map((item, i) => (
<Row key={i} alignItems="start" gap={spacings.xs}>
{item}
</Row>
))}
</Column>
</Card>
);
export const MultiShareBackupStep5Done = () => (
<Body>
<Section>
<Text typographyStyle="callout" variant="primary">
<Translation id="TR_MULTI_SHARE_BACKUP_GREAT" />
</Text>
<Translation id="TR_CREATE_MULTI_SHARE_BACKUP_CREATED_INFO_TEXT" />
</Section>
<Section>
<Text typographyStyle="callout">
<Translation id="TR_MULTI_SHARE_BACKUP_BACKUPS" />
</Text>
<Row gap={spacings.lg} alignItems="stretch">
<Callout
header="TR_MULTI_SHARE_BACKUP_SUCCESS_LEFT_HEADER"
items={[
<>
<Icon icon="COINS" />
<Translation id="TR_MULTI_SHARE_BACKUP_SUCCESS_LEFT_LINE1" />
</>,
<>
<Icon icon="EYE_SLASH" />
<Translation id="TR_MULTI_SHARE_BACKUP_SUCCESS_LEFT_LINE2" />
</>,
]}
/>
<Callout
header="TR_MULTI_SHARE_BACKUP_SUCCESS_RIGHT_HEADER"
items={[
<>
<Icon icon="COINS" />
<Translation id="TR_MULTI_SHARE_BACKUP_SUCCESS_RIGHT_LINE1" />
</>,
<>
<Icon icon="EYE_SLASH" />
<Translation id="TR_MULTI_SHARE_BACKUP_SUCCESS_RIGHT_LINE2" />
</>,
]}
/>
</Row>
</Section>
<Section>
<Text typographyStyle="callout">
<Translation id="TR_MULTI_SHARE_BACKUP_SUCCESS_WHY_IS_BACKUP_IMPORTANT" />
</Text>
<GradientCallout>
<Row gap={spacings.lg} alignItems="stretch">
<GradientCalloutCard>
<IconQuestionMarkWrapper>
<Icon icon="TREZOR_T2T1" size={40} />
<IconQuestionMark icon="QUESTION_FILLED" size={24} variant="primary" />
</IconQuestionMarkWrapper>
<TextDiv variant="primary">
<Translation id="TR_MULTI_SHARE_BACKUP_LOST_YOUR_TREZOR" />
</TextDiv>
<TextDiv color="subdued">
<Translation id="TR_MULTI_SHARE_BACKUP_LOST_YOUR_TREZOR_INFO_TEXT" />
</TextDiv>
</GradientCalloutCard>
<GradientCalloutCard>
<IconQuestionMarkWrapper>
<Icon icon="BACKUP_2" size={40} />
<IconQuestionMark icon="QUESTION_FILLED" size={24} variant="warning" />
</IconQuestionMarkWrapper>
<TextDiv variant="warning">
<Translation id="TR_MULTI_SHARE_BACKUP_LOST_YOUR_TREZOR" />
</TextDiv>
<TextDiv color="subdued">
<Translation id="TR_MULTI_SHARE_BACKUP_LOST_YOUR_BACKUP_INFO_TEXT" />
</TextDiv>
</GradientCalloutCard>
</Row>
</GradientCallout>
</Section>
</Body>
);

View File

@@ -0,0 +1,87 @@
import { Row } from '@trezor/components';
import styled from 'styled-components';
import { Image, Text } from '@trezor/components';
import { borders, spacings, spacingsPx } from '@trezor/theme';
import { ESHOP_KEEP_METAL_URL, HELP_CENTER_SEED_CARD_URL } from '@trezor/urls';
import { Translation, TrezorLink } from 'src/components/suite';
import { BackupInstructionsCard } from './BackupInstructionsCard';
import { BackupInstructionsStepProps } from './BackupInstructionsStep';
const CardWrapper = styled.div`
display: grid;
gap: ${spacingsPx.sm};
grid-template-columns: repeat(3, 1fr);
`;
const Illustration = styled.div`
display: flex;
align-items: center;
flex-direction: column;
border: ${borders.widths.small} solid ${({ theme }) => theme.borderElevation0};
border-radius: ${borders.radii.md};
padding-bottom: ${spacingsPx.lg};
`;
export type InstructionBaseConfig = Pick<
BackupInstructionsStepProps,
'children' | 'description' | 'heading' | 'time' | 'completeness'
>;
export const verifyTrezorOwnershipInstruction: InstructionBaseConfig = {
heading: 'TR_VERIFY_TREZOR_OWNERSHIP',
time: 2,
description: 'TR_VERIFY_TREZOR_OWNERSHIP_EXPLANATION',
children: (
<Row gap={spacings.sm}>
<BackupInstructionsCard isHorizontal icon="BACKUP_2">
<Translation id="TR_VERIFY_TREZOR_OWNERSHIP_CARD_1" />
</BackupInstructionsCard>
<BackupInstructionsCard isHorizontal icon="CAMERA_SLASH">
<Translation id="TR_VERIFY_TREZOR_OWNERSHIP_CARD_2" />
</BackupInstructionsCard>
</Row>
),
};
export const createSharesInstruction: InstructionBaseConfig = {
heading: 'TR_CREATE_SHARES',
time: 10,
description: 'TR_CREATE_SHARES_EXPLANATION',
children: (
<>
<Illustration>
<Image image="SHAMIR_SHARES" />
<Text typographyStyle="hint" variant="tertiary">
<Translation id="TR_CREATE_SHARES_EXAMPLE" />
</Text>
</Illustration>
<CardWrapper>
<BackupInstructionsCard icon="PENCIL">
<Translation
id="TR_CREATE_SHARES_CARD_1"
values={{
cardsLink: chunks => (
<TrezorLink href={HELP_CENTER_SEED_CARD_URL} variant="underline">
{chunks}
</TrezorLink>
),
keepLink: chunks => (
<TrezorLink href={ESHOP_KEEP_METAL_URL} variant="underline">
{chunks}
</TrezorLink>
),
}}
/>
</BackupInstructionsCard>
<BackupInstructionsCard icon="CAMERA_SLASH">
<Translation id="TR_CREATE_SHARES_CARD_2" />
</BackupInstructionsCard>
<BackupInstructionsCard icon="EYE_SLASH">
<Translation id="TR_CREATE_SHARES_CARD_3" />
</BackupInstructionsCard>
</CardWrapper>
</>
),
};

View File

@@ -0,0 +1,15 @@
import { Column } from '@trezor/components';
import { spacings } from '@trezor/theme';
import { ReactNode } from 'react';
export const Body = ({ children }: { children: ReactNode }) => (
<Column gap={spacings.lg} alignItems="start">
{children}
</Column>
);
export const Section = ({ children }: { children: ReactNode }) => (
<Column gap={spacings.xs} alignItems="start" flex={1}>
{children}
</Column>
);

View File

@@ -46,3 +46,4 @@ export { StakeEthInANutshellModal } from './ReduxModal/UserContextModal/StakeEth
export { StakeModal } from './ReduxModal/UserContextModal/StakeModal/StakeModal';
export { UnstakeModal } from './ReduxModal/UserContextModal/UnstakeModal/UnstakeModal';
export { ClaimModal } from './ReduxModal/UserContextModal/ClaimModal/ClaimModal';
export { MultiShareBackupModal } from './ReduxModal/UserContextModal/MultiShareBackupModal/MultiShareBackupModal';

View File

@@ -8,22 +8,26 @@ import { ModalAppParams } from 'src/utils/suite/router';
const isForegroundApp = (route: Route): route is ForegroundAppRoute =>
!route.isFullscreenApp && !!route.isForegroundApp;
// Firmware, FirmwareCustom, Bridge, Udev, Version - always beats redux modals
// Backup, SwitchDevice - always get beaten by redux modals
// Recovery - beats redux modals with some exceptions (raw-rendered)
const hasPriority = (route: ForegroundAppRoute) => {
switch (route.app) {
case 'bridge':
case 'firmware':
case 'firmware-type':
case 'firmware-custom':
case 'recovery':
case 'udev':
case 'version':
return true;
default:
return false;
}
const map: Record<ForegroundAppRoute['app'], boolean> = {
// Firmware, FirmwareCustom, Bridge, Udev, Version, Create New Multi-share Backup - always beats redux modals
firmware: true,
'firmware-type': true,
'firmware-custom': true,
bridge: true,
udev: true,
version: true,
'create-multi-share-backup': true,
// Recovery - beats redux modals with some exceptions (raw-rendered)
recovery: true,
// Backup, SwitchDevice - always get beaten by redux modals
'switch-device': false,
backup: false,
};
return map[route.app];
};
const getForegroundAppAction = (route: ForegroundAppRoute, params: Partial<ModalAppParams>) =>

View File

@@ -7,6 +7,7 @@ import { SUITE } from 'src/actions/suite/constants';
import * as recoveryActions from 'src/actions/recovery/recoveryActions';
import * as onboardingActions from 'src/actions/onboarding/onboardingActions';
import { AppState, Action, Dispatch } from 'src/types/suite';
import { isRecoveryInProgress } from '../../utils/device/isRecoveryInProgress';
const recovery =
(api: MiddlewareAPI<Dispatch, AppState>) =>
@@ -31,7 +32,8 @@ const recovery =
if (
deviceActions.updateSelectedDevice.match(action) &&
action.payload?.features?.recovery_mode &&
action.payload?.features !== undefined &&
isRecoveryInProgress(action.payload?.features) &&
recovery.status !== 'in-progress'
) {
api.dispatch(
@@ -41,7 +43,7 @@ const recovery =
}),
);
if (!analytics.confirmed) {
// If you connect T2T1 in recovery mode to fresh Suite, you should see analytics optout option first.
// If you connect T2T1 in recovery mode to fresh Suite, you should see analytics opt-out option first.
api.dispatch(recoveryActions.setStatus('in-progress'));
} else {
api.dispatch(recoveryActions.rerun());

View File

@@ -2100,7 +2100,7 @@ export default defineMessages({
},
TR_BACKUP_SUBHEADING_1: {
defaultMessage:
"A wallet backup is a series of randomly generated words created by your Trezor. Its important to write down your wallet backup and keep it safe, as it's the only way to recover and access your funds.",
"A wallet backup is a series of randomly generated words created by your Trezor. It's important to write down your wallet backup and keep it safe, as it's the only way to recover and access your funds.",
description: 'Explanation what recovery seed is',
id: 'TR_BACKUP_SUBHEADING_1',
},
@@ -2123,6 +2123,189 @@ export default defineMessages({
description: 'Enter words on your computer, recovery takes about 2 minutes.',
id: 'TR_BASIC_RECOVERY_OPTION',
},
TR_MULTI_SHARE_BACKUP: {
defaultMessage: 'Multi-share Backup',
id: 'TR_MULTI_SHARE_BACKUP',
},
TR_MULTI_SHARE_BACKUP_DESCRIPTION: {
defaultMessage:
'Generate multiple shares, each containing 20 words. These shares are used collectively to recover your funds.',
id: 'TR_MULTI_SHARE_BACKUP_DESCRIPTION',
},
TR_MULTI_SHARE_BACKUP_IN_PROGRESS: {
defaultMessage: 'Multi-share Backup creation in progress',
id: 'TR_MULTI_SHARE_BACKUP_IN_PROGRESS',
},
TR_MULTI_SHARE_BACKUP_IN_PROGRESS_HEADING: {
defaultMessage: 'Please continue creating shares on your Trezor',
id: 'TR_MULTI_SHARE_BACKUP_IN_PROGRESS_HEADING',
},
TR_MULTI_SHARE_BACKUP_IN_PROGRESS_DESCRIPTION: {
defaultMessage:
'After successfully creating a Multi-share Backup, you will be able to continue using your device with Trezor Suite.',
id: 'TR_MULTI_SHARE_BACKUP_IN_PROGRESS_DESCRIPTION',
},
TR_CREATE_MULTI_SHARE_BACKUP: {
defaultMessage: 'Create multi-share backup',
id: 'TR_CREATE_MULTI_SHARE_BACKUP',
},
TR_MULTI_SHARE_BACKUP_CALLOUT_1: {
defaultMessage: 'What does it do?',
id: 'TR_MULTI_SHARE_BACKUP_CALLOUT_1',
},
TR_MULTI_SHARE_BACKUP_CALLOUT_2: {
defaultMessage: 'What about my current backup?',
id: 'TR_MULTI_SHARE_BACKUP_CALLOUT_2',
},
TR_MULTI_SHARE_BACKUP_CALLOUT_3: {
defaultMessage: 'Please note',
id: 'TR_MULTI_SHARE_BACKUP_CALLOUT_3',
},
TR_MULTI_SHARE_BACKUP_EXPLANATION_1: {
defaultMessage:
'Multi backup creates multiple 20 word shares that will be needed for recovering your Trezor. You can distribute these among your family, friends, or hide them in different places. If the time comes to recover your Trezor, multiple shares must be used collectively to regain access to your funds.',
id: 'TR_MULTI_SHARE_BACKUP_EXPLANATION_1',
},
TR_MULTI_SHARE_BACKUP_EXPLANATION_2: {
defaultMessage:
'You existing backup will still allow you to recover your funds. You should hide it well, ideally away from the new backup shares.',
id: 'TR_MULTI_SHARE_BACKUP_EXPLANATION_2',
},
TR_MULTI_SHARE_BACKUP_CHECKBOX_1: {
defaultMessage: 'This is an advanced feature, and I understand the extra responsibility',
id: 'TR_MULTI_SHARE_BACKUP_CHECKBOX_1',
},
TR_MULTI_SHARE_BACKUP_CHECKBOX_2: {
defaultMessage: 'My current backup will still be able to recover my wallet',
id: 'TR_MULTI_SHARE_BACKUP_CHECKBOX_2',
},
TR_MULTI_SHARE_TIPS_ON_STORING_BACKUP: {
defaultMessage: 'Tips on storing backup',
id: 'TR_MULTI_SHARE_TIPS_ON_STORING_BACKUP',
},
TR_CREATE_MULTI_SHARE_BACKUP_CREATED: {
defaultMessage: 'Multi-share Backup created',
id: 'TR_CREATE_MULTI_SHARE_BACKUP_CREATED',
},
TR_MULTI_SHARE_BACKUP_GREAT: {
defaultMessage: 'Great!',
id: 'TR_MULTI_SHARE_BACKUP_GREAT',
},
TR_CREATE_MULTI_SHARE_BACKUP_CREATED_INFO_TEXT: {
defaultMessage:
'Youve done a huge step for improving your security. Dont forget to hide and distribute your backup well.',
id: 'TR_CREATE_MULTI_SHARE_BACKUP_CREATED_INFO_TEXT',
},
TR_MULTI_SHARE_BACKUP_BACKUPS: {
defaultMessage: 'Backups',
id: 'TR_MULTI_SHARE_BACKUP_BACKUPS',
},
TR_MULTI_SHARE_BACKUP_SUCCESS_LEFT_HEADER: {
defaultMessage: 'My previous backup',
id: 'TR_MULTI_SHARE_BACKUP_SUCCESS_LEFT',
},
TR_MULTI_SHARE_BACKUP_SUCCESS_LEFT_LINE1: {
defaultMessage: 'Still recovers you funds',
id: 'TR_MULTI_SHARE_BACKUP_SUCCESS_LEFT_LINE1',
},
TR_MULTI_SHARE_BACKUP_SUCCESS_LEFT_LINE2: {
defaultMessage: 'Hide it well',
id: 'TR_MULTI_SHARE_BACKUP_SUCCESS_LEFT_LINE2',
},
TR_MULTI_SHARE_BACKUP_SUCCESS_RIGHT_HEADER: {
defaultMessage: 'My new Multi-share Backup',
id: 'TR_MULTI_SHARE_BACKUP_SUCCESS_RIGHT',
},
TR_MULTI_SHARE_BACKUP_SUCCESS_RIGHT_LINE1: {
defaultMessage: 'Collect the number of shares set as the threshold to recover your funds',
id: 'TR_MULTI_SHARE_BACKUP_SUCCESS_RIGHT_LINE1',
},
TR_MULTI_SHARE_BACKUP_SUCCESS_RIGHT_LINE2: {
defaultMessage: 'Hide them well. Can be different places, different people',
id: 'TR_MULTI_SHARE_BACKUP_SUCCESS_RIGHT_LINE2',
},
TR_MULTI_SHARE_BACKUP_SUCCESS_WHY_IS_BACKUP_IMPORTANT: {
defaultMessage: 'Why is backup important',
id: 'TR_MULTI_SHARE_BACKUP_SUCCESS_WHY_IS_BACKUP_IMPORTANT',
},
TR_MULTI_SHARE_BACKUP_LOST_YOUR_TREZOR: {
defaultMessage: 'Lost your Trezor?',
id: 'TR_MULTI_SHARE_BACKUP_LOST_YOUR_TREZOR',
},
TR_MULTI_SHARE_BACKUP_LOST_YOUR_TREZOR_INFO_TEXT: {
defaultMessage: 'Not a problem, recover your coins with Backup!',
id: 'TR_MULTI_SHARE_BACKUP_LOST_YOUR_TREZOR_INFO_TEXT',
},
TR_MULTI_SHARE_BACKUP_LOST_YOUR_BACKUP: {
defaultMessage: 'Lost your Backup?',
id: 'TR_MULTI_SHARE_BACKUP_LOST_YOUR_BACKUP',
},
TR_MULTI_SHARE_BACKUP_LOST_YOUR_BACKUP_INFO_TEXT: {
defaultMessage:
'That could be very bad if you also lost your Trezor. Contact support if that happens.',
id: 'TR_MULTI_SHARE_BACKUP_LOST_YOUR_BACKUP_INFO_TEXT',
},
TR_NEXT_UP: {
defaultMessage: 'Next up',
id: 'TR_NEXT_UP',
},
TR_N_MIN: {
defaultMessage: '{n} min',
id: 'TR_N_MIN',
},
TR_VERIFY_TREZOR_OWNERSHIP: {
defaultMessage: 'Verify Trezor ownership',
id: 'TR_VERIFY_TREZOR_OWNERSHIP',
},
TR_VERIFY_TREZOR_OWNERSHIP_EXPLANATION: {
defaultMessage:
'We need you to prove you own this wallet by entering your existing wallet backup.',
id: 'TR_VERIFY_TREZOR_OWNERSHIP_EXPLANATION',
},
TR_VERIFY_TREZOR_OWNERSHIP_CARD_1: {
defaultMessage: 'Grab your existing wallet backup',
id: 'TR_VERIFY_TREZOR_OWNERSHIP_CARD_1',
},
TR_VERIFY_TREZOR_OWNERSHIP_CARD_2: {
defaultMessage: "Don't take photos, or digital copies of backup",
id: 'TR_VERIFY_TREZOR_OWNERSHIP_CARD_2',
},
TR_CREATE_SHARES: {
defaultMessage: 'Create shares on Trezor',
id: 'TR_CREATE_SHARES',
},
TR_CREATE_SHARES_EXPLANATION: {
defaultMessage:
"Now, you'll be selecting amount of shares, and minimum of shares required to recover your Trezor.",
id: 'TR_CREATE_SHARES_EXPLANATION',
},
TR_CREATE_SHARES_EXAMPLE: {
defaultMessage: 'e.g. 5 shares total, at least any 3 for recovery',
id: 'TR_CREATE_SHARES_EXAMPLE',
},
TR_CREATE_SHARES_CARD_1: {
defaultMessage:
'Grab pen and paper. Or print <cardsLink>Trezor cards</cardsLink>, or use <keepLink>Trezor Keep</keepLink>',
id: 'TR_CREATE_SHARES_CARD_1',
},
TR_CREATE_SHARES_CARD_2: {
defaultMessage: "Don't take photos, or digital copies of backup",
id: 'TR_CREATE_SHARES_CARD_2',
},
TR_CREATE_SHARES_CARD_3: {
defaultMessage: "Make sure it's just you, no curious onlookers",
id: 'TR_CREATE_SHARES_CARD_3',
},
TR_ENTER_EXISTING_BACKUP: {
defaultMessage: 'Enter existing backup on Trezor',
id: 'TR_ENTER_EXISTING_BACKUP',
},
TR_DONT_HAVE_BACKUP: {
defaultMessage: "I don't have a backup",
id: 'TR_DONT_HAVE_BACKUP',
},
TR_BCH_ADDRESS_INFO: {
defaultMessage:
'Bitcoin Cash changed the address format to cashaddr. Find more info about how to convert your address on our blog. {TR_LEARN_MORE}',
@@ -8969,8 +9152,8 @@ export default defineMessages({
id: 'TR_VIEW_ONLY_TOOLTIP_CHANGE_INFO',
defaultMessage: 'You can change it here',
},
TR_VIEW_ONLY_TOOLTIP_BUTTON: {
id: 'TR_VIEW_ONLY_TOOLTIP_BUTTON',
TR_GOT_IT_BUTTON: {
id: 'TR_GOT_IT_BUTTON',
defaultMessage: 'Got it',
},
TR_VIEW_ONLY_ENABLED: {

View File

@@ -0,0 +1,27 @@
import { PROTO } from '@trezor/connect';
export const isAdditionalShamirBackupInProgress = (features: PROTO.Features) =>
features.recovery_status === 'Backup' &&
features.recovery_type === undefined &&
features.backup_availability === 'Available';
export const isRecoveryInProgress = (features: PROTO.Features) => {
const { recovery_status, backup_availability } = features;
if (recovery_status === undefined) {
return false;
}
if (recovery_status === 'Recovery') {
return true;
}
const isShamirAdditionalBackupRecovery =
recovery_status === 'Backup' && backup_availability === 'NotAvailable';
if (isShamirAdditionalBackupRecovery) {
return true;
}
return false;
};

View File

@@ -2,6 +2,10 @@ import type { TransportInfo } from '@trezor/connect';
import { DefinedUnionMember } from '@trezor/type-utils';
import { RouterState } from 'src/reducers/suite/routerReducer';
import type { TrezorDevice, AppState } from 'src/types/suite';
import {
isAdditionalShamirBackupInProgress,
isRecoveryInProgress,
} from '../device/isRecoveryInProgress';
type GetPrerequisiteNameParams = {
router: AppState['router'];
@@ -38,7 +42,13 @@ export const getPrerequisiteName = ({ router, device, transport }: GetPrerequisi
// similar to initialize, there is no seed in device
// difference is it is in recovery mode.
// todo: this could be added to @trezor/connect to device.mode I think.
if (device.features.recovery_mode) return 'device-recovery-mode';
if (isRecoveryInProgress(device.features)) {
return 'device-recovery-mode';
}
if (isAdditionalShamirBackupInProgress(device.features)) {
return 'multi-share-backup-in-progress';
}
// device is not initialized
// todo: should not happen and redirect to onboarding instead?
@@ -66,6 +76,7 @@ export const getExcludedPrerequisites = (router: RouterState): PrerequisiteType[
'device-bootloader',
'firmware-missing',
'firmware-required',
'multi-share-backup-in-progress',
];
}

View File

@@ -1,7 +1,7 @@
import styled from 'styled-components';
import { Paragraph, Button, Image, Row } from '@trezor/components';
import { HELP_CENTER_FAILED_BACKUP_URL } from '@trezor/urls';
import { HELP_CENTER_RECOVERY_ISSUES_URL } from '@trezor/urls';
import { selectDevice } from '@suite-common/wallet-core';
import { useDispatch, useSelector } from 'src/hooks/suite';
@@ -112,7 +112,7 @@ export const Backup = ({ cancelable, onCancel }: ForegroundAppProps) => {
if (
backup.status !== 'finished' &&
!backup.error &&
device.features.needs_backup === false &&
device.features.backup_availability !== 'Required' &&
device.features.unfinished_backup !== null
) {
return (
@@ -127,7 +127,7 @@ export const Backup = ({ cancelable, onCancel }: ForegroundAppProps) => {
<StyledImage image="UNI_ERROR" />
<StyledP data-test="@backup/already-failed-message">
<Translation id="BACKUP_BACKUP_ALREADY_FAILED_DESCRIPTION" />
<TrezorLink icon="EXTERNAL_LINK" href={HELP_CENTER_FAILED_BACKUP_URL}>
<TrezorLink icon="EXTERNAL_LINK" href={HELP_CENTER_RECOVERY_ISSUES_URL}>
<Translation id="TR_LEARN_MORE" />
</TrezorLink>
</StyledP>

View File

@@ -51,7 +51,8 @@ const SecurityFeatures = () => {
if (device && device.features) {
// TODO: add "error - backup failed" instead of needsBackup
// TODO: add "enable passphrase" instead of hiddenWalletCreated
needsBackup = device.features.needs_backup || device.features.unfinished_backup;
needsBackup =
device.features.backup_availability === 'Required' || device.features.unfinished_backup;
pinEnabled = device.features.pin_protection;
hiddenWalletCreated = device.features.passphrase_protection;
backupFailed = device.features.unfinished_backup;

View File

@@ -1,4 +1,4 @@
import { HELP_CENTER_FAILED_BACKUP_URL } from '@trezor/urls';
import { HELP_CENTER_RECOVERY_ISSUES_URL } from '@trezor/urls';
import { SettingsSectionItem } from 'src/components/settings';
import { ActionButton, ActionColumn, TextColumn, Translation } from 'src/components/suite';
@@ -10,7 +10,7 @@ export const BackupFailed = () => {
<TextColumn
title={<Translation id="TR_BACKUP_RECOVERY_SEED_FAILED_TITLE" />}
description={<Translation id="TR_BACKUP_RECOVERY_SEED_FAILED_DESC" />}
buttonLink={HELP_CENTER_FAILED_BACKUP_URL}
buttonLink={HELP_CENTER_RECOVERY_ISSUES_URL}
/>
<ActionColumn>
<ActionButton isDisabled>

View File

@@ -14,7 +14,7 @@ export const BackupRecoverySeed = ({ isDeviceLocked }: BackupRecoverySeedProps)
const dispatch = useDispatch();
const { device } = useDevice();
const needsBackup = !!device?.features?.needs_backup;
const needsBackup = device?.features?.backup_availability === 'Required';
const handleClick = () => dispatch(goto('backup-index', { params: { cancelable: true } }));

View File

@@ -14,7 +14,7 @@ export const CheckRecoverySeed = ({ isDeviceLocked }: CheckRecoverySeedProps) =>
const dispatch = useDispatch();
const { device } = useDevice();
const needsBackup = !!device?.features?.needs_backup;
const needsBackup = device?.features?.backup_availability === 'Required';
const learnMoreUrl = getCheckBackupUrl(device);
const handleClick = () => dispatch(goto('recovery-index', { params: { cancelable: true } }));

View File

@@ -0,0 +1,61 @@
import { HELP_CENTER_SEED_CARD_URL } from '@trezor/urls';
import {
ActionButton,
ActionColumn,
SectionItem,
TextColumn,
Translation,
} from 'src/components/suite';
import { useDispatch, useSelector } from 'src/hooks/suite';
import { selectDevice } from '@suite-common/wallet-core';
import { TrezorDevice } from '@suite-common/suite-types';
import { goto } from '../../../actions/suite/routerActions';
const doesSupportMultiShare = (device: TrezorDevice | undefined): boolean => {
if (device?.features === undefined) {
return false;
}
if (!device.features.capabilities?.includes('Capability_Shamir')) {
return false;
}
return (
device.features.backup_type !== null &&
[
'Slip39_Single_Extendable',
'Slip39_Basic_Extendable',
'Slip39_Advanced_Extendable',
].includes(device.features.backup_type)
);
};
export const MultiShareBackup = () => {
const device = useSelector(selectDevice);
const dispatch = useDispatch();
if (!doesSupportMultiShare(device)) {
return;
}
const handleClick = () => dispatch(goto('create-multi-share-backup'));
return (
<SectionItem>
<TextColumn
title={<Translation id="TR_MULTI_SHARE_BACKUP" />}
description={<Translation id="TR_MULTI_SHARE_BACKUP_DESCRIPTION" />}
buttonLink={HELP_CENTER_SEED_CARD_URL}
/>
<ActionColumn>
<ActionButton
variant="secondary"
data-test="@settings/device/create-multi-share-backup-button"
onClick={handleClick}
>
<Translation id="TR_CREATE_MULTI_SHARE_BACKUP" />
</ActionButton>
</ActionColumn>
</SectionItem>
);
};

View File

@@ -20,6 +20,7 @@ import { DisplayRotation } from './DisplayRotation';
import { FirmwareTypeChange } from './FirmwareTypeChange';
import { FirmwareVersion } from './FirmwareVersion';
import { Homescreen } from './Homescreen';
import { MultiShareBackup } from './MultiShareBackup';
import { Passphrase } from './Passphrase';
import { PinProtection } from './PinProtection';
import { SafetyChecks } from './SafetyChecks';
@@ -28,12 +29,14 @@ import { WipeDevice } from './WipeDevice';
import { ChangeLanguage } from './ChangeLanguage';
import { EnableViewOnly } from './EnableViewOnly';
import { selectSuiteFlags } from 'src/reducers/suite/suiteReducer';
import { isRecoveryInProgress } from '../../../utils/device/isRecoveryInProgress';
const deviceSettingsUnavailable = (device?: TrezorDevice, transport?: Partial<TransportInfo>) => {
const noTransportAvailable = transport && !transport.type;
const wrongDeviceType = device?.type && ['unacquired', 'unreadable'].includes(device.type);
const wrongDeviceMode =
(device?.mode && ['seedless'].includes(device.mode)) || device?.features?.recovery_mode;
(device?.mode && ['seedless'].includes(device.mode)) ||
(device?.features !== undefined && isRecoveryInProgress(device?.features));
const firmwareUpdateRequired = device?.firmware === 'required';
return noTransportAvailable || wrongDeviceType || wrongDeviceMode || firmwareUpdateRequired;
@@ -120,6 +123,7 @@ export const SettingsDevice = () => {
) : (
<>
<BackupRecoverySeed isDeviceLocked={isDeviceLocked} />
<MultiShareBackup />
<CheckRecoverySeed isDeviceLocked={isDeviceLocked} />
</>
)}

View File

@@ -52,7 +52,7 @@ export const ViewOnlyTooltip = ({ children }: ViewOnlyTooltipProps) => {
</Text>
</TextContent>
<Button variant="tertiary" onClick={handleClose}>
<Translation id="TR_VIEW_ONLY_TOOLTIP_BUTTON" />
<Translation id="TR_GOT_IT_BUTTON" />
</Button>
</Notification>
}

View File

@@ -56,7 +56,7 @@ export const HELP_CENTER_FW_DOWNGRADE_T2B1_URL =
'https://trezor.io/learn/a/downgrade-firmware-trezor-safe-3';
export const HELP_CENTER_FW_DOWNGRADE_T3T1_URL =
'https://trezor.io/learn/a/downgrade-firmware-trezor-safe-5';
export const HELP_CENTER_FAILED_BACKUP_URL = 'https://trezor.io/support/a/trezor-recovery-issues';
export const HELP_CENTER_RECOVERY_ISSUES_URL = 'https://trezor.io/support/a/trezor-recovery-issues';
export const HELP_CENTER_ADVANCED_RECOVERY_URL =
'https://trezor.io/learn/a/advanced-recovery-on-trezor-model-one';
export const HELP_CENTER_XPUB_URL = 'https://trezor.io/learn/a/trezor-suite-app-public-keys-xpub';
@@ -71,6 +71,11 @@ export const HELP_CENTER_DEVICE_AUTHENTICATION =
'https://trezor.io/learn/a/trezor-safe-3-authentication-check';
export const HELP_CENTER_ETH_STAKING =
'https://trezor.io/learn/a/stake-ethereum-eth-in-trezor-suite';
export const HELP_CENTER_SEED_CARD_URL = 'https://trezor.io/learn/a/recovery-seed-card';
export const HELP_CENTER_MULTI_SHARE_BACKUP_URL =
'https://trezor.io/learn/a/introducing-multi-share-backup';
export const HELP_CENTER_KEEPING_SEED_SAFE_URL =
'https://trezor.io/learn/a/keeping-your-recovery-seed-safe';
export const INVITY_URL = 'https://invity.io/';
export const INVITY_SCHEDULE_OF_FEES = 'https://blog.invity.io/schedule-of-fees';
@@ -95,5 +100,4 @@ export const ZKSNACKS_TERMS_URL =
'https://github.com/zkSNACKs/WalletWasabi/blob/master/WalletWasabi/Legal/Assets/LegalDocumentsWw2.txt';
export const CROWDIN_URL = 'https://crowdin.com/project/trezor-suite';
export const HELP_CENTER_MULTI_SHARE_BACKUP_URL =
'https://trezor.io/learn/a/introducing-multi-share-backup';
export const ESHOP_KEEP_METAL_URL = 'https://trezor.io/trezor-keep-metal';

View File

@@ -113,6 +113,13 @@ export const routes = [
isForegroundApp: true,
params: modalAppParams,
},
{
name: 'create-multi-share-backup',
pattern: '/create-multi-share-backup',
app: 'create-multi-share-backup',
isForegroundApp: true,
params: modalAppParams,
},
{
name: 'wallet-index',
pattern: '/accounts',

View File

@@ -113,7 +113,7 @@ const getDeviceFeatures = (feat?: Partial<Features>): Features => ({
imported: null,
unlocked: true,
firmware_present: null,
needs_backup: false,
backup_availability: 'NotAvailable',
flags: 0,
model: 'T',
internal_model: DeviceModelInternal.T2T1,
@@ -123,7 +123,7 @@ const getDeviceFeatures = (feat?: Partial<Features>): Features => ({
fw_vendor: null,
unfinished_backup: false,
no_backup: false,
recovery_mode: false,
recovery_status: 'Nothing',
capabilities: [],
backup_type: 'Bip39',
sd_card_present: false,

View File

@@ -35,7 +35,7 @@ export const portfolioTrackerDevice: TrezorDevice = {
imported: null,
unlocked: true,
firmware_present: null,
needs_backup: false,
backup_availability: 'NotAvailable',
flags: 0,
model: 'T',
internal_model: DeviceModelInternal.T2T1,
@@ -45,7 +45,7 @@ export const portfolioTrackerDevice: TrezorDevice = {
fw_vendor: null,
unfinished_backup: false,
no_backup: false,
recovery_mode: false,
recovery_status: 'Nothing',
capabilities: [],
backup_type: 'Bip39',
sd_card_present: false,