diff --git a/Cargo.lock b/Cargo.lock index 78ff785f915144..035ce19bdc415b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -799,6 +799,12 @@ version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" +[[package]] +name = "bech32" +version = "0.10.0-beta" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98f7eed2b2781a6f0b5c903471d48e15f56fb4e1165df8a9a2337fd1a59d45ea" + [[package]] name = "bincode" version = "1.3.3" @@ -843,6 +849,36 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bitcoin" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd00f3c09b5f21fb357abe32d29946eb8bb7a0862bae62c0b5e4a692acbbe73c" +dependencies = [ + "bech32", + "bitcoin-internals", + "bitcoin_hashes", + "hex-conservative", + "hex_lit", + "secp256k1", +] + +[[package]] +name = "bitcoin-internals" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb" + +[[package]] +name = "bitcoin_hashes" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b" +dependencies = [ + "bitcoin-internals", + "hex-conservative", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -2627,6 +2663,18 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-conservative" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ed443af458ccb6d81c1e7e661545f94d3176752fb1df2f543b902a1e0f51e2" + +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + [[package]] name = "hidapi" version = "2.6.1" @@ -3233,6 +3281,18 @@ dependencies = [ "libsecp256k1-core", ] +[[package]] +name = "libusb1-sys" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d0e2afce4245f2c9a418511e5af8718bcaf2fa408aefb259504d1a9cb25f27" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "libz-sys" version = "1.1.3" @@ -4260,6 +4320,17 @@ dependencies = [ "tonic-build", ] +[[package]] +name = "protobuf" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65f4a8ec18723a734e5dc09c173e0abf9690432da5340285d536edcb4dac190" +dependencies = [ + "once_cell", + "protobuf-support", + "thiserror", +] + [[package]] name = "protobuf-src" version = "1.1.0+21.5" @@ -4269,6 +4340,15 @@ dependencies = [ "autotools", ] +[[package]] +name = "protobuf-support" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6872f4d4f4b98303239a2b5838f5bbbb77b01ffc892d627957f37a22d7cfe69c" +dependencies = [ + "thiserror", +] + [[package]] name = "qstring" version = "0.7.2" @@ -4694,6 +4774,16 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "rusb" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45fff149b6033f25e825cbb7b2c625a11ee8e6dac09264d49beb125e39aa97bf" +dependencies = [ + "libc", + "libusb1-sys", +] + [[package]] name = "rustc-demangle" version = "0.1.21" @@ -4877,6 +4967,25 @@ dependencies = [ "untrusted 0.7.1", ] +[[package]] +name = "secp256k1" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" +dependencies = [ + "bitcoin_hashes", + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d1746aae42c19d583c3c1a8c646bfad910498e2051c551a7f2e3c0c9fbb7eb" +dependencies = [ + "cc", +] + [[package]] name = "security-framework" version = "2.4.2" @@ -6807,8 +6916,10 @@ dependencies = [ "parking_lot 0.12.1", "qstring", "semver 1.0.22", + "serial_test", "solana-sdk", "thiserror", + "trezor-client", "uriparse", ] @@ -8737,6 +8848,22 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de5f738ceab88e2491a94ddc33c3feeadfa95fedc60363ef110845df12f3878" +[[package]] +name = "trezor-client" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f62c95b37f6c769bd65a0d0beb8b2b003e72998003b896a616a6777c645c05ed" +dependencies = [ + "bitcoin", + "byteorder", + "hex", + "protobuf", + "rusb", + "thiserror", + "tracing", + "unicode-normalization", +] + [[package]] name = "try-lock" version = "0.2.3" diff --git a/Cargo.toml b/Cargo.toml index 14b86c3f22f2f8..99cb7721072f87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -158,6 +158,7 @@ atty = "0.2.11" backoff = "0.4.0" base64 = "0.22.0" bincode = "1.3.3" +bitcoin = "0.31.1" bitflags = { version = "2.4.2", features = ["serde"] } blake3 = "1.5.1" block-buffer = "0.10.4" @@ -428,6 +429,7 @@ toml = "0.8.10" tonic = "0.9.2" tonic-build = "0.9.2" trees = "0.4.2" +trezor-client = { version = "0.1.3", features = ["solana"]} tungstenite = "0.20.1" uriparse = "0.6.4" url = "2.5.0" diff --git a/docs/src/cli/wallets/hardware/index.md b/docs/src/cli/wallets/hardware/index.md index 30f53f86d3a3d8..1d4602d17a1758 100644 --- a/docs/src/cli/wallets/hardware/index.md +++ b/docs/src/cli/wallets/hardware/index.md @@ -23,6 +23,7 @@ hardware wallet. The Solana CLI supports the following hardware wallets: - [Ledger Nano S and Ledger Nano X](./ledger.md) +- [Trezor Model T and Trezor Safe 3](./trezor.md) ## Specify a Keypair URL diff --git a/docs/src/cli/wallets/hardware/trezor.md b/docs/src/cli/wallets/hardware/trezor.md new file mode 100644 index 00000000000000..18a0a68d7b6fa4 --- /dev/null +++ b/docs/src/cli/wallets/hardware/trezor.md @@ -0,0 +1,175 @@ +--- +title: Using Trezor Hardware Wallets in the Solana CLI +pagination_label: "Hardware Wallets in the Solana CLI: Trezor" +sidebar_label: Trezor +--- + +This page describes how to use a Trezor Model T or Trezor Safe 3 device to +interact with Solana using the command line tools. + +## Before You Begin + +- [Install the Solana command-line tools](../../install.md) +- [Review Trezor and BIP-32](https://trezor.io/learn/a/what-is-bip32) +- [Review Trezor and BIP-44](https://trezor.io/learn/a/what-is-bip44) + +## Use Trezor Model T or Trezor Safe 3 with Solana CLI + +1. Plug your Trezor Model T or Trezor Safe 3 device into your computer's USB port + +### View your Wallet Addresses + +On your computer, run: + +```bash +solana-keygen pubkey usb://trezor?key=0/0 +``` + +This confirms your Trezor device is connected properly and in the correct state +to interact with the Solana CLI. The command returns your Trezor device's first Solana account's +external (receiving) wallet address using the [BIP-32](https://trezor.io/learn/a/what-is-bip32) derivation path `m/44'/501'/0'/0'`. + +Your Trezor device supports an arbitrary number of valid wallet addresses and signers. To +view any address, use the `solana-keygen pubkey` command, as shown below, +followed by a valid [keypair URL](./index.md#specify-a-keypair-url). + +Multiple wallet addresses can be useful if you want to transfer tokens between +your own accounts for different purposes, or use different keypairs on the +device as signing authorities for a stake account, for example. + +All of the following commands will display different addresses, associated with +the keypair path given. Try them out! + +```bash +solana-keygen pubkey usb://trezor?key=0/0 +solana-keygen pubkey usb://trezor?key=0/1 +solana-keygen pubkey usb://trezor?key=1/0 +solana-keygen pubkey usb://trezor?key=1/1 +``` + +- NOTE: keypair url parameters are ignored in **zsh** +  [see troubleshooting for more info](#troubleshooting) + +You can use other values for the number after `key=` as well. Any of the +addresses displayed by these commands are valid Solana wallet addresses. The +private portion associated with each address is stored securely on the Trezor device, and +is used to sign transactions from this address. Just make a note of which +keypair URL you used to derive any address you will be using to receive tokens. + +If you are only planning to use a single address/keypair on your device, a good +easy-to-remember path might be to use the address at `key=0/`. View this address +with: + +```bash +solana-keygen pubkey usb://trezor?key=0/0 +solana-keygen pubkey usb://trezor?key=0/1 +``` + +Now you have a wallet address (or multiple addresses), you can share any of +these addresses publicly to act as a receiving address, and you can use the +associated keypair URL as the signer for transactions from that address. + +### View your Balance + +To view the balance of any account, regardless of which wallet it uses, use the +`solana balance` command: + +```bash +solana balance SOME_WALLET_ADDRESS +``` + +For example, if your address is `7cvkjYAkUYs4W8XcXsca7cBrEGFeSUjeZmKoNBvEwyri`, +then enter the following command to view the balance: + +```bash +solana balance 7cvkjYAkUYs4W8XcXsca7cBrEGFeSUjeZmKoNBvEwyri +``` + +You can also view the balance of any account address on the Accounts tab in the +[Explorer](https://explorer.solana.com/accounts) and paste the address in the +box to view the balance in your web browser. + +Note: Any address with a balance of 0 SOL, such as a newly created one on your +Trezor, will show as "Not Found" in the explorer. Empty accounts and +non-existent accounts are treated the same in Solana. This will change when your +account address has some SOL in it. + +### Send SOL from a Trezor + +To send some tokens from an address controlled by your Trezor, you will need to +use the device to sign a transaction, using the same keypair URL you used to +derive the address. To do this, make sure your Trezor is plugged in. + +The `solana transfer` command is used to specify to which address to send +tokens, how many tokens to send, and uses the `--keypair` argument to specify +which keypair is sending the tokens, which will sign the transaction, and the +balance from the associated address will decrease. + +```bash +solana transfer RECIPIENT_ADDRESS AMOUNT --keypair KEYPAIR_URL_OF_SENDER +``` + +Below is a full example. First, an address is viewed at a certain keypair URL. +Second, the balance of that address is checked. Lastly, a transfer transaction +is entered to send `1` SOL to the recipient address +`7cvkjYAkUYs4W8XcXsca7cBrEGFeSUjeZmKoNBvEwyri`. When you hit Enter for a +transfer command, you will be prompted to approve the transaction details on +your Trezor device. Follow the prompts on the device and review the +transaction details. If they look correct, follow the prompts on your device. + +```bash +~$ solana-keygen pubkey usb://trezor?key=0/0 +CjeqzArkZt6xwdnZ9NZSf8D1CNJN1rjeFiyd8q7iLWAV + +~$ solana balance CjeqzArkZt6xwdnZ9NZSf8D1CNJN1rjeFiyd8q7iLWAV +1.000005 SOL + +~$ solana transfer 7cvkjYAkUYs4W8XcXsca7cBrEGFeSUjeZmKoNBvEwyri 1 --keypair usb://trezor?key=0/0 +Waiting for your approval on Trezor hardware wallet +✅ Approved + +Signature: kemu9jDEuPirKNRKiHan7ycybYsZp7pFefAdvWZRq5VRHCLgXTXaFVw3pfh87MQcWX4kQY4TjSBmESrwMApom1V +``` + +After approving the transaction on your device, the program will display the +transaction signature, and wait for the maximum number of confirmations (32) +before returning. This only takes a few seconds, and then the transaction is +finalized on the Solana network. You can view details of this or any other +transaction by going to the Transaction tab in the +[Explorer](https://explorer.solana.com/transactions) and paste in the +transaction signature. + +## Troubleshooting + +### Keypair URL parameters are ignored in zsh + +The question mark character is a special character in zsh. If that's not a +feature you use, add the following line to your `~/.zshrc` to treat it as a +normal character: + +```bash +unsetopt nomatch +``` + +Then either restart your shell window or run `~/.zshrc`: + +```bash +source ~/.zshrc +``` + +If you would prefer not to disable zsh's special handling of the question mark +character, you can disable it explicitly with a backslash in your keypair URLs. +For example: + +```bash +solana-keygen pubkey usb://trezor\?key=0/0 +``` + +## Support + +You can find additional support and get help on the +[Solana StackExchange](https://solana.stackexchange.com). + +Read more about [sending and receiving tokens](../../examples/transfer-tokens.md) and +[delegating stake](../../examples/delegate-stake.md). You can use your Ledger keypair +URL anywhere you see an option or argument that accepts a ``. diff --git a/docs/src/cli/wallets/index.md b/docs/src/cli/wallets/index.md index 9643ef61ec13eb..af90acb5168409 100644 --- a/docs/src/cli/wallets/index.md +++ b/docs/src/cli/wallets/index.md @@ -54,7 +54,7 @@ some interface for signing transactions. ### Hardware Wallet Security A hardware wallet, such as the -[Ledger hardware wallet](https://www.ledger.com/), offers a great blend of +[Ledger hardware wallet](https://www.ledger.com/) or [Trezor hardware wallet](https://trezor.io/), offers a great blend of security and convenience for cryptocurrencies. It effectively automates the process of offline signing while retaining nearly all the convenience of a file system wallet. diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index dc45fc6c3afd8d..1da3e08a4008ee 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -627,6 +627,12 @@ version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" +[[package]] +name = "bech32" +version = "0.10.0-beta" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98f7eed2b2781a6f0b5c903471d48e15f56fb4e1165df8a9a2337fd1a59d45ea" + [[package]] name = "bincode" version = "1.3.3" @@ -656,6 +662,36 @@ dependencies = [ "syn 2.0.52", ] +[[package]] +name = "bitcoin" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c85783c2fe40083ea54a33aa2f0ba58831d90fcd190f5bdc47e74e84d2a96ae" +dependencies = [ + "bech32", + "bitcoin-internals", + "bitcoin_hashes", + "hex-conservative", + "hex_lit", + "secp256k1", +] + +[[package]] +name = "bitcoin-internals" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb" + +[[package]] +name = "bitcoin_hashes" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b" +dependencies = [ + "bitcoin-internals", + "hex-conservative", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -2115,6 +2151,24 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-conservative" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ed443af458ccb6d81c1e7e661545f94d3176752fb1df2f543b902a1e0f51e2" + +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + [[package]] name = "histogram" version = "0.6.9" @@ -2742,6 +2796,18 @@ dependencies = [ "libsecp256k1-core 0.3.0", ] +[[package]] +name = "libusb1-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da050ade7ac4ff1ba5379af847a10a10a8e284181e060105bf8d86960ce9ce0f" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "libz-sys" version = "1.1.5" @@ -3706,6 +3772,17 @@ dependencies = [ "prost", ] +[[package]] +name = "protobuf" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65f4a8ec18723a734e5dc09c173e0abf9690432da5340285d536edcb4dac190" +dependencies = [ + "once_cell", + "protobuf-support", + "thiserror", +] + [[package]] name = "protobuf-src" version = "1.1.0+21.5" @@ -3715,6 +3792,15 @@ dependencies = [ "autotools", ] +[[package]] +name = "protobuf-support" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6872f4d4f4b98303239a2b5838f5bbbb77b01ffc892d627957f37a22d7cfe69c" +dependencies = [ + "thiserror", +] + [[package]] name = "qstring" version = "0.7.2" @@ -4084,6 +4170,16 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "rusb" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab9f9ff05b63a786553a4c02943b74b34a988448671001e9a27e2f0565cc05a4" +dependencies = [ + "libc", + "libusb1-sys", +] + [[package]] name = "rustc-demangle" version = "0.1.16" @@ -4246,6 +4342,25 @@ dependencies = [ "untrusted 0.7.1", ] +[[package]] +name = "secp256k1" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" +dependencies = [ + "bitcoin_hashes", + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d1746aae42c19d583c3c1a8c646bfad910498e2051c551a7f2e3c0c9fbb7eb" +dependencies = [ + "cc", +] + [[package]] name = "security-framework" version = "2.6.1" @@ -5533,6 +5648,7 @@ dependencies = [ name = "solana-remote-wallet" version = "2.0.0" dependencies = [ + "bitcoin", "console", "dialoguer", "log", @@ -5543,6 +5659,7 @@ dependencies = [ "semver", "solana-sdk", "thiserror", + "trezor-client", "uriparse", ] @@ -7536,6 +7653,22 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de5f738ceab88e2491a94ddc33c3feeadfa95fedc60363ef110845df12f3878" +[[package]] +name = "trezor-client" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f62c95b37f6c769bd65a0d0beb8b2b003e72998003b896a616a6777c645c05ed" +dependencies = [ + "bitcoin", + "byteorder 1.5.0", + "hex", + "protobuf", + "rusb", + "thiserror", + "tracing", + "unicode-normalization", +] + [[package]] name = "try-lock" version = "0.2.2" diff --git a/remote-wallet/Cargo.toml b/remote-wallet/Cargo.toml index 8cea360d7c14ca..4b3fab819f8799 100644 --- a/remote-wallet/Cargo.toml +++ b/remote-wallet/Cargo.toml @@ -21,10 +21,12 @@ qstring = { workspace = true } semver = { workspace = true } solana-sdk = { workspace = true } thiserror = { workspace = true } +trezor-client = { workspace = true } uriparse = { workspace = true } [dev-dependencies] assert_matches = { workspace = true } +serial_test = { workspace = true } [features] default = ["linux-static-hidraw", "hidapi"] diff --git a/remote-wallet/src/lib.rs b/remote-wallet/src/lib.rs index 2400850734b1c1..fcad63e7c28db4 100644 --- a/remote-wallet/src/lib.rs +++ b/remote-wallet/src/lib.rs @@ -5,3 +5,5 @@ pub mod ledger_error; pub mod locator; pub mod remote_keypair; pub mod remote_wallet; +pub mod trezor; +pub mod trezor_error; diff --git a/remote-wallet/src/locator.rs b/remote-wallet/src/locator.rs index 613824990211f6..0e1c3a1bb715a2 100644 --- a/remote-wallet/src/locator.rs +++ b/remote-wallet/src/locator.rs @@ -12,6 +12,7 @@ use { pub enum Manufacturer { Unknown, Ledger, + Trezor, } impl Default for Manufacturer { @@ -22,6 +23,7 @@ impl Default for Manufacturer { const MANUFACTURER_UNKNOWN: &str = "unknown"; const MANUFACTURER_LEDGER: &str = "ledger"; +const MANUFACTURER_TREZOR: &str = "trezor"; #[derive(Clone, Debug, Error, PartialEq, Eq)] #[error("not a manufacturer")] @@ -39,6 +41,7 @@ impl FromStr for Manufacturer { let s = s.to_ascii_lowercase(); match s.as_str() { MANUFACTURER_LEDGER => Ok(Self::Ledger), + MANUFACTURER_TREZOR => Ok(Self::Trezor), _ => Err(ManufacturerError), } } @@ -56,6 +59,7 @@ impl AsRef for Manufacturer { match self { Self::Unknown => MANUFACTURER_UNKNOWN, Self::Ledger => MANUFACTURER_LEDGER, + Self::Trezor => MANUFACTURER_TREZOR, } } } @@ -173,7 +177,11 @@ mod tests { matches!(Manufacturer::from_str(MANUFACTURER_LEDGER), Ok(v) if v == Manufacturer::Ledger) ); assert_eq!(Manufacturer::Ledger.as_ref(), MANUFACTURER_LEDGER); - + assert_eq!(MANUFACTURER_TREZOR.try_into(), Ok(Manufacturer::Trezor)); + assert!( + matches!(Manufacturer::from_str(MANUFACTURER_TREZOR), Ok(v) if v == Manufacturer::Trezor) + ); + assert_eq!(Manufacturer::Trezor.as_ref(), MANUFACTURER_TREZOR); assert!( matches!(Manufacturer::from_str("bad-manufacturer"), Err(e) if e == ManufacturerError) ); @@ -212,6 +220,35 @@ mod tests { Ok(e) if e == expect ); + let manufacturer = Manufacturer::Trezor; + let manufacturer_str = "trezor"; + + let expect = Locator { + manufacturer, + pubkey: None, + }; + assert_matches!( + Locator::new_from_parts(manufacturer, None::), + Ok(e) if e == expect + ); + assert_matches!( + Locator::new_from_parts(manufacturer_str, None::), + Ok(e) if e == expect + ); + + let expect = Locator { + manufacturer, + pubkey: Some(pubkey), + }; + assert_matches!( + Locator::new_from_parts(manufacturer, Some(pubkey)), + Ok(e) if e == expect + ); + assert_matches!( + Locator::new_from_parts(manufacturer_str, Some(pubkey_str.as_str())), + Ok(e) if e == expect + ); + assert_matches!( Locator::new_from_parts("bad-manufacturer", None::), Err(LocatorError::ManufacturerError(e)) if e == ManufacturerError @@ -309,27 +346,110 @@ mod tests { Err(LocatorError::UnimplementedScheme) ); - // usb://bad-manufacturer + // usb://ledger/bad-pubkey let mut builder = URIReferenceBuilder::new(); builder .try_scheme(Some("usb")) .unwrap() - .try_authority(Some("bad-manufacturer")) + .try_authority(Some(Manufacturer::Ledger.as_ref())) + .unwrap() + .try_path("bad-pubkey") + .unwrap(); + let uri = builder.build().unwrap(); + assert_eq!( + Locator::new_from_uri(&uri), + Err(LocatorError::PubkeyError(ParsePubkeyError::Invalid)) + ); + + let manufacturer = Manufacturer::Trezor; + + // usb://trezor/{PUBKEY}?key=0/0 + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("usb")) + .unwrap() + .try_authority(Some(Manufacturer::Trezor.as_ref())) + .unwrap() + .try_path(pubkey_str.as_str()) + .unwrap() + .try_query(Some("key=0/0")) + .unwrap(); + let uri = builder.build().unwrap(); + let expect = Locator { + manufacturer, + pubkey: Some(pubkey), + }; + assert_eq!(Locator::new_from_uri(&uri), Ok(expect)); + + // usb://trezor/{PUBKEY} + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("usb")) + .unwrap() + .try_authority(Some(Manufacturer::Trezor.as_ref())) + .unwrap() + .try_path(pubkey_str.as_str()) + .unwrap(); + let uri = builder.build().unwrap(); + let expect = Locator { + manufacturer, + pubkey: Some(pubkey), + }; + assert_eq!(Locator::new_from_uri(&uri), Ok(expect)); + + // usb://trezor + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("usb")) + .unwrap() + .try_authority(Some(Manufacturer::Trezor.as_ref())) + .unwrap() + .try_path("") + .unwrap(); + let uri = builder.build().unwrap(); + let expect = Locator { + manufacturer, + pubkey: None, + }; + assert_eq!(Locator::new_from_uri(&uri), Ok(expect)); + + // usb://trezor/ + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("usb")) + .unwrap() + .try_authority(Some(Manufacturer::Trezor.as_ref())) + .unwrap() + .try_path("/") + .unwrap(); + let uri = builder.build().unwrap(); + let expect = Locator { + manufacturer, + pubkey: None, + }; + assert_eq!(Locator::new_from_uri(&uri), Ok(expect)); + + // bad-scheme://trezor + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("bad-scheme")) + .unwrap() + .try_authority(Some(Manufacturer::Trezor.as_ref())) .unwrap() .try_path("") .unwrap(); let uri = builder.build().unwrap(); assert_eq!( Locator::new_from_uri(&uri), - Err(LocatorError::ManufacturerError(ManufacturerError)) + Err(LocatorError::UnimplementedScheme) ); - // usb://ledger/bad-pubkey + // usb://trezor/bad-pubkey let mut builder = URIReferenceBuilder::new(); builder .try_scheme(Some("usb")) .unwrap() - .try_authority(Some(Manufacturer::Ledger.as_ref())) + .try_authority(Some(Manufacturer::Trezor.as_ref())) .unwrap() .try_path("bad-pubkey") .unwrap(); @@ -338,6 +458,21 @@ mod tests { Locator::new_from_uri(&uri), Err(LocatorError::PubkeyError(ParsePubkeyError::Invalid)) ); + + // usb://bad-manufacturer + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("usb")) + .unwrap() + .try_authority(Some("bad-manufacturer")) + .unwrap() + .try_path("") + .unwrap(); + let uri = builder.build().unwrap(); + assert_eq!( + Locator::new_from_uri(&uri), + Err(LocatorError::ManufacturerError(ManufacturerError)) + ); } #[test] @@ -386,18 +521,68 @@ mod tests { Err(LocatorError::UnimplementedScheme) ); - // usb://bad-manufacturer - let path = "usb://bad-manufacturer"; + // usb://ledger/bad-pubkey + let path = "usb://ledger/bad-pubkey"; assert_eq!( Locator::new_from_path(path), - Err(LocatorError::ManufacturerError(ManufacturerError)) + Err(LocatorError::PubkeyError(ParsePubkeyError::Invalid)) ); - // usb://ledger/bad-pubkey - let path = "usb://ledger/bad-pubkey"; + let manufacturer = Manufacturer::Trezor; + let path = format!("usb://trezor/{pubkey}?key=0/0"); + Locator::new_from_path(path).unwrap(); + + // usb://trezor/{PUBKEY}?key=0'/0' + let path = format!("usb://trezor/{pubkey}?key=0'/0'"); + let expect = Locator { + manufacturer, + pubkey: Some(pubkey), + }; + assert_eq!(Locator::new_from_path(path), Ok(expect)); + + // usb://trezor/{PUBKEY} + let path = format!("usb://trezor/{pubkey}"); + let expect = Locator { + manufacturer, + pubkey: Some(pubkey), + }; + assert_eq!(Locator::new_from_path(path), Ok(expect)); + + // usb://trezor + let path = "usb://trezor"; + let expect = Locator { + manufacturer, + pubkey: None, + }; + assert_eq!(Locator::new_from_path(path), Ok(expect)); + + // usb://trezor/ + let path = "usb://trezor/"; + let expect = Locator { + manufacturer, + pubkey: None, + }; + assert_eq!(Locator::new_from_path(path), Ok(expect)); + + // bad-scheme://trezor + let path = "bad-scheme://trezor"; + assert_eq!( + Locator::new_from_path(path), + Err(LocatorError::UnimplementedScheme) + ); + + // usb://trezor/bad-pubkey + let path = "usb://trezor/bad-pubkey"; assert_eq!( Locator::new_from_path(path), Err(LocatorError::PubkeyError(ParsePubkeyError::Invalid)) ); + + // usb://bad-manufacturer + let path = "usb://bad-manufacturer"; + assert_eq!( + Locator::new_from_path(path), + Err(LocatorError::ManufacturerError(ManufacturerError)) + ); } } diff --git a/remote-wallet/src/remote_keypair.rs b/remote-wallet/src/remote_keypair.rs index d37eefe2427175..011c9d3c9433c0 100644 --- a/remote-wallet/src/remote_keypair.rs +++ b/remote-wallet/src/remote_keypair.rs @@ -6,6 +6,7 @@ use { RemoteWallet, RemoteWalletError, RemoteWalletInfo, RemoteWalletManager, RemoteWalletType, }, + trezor::get_trezor_from_info, }, solana_sdk::{ derivation_path::DerivationPath, @@ -30,6 +31,7 @@ impl RemoteKeypair { ) -> Result { let pubkey = match &wallet_type { RemoteWalletType::Ledger(wallet) => wallet.get_pubkey(&derivation_path, confirm_key)?, + RemoteWalletType::Trezor(wallet) => wallet.get_pubkey(&derivation_path, confirm_key)?, }; Ok(Self { @@ -51,6 +53,9 @@ impl Signer for RemoteKeypair { RemoteWalletType::Ledger(wallet) => wallet .sign_message(&self.derivation_path, message) .map_err(|e| e.into()), + RemoteWalletType::Trezor(wallet) => wallet + .sign_message(&self.derivation_path, message) + .map_err(|e| e.into()), } } @@ -67,16 +72,27 @@ pub fn generate_remote_keypair( keypair_name: &str, ) -> Result { let remote_wallet_info = RemoteWalletInfo::parse_locator(locator); - if remote_wallet_info.manufacturer == Manufacturer::Ledger { - let ledger = get_ledger_from_info(remote_wallet_info, keypair_name, wallet_manager)?; - let path = format!("{}{}", ledger.pretty_path, derivation_path.get_query()); - Ok(RemoteKeypair::new( - RemoteWalletType::Ledger(ledger), - derivation_path, - confirm_key, - path, - )?) - } else { - Err(RemoteWalletError::DeviceTypeMismatch) + match remote_wallet_info.manufacturer { + Manufacturer::Ledger => { + let ledger = get_ledger_from_info(remote_wallet_info, keypair_name, wallet_manager)?; + let path = format!("{}{}", ledger.pretty_path, derivation_path.get_query()); + Ok(RemoteKeypair::new( + RemoteWalletType::Ledger(ledger), + derivation_path, + confirm_key, + path, + )?) + } + Manufacturer::Trezor => { + let trezor = get_trezor_from_info(remote_wallet_info, keypair_name, wallet_manager)?; + let path = format!("{}{}", trezor.pretty_path, derivation_path.get_query()); + Ok(RemoteKeypair::new( + RemoteWalletType::Trezor(trezor), + derivation_path, + confirm_key, + path, + )?) + } + _ => Err(RemoteWalletError::DeviceTypeMismatch), } } diff --git a/remote-wallet/src/remote_wallet.rs b/remote-wallet/src/remote_wallet.rs index 33d3b7b993909f..e1992ccb8b92dc 100644 --- a/remote-wallet/src/remote_wallet.rs +++ b/remote-wallet/src/remote_wallet.rs @@ -5,6 +5,8 @@ use { ledger::LedgerWallet, ledger_error::LedgerError, locator::{Locator, LocatorError, Manufacturer}, + trezor::TrezorWallet, + trezor_error::TrezorError, }, log::*, parking_lot::RwLock, @@ -14,10 +16,12 @@ use { signature::{Signature, SignerError}, }, std::{ + cell::RefCell, rc::Rc, time::{Duration, Instant}, }, thiserror::Error, + trezor_client::{self, error::Error as TrezorClientError, AvailableDevice}, }; const HID_GLOBAL_USAGE_PAGE: u16 = 0xFF00; @@ -56,6 +60,9 @@ pub enum RemoteWalletError { #[error("pubkey not found for given address")] PubkeyNotFound, + #[error(transparent)] + TrezorError(#[from] TrezorError), + #[error("remote wallet operation rejected by the user")] UserCancel, @@ -78,6 +85,7 @@ impl From for SignerError { RemoteWalletError::InvalidDevice => SignerError::Connection(err.to_string()), RemoteWalletError::InvalidInput(input) => SignerError::InvalidInput(input), RemoteWalletError::LedgerError(e) => SignerError::Protocol(e.to_string()), + RemoteWalletError::TrezorError(e) => SignerError::Protocol(e.to_string()), RemoteWalletError::NoDeviceFound => SignerError::NoDeviceFound, RemoteWalletError::Protocol(e) => SignerError::Protocol(e.to_string()), RemoteWalletError::UserCancel => { @@ -88,11 +96,18 @@ impl From for SignerError { } } +impl From for RemoteWalletError { + fn from(err: TrezorClientError) -> RemoteWalletError { + RemoteWalletError::TrezorError(TrezorError::TrezorError(err)) + } +} + /// Collection of connected RemoteWallets pub struct RemoteWalletManager { #[cfg(feature = "hidapi")] usb: Arc>, devices: RwLock>, + trezor_available_devices: Rc>>, } impl RemoteWalletManager { @@ -102,6 +117,7 @@ impl RemoteWalletManager { Rc::new(Self { usb, devices: RwLock::new(Vec::new()), + trezor_available_devices: Rc::new(RefCell::new(trezor_client::find_devices(false))), }) } @@ -145,7 +161,11 @@ impl RemoteWalletManager { } } - let num_curr_devices = detected_devices.len(); + let trezor_available_devices = trezor_client::find_devices(false); + let num_curr_trezor_devices = trezor_available_devices.len(); + *self.trezor_available_devices.borrow_mut() = trezor_available_devices; + + let num_curr_devices = detected_devices.len() + num_curr_trezor_devices; *self.devices.write() = detected_devices; if num_curr_devices == 0 && !errors.is_empty() { @@ -167,6 +187,11 @@ impl RemoteWalletManager { self.devices.read().iter().map(|d| d.info.clone()).collect() } + /// List trezor_available_devices for connecting + pub fn get_trezor_available_devices(&self) -> Rc>> { + Rc::clone(&self.trezor_available_devices) + } + /// Get a particular wallet #[allow(unreachable_patterns)] pub fn get_ledger( @@ -262,6 +287,7 @@ pub struct Device { #[derive(Debug)] pub enum RemoteWalletType { Ledger(Rc), + Trezor(Rc), } /// Remote wallet information. @@ -366,6 +392,35 @@ mod tests { pubkey: Pubkey::default(), error: None, })); + + let locator = Locator { + manufacturer: Manufacturer::Trezor, + pubkey: Some(pubkey), + }; + let wallet_info = RemoteWalletInfo::parse_locator(locator); + assert!(wallet_info.matches(&RemoteWalletInfo { + model: "T".to_string(), + manufacturer: Manufacturer::Trezor, + serial: "".to_string(), + host_device_path: "/host/device/path".to_string(), + pubkey, + error: None, + })); + + // Test that pubkey need not be populated + let locator = Locator { + manufacturer: Manufacturer::Trezor, + pubkey: None, + }; + let wallet_info = RemoteWalletInfo::parse_locator(locator); + assert!(wallet_info.matches(&RemoteWalletInfo { + model: "T".to_string(), + manufacturer: Manufacturer::Trezor, + serial: "".to_string(), + host_device_path: "/host/device/path".to_string(), + pubkey: Pubkey::default(), + error: None, + })); } #[test] @@ -397,6 +452,33 @@ mod tests { assert!(!info.matches(&test_info)); test_info.pubkey = pubkey; assert!(info.matches(&test_info)); + + let info = RemoteWalletInfo { + manufacturer: Manufacturer::Trezor, + model: "T".to_string(), + serial: "".to_string(), + host_device_path: "/host/device/path".to_string(), + pubkey, + error: None, + }; + let mut test_info = RemoteWalletInfo { + manufacturer: Manufacturer::Unknown, + ..RemoteWalletInfo::default() + }; + assert!(!info.matches(&test_info)); + test_info.manufacturer = Manufacturer::Trezor; + assert!(info.matches(&test_info)); + test_info.model = "Other".to_string(); + assert!(info.matches(&test_info)); + test_info.model = "T".to_string(); + assert!(info.matches(&test_info)); + test_info.host_device_path = "/host/device/path".to_string(); + assert!(info.matches(&test_info)); + let another_pubkey = solana_sdk::pubkey::new_rand(); + test_info.pubkey = another_pubkey; + assert!(!info.matches(&test_info)); + test_info.pubkey = pubkey; + assert!(info.matches(&test_info)); } #[test] @@ -415,5 +497,18 @@ mod tests { remote_wallet_info.get_pretty_path(), format!("usb://ledger/{pubkey_str}") ); + + let remote_wallet_info = RemoteWalletInfo { + model: "T".to_string(), + manufacturer: Manufacturer::Trezor, + serial: "".to_string(), + host_device_path: "/host/device/path".to_string(), + pubkey, + error: None, + }; + assert_eq!( + remote_wallet_info.get_pretty_path(), + format!("usb://trezor/{pubkey_str}") + ); } } diff --git a/remote-wallet/src/trezor.rs b/remote-wallet/src/trezor.rs new file mode 100644 index 00000000000000..ec4ee93b01d543 --- /dev/null +++ b/remote-wallet/src/trezor.rs @@ -0,0 +1,242 @@ +use { + crate::remote_wallet::{ + RemoteWallet, RemoteWalletError, RemoteWalletInfo, RemoteWalletManager, + }, + console::Emoji, + dialoguer::{theme::ColorfulTheme, Select}, + semver::Version as FirmwareVersion, + solana_sdk::{derivation_path::DerivationPath, pubkey::Pubkey, signature::Signature}, + std::{cell::RefCell, fmt, rc::Rc}, + trezor_client::{ + client::common::handle_interaction, + protos::{SolanaGetPublicKey, SolanaPublicKey, SolanaSignTx, SolanaTxSignature}, + Trezor, + }, +}; + +static CHECK_MARK: Emoji = Emoji("✅ ", ""); + +/// Trezor Wallet device +pub struct TrezorWallet { + pub trezor_client: Rc>, + pub pretty_path: String, +} + +impl fmt::Debug for TrezorWallet { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "trezor_client") + } +} + +impl TrezorWallet { + pub fn new(trezor_client: Trezor, pretty_path: String) -> Self { + Self { + trezor_client: Rc::new(RefCell::new(trezor_client)), + pretty_path, + } + } + + pub fn get_trezor_firmware_version(&self) -> Result { + let trezor_client = self.trezor_client.borrow(); + let features = trezor_client + .features() + .ok_or(RemoteWalletError::NoDeviceFound)?; + Ok(FirmwareVersion::new( + features.major_version().into(), + features.minor_version().into(), + features.patch_version().into(), + )) + } + + pub fn get_trezor_model(&self) -> Result { + let trezor_client = self.trezor_client.borrow(); + let features = trezor_client + .features() + .ok_or(RemoteWalletError::NoDeviceFound)?; + Ok(features.model().to_string()) + } + + pub fn get_trezor_device_id(&self) -> Result { + let trezor_client = self.trezor_client.borrow(); + let features = trezor_client + .features() + .ok_or(RemoteWalletError::NoDeviceFound)?; + Ok(features.device_id().to_string()) + } +} + +impl RemoteWallet for TrezorWallet { + fn name(&self) -> &str { + "Trezor hardware wallet" + } + + /// Parse device info and get device base pubkey + fn read_device(&mut self, _dev_info: &Trezor) -> Result { + unimplemented!(); + } + + /// Get solana pubkey from a RemoteWallet + fn get_pubkey( + &self, + derivation_path: &DerivationPath, + confirm_key: bool, + ) -> Result { + let address_n = DerivationPath::to_u32_vec(derivation_path); + let solana_get_pubkey = SolanaGetPublicKey { + address_n, + show_display: Some(confirm_key), + ..SolanaGetPublicKey::default() + }; + if confirm_key { + println!("Waiting for your approval on {}", self.name()); + } + let pubkey = handle_interaction( + self.trezor_client + .borrow_mut() + .call(solana_get_pubkey, Box::new(|_, m: SolanaPublicKey| Ok(m)))?, + )?; + if confirm_key { + println!("{CHECK_MARK}Approved"); + } + Pubkey::try_from(pubkey.public_key()) + .map_err(|_| RemoteWalletError::Protocol("Key packet size mismatch")) + } + + /// Sign transaction data with wallet managing pubkey at derivation path + /// `m/44'/501'/'/'`. + fn sign_message( + &self, + derivation_path: &DerivationPath, + data: &[u8], + ) -> Result { + let address_n = DerivationPath::to_u32_vec(derivation_path); + let solana_sign_tx = SolanaSignTx { + address_n, + serialized_tx: Some(data.to_vec()), + ..SolanaSignTx::default() + }; + let solana_tx_signature = handle_interaction( + self.trezor_client + .borrow_mut() + .call(solana_sign_tx, Box::new(|_, m: SolanaTxSignature| Ok(m)))?, + )?; + Signature::try_from(solana_tx_signature.signature()) + .map_err(|_e| RemoteWalletError::Protocol("Signature packet size mismatch")) + } + + /// Sign off-chain message with wallet managing pubkey at derivation path + /// `m/44'/501'/'/'`. + fn sign_offchain_message( + &self, + derivation_path: &DerivationPath, + message: &[u8], + ) -> Result { + Self::sign_message(self, derivation_path, message) + } +} + +pub fn get_trezor_from_info( + info: RemoteWalletInfo, + keypair_name: &str, + wallet_manager: &RemoteWalletManager, +) -> Result, RemoteWalletError> { + let binding = wallet_manager.get_trezor_available_devices(); + let mut trezor_available_devices = binding.borrow_mut(); + if trezor_available_devices.is_empty() { + return Err(RemoteWalletError::NoDeviceFound); + } else if trezor_available_devices.len() == 1 { + let mut trezor = trezor_available_devices + .remove(1) + .connect() + .expect("connection error"); + trezor.init_device(None)?; + return Ok(Rc::new(TrezorWallet::new(trezor, info.get_pretty_path()))); + } else { + let selection = Select::with_theme(&ColorfulTheme::default()) + .with_prompt(format!( + "Multiple hardware wallets found. Please select a device for {keypair_name:?}" + )) + .default(0) + .items(&trezor_available_devices[..]) + .interact() + .unwrap(); + let mut trezor = trezor_available_devices + .remove(selection) + .connect() + .expect("connection error"); + trezor.init_device(None)?; + return Ok(Rc::new(TrezorWallet::new(trezor, info.get_pretty_path()))); + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + serial_test::serial, + trezor_client::{find_devices, Model}, + }; + + fn init_emulator() -> Trezor { + let mut emulator = find_devices(false) + .into_iter() + .find(|t| t.model == Model::TrezorEmulator) + .expect("An emulator should be found") + .connect() + .expect("Connection to the emulator should succeed"); + emulator + .init_device(None) + .expect("Initialization of device should succeed"); + emulator + } + + #[test] + #[serial] + fn test_emulator_find() { + let trezors = find_devices(false); + assert!(!trezors.is_empty()); + assert!(trezors.iter().any(|t| t.model == Model::TrezorEmulator)); + } + + #[test] + #[serial] + fn test_solana_pubkey() { + let mut emulator = init_emulator(); + let derivation_path_str = "m/44'/501'/0'/0'"; + let derivation_path = DerivationPath::from_absolute_path_str(derivation_path_str).unwrap(); + let address_n = DerivationPath::to_u32_vec(&derivation_path); + let solana_get_pubkey = SolanaGetPublicKey { + address_n, + show_display: Some(false), + ..SolanaGetPublicKey::default() + }; + let pubkey = handle_interaction( + emulator.call(solana_get_pubkey, Box::new(|_, m: SolanaPublicKey| Ok(m))).expect("Trezor client (the emulator) has been initialized and SolanaGetPublicKey is initialized correctly") + ).expect("Trezor client (the emulator) has been initialized and SolanaGetPublicKey is initialized correctly"); + assert!(Pubkey::try_from(pubkey.public_key()).is_ok()); + } + + #[test] + #[serial] + fn test_trezor_wallet() { + let emulator = init_emulator(); + let pretty_path = "usb://trezor?key=0/0".to_string(); + let trezor_wallet = TrezorWallet::new(emulator, pretty_path); + let expected_model = "T".to_string(); + let model = trezor_wallet + .get_trezor_model() + .expect("Trezor client (the emulator) has been initialized"); + assert_eq!(expected_model, model); + let device_id = trezor_wallet + .get_trezor_device_id() + .expect("Trezor client (the emulator) has been initialized"); + assert!(!device_id.is_empty()); + let firmware_version = trezor_wallet.get_trezor_firmware_version(); + assert!(firmware_version.is_ok()); + let derivation_path = DerivationPath::new_bip44(Some(0), Some(0)); + let pubkey = trezor_wallet + .get_pubkey(&derivation_path, false) + .expect("Trezor client (the emulator) has been initialized"); + assert!(!pubkey.to_string().is_empty()); + } +} diff --git a/remote-wallet/src/trezor_error.rs b/remote-wallet/src/trezor_error.rs new file mode 100644 index 00000000000000..722fe4dfba35b4 --- /dev/null +++ b/remote-wallet/src/trezor_error.rs @@ -0,0 +1,13 @@ +use {thiserror::Error, trezor_client::error::Error as TrezorClientError}; + +#[derive(Error, Debug)] +pub enum TrezorError { + #[error(transparent)] + TrezorError(#[from] TrezorClientError), +} + +impl Clone for TrezorError { + fn clone(&self) -> Self { + unimplemented!(); + } +} diff --git a/sdk/src/derivation_path.rs b/sdk/src/derivation_path.rs index 4e76ddf95d2a9b..e51fbcd2b3db5b 100644 --- a/sdk/src/derivation_path.rs +++ b/sdk/src/derivation_path.rs @@ -188,6 +188,11 @@ impl DerivationPath { Ok(None) } } + + /// Convert a BIP-32 derivation path into a `Vec`. + pub fn to_u32_vec(path: &DerivationPath) -> Vec { + path.into_iter().map(|i| i.to_bits()).collect() + } } impl fmt::Debug for DerivationPath { @@ -747,6 +752,14 @@ mod tests { ); } + #[test] + fn test_to_u32_vec() { + let derivation_path_str = "m/44'/501'/0'/0'"; + let derivation_path = DerivationPath::from_absolute_path_str(derivation_path_str).unwrap(); + let expected: Vec = vec![2147483692, 2147484149, 2147483648, 2147483648]; + assert_eq!(expected, DerivationPath::to_u32_vec(&derivation_path)); + } + #[test] fn test_get_query() { let derivation_path = DerivationPath::new_bip44_with_coin(TestCoin, None, None);