2017-02-10 01:07:06 +01:00
|
|
|
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
|
|
|
|
// This file is part of Parity.
|
|
|
|
|
|
|
|
// Parity is free software: you can redistribute it and/or modify
|
|
|
|
// it under the terms of the GNU General Public License as published by
|
|
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
|
|
// (at your option) any later version.
|
|
|
|
|
|
|
|
// Parity is distributed in the hope that it will be useful,
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
// GNU General Public License for more details.
|
|
|
|
|
|
|
|
// You should have received a copy of the GNU General Public License
|
|
|
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
//! Ledger hardware wallet module. Supports Ledger Blue and Nano S.
|
|
|
|
/// See https://github.com/LedgerHQ/blue-app-eth/blob/master/doc/ethapp.asc for protocol details.
|
|
|
|
|
|
|
|
use hidapi;
|
|
|
|
use std::fmt;
|
|
|
|
use std::cmp::min;
|
|
|
|
use std::str::FromStr;
|
|
|
|
use std::time::Duration;
|
|
|
|
use super::WalletInfo;
|
|
|
|
use ethkey::{Address, Signature};
|
2017-05-24 12:31:33 +02:00
|
|
|
use bigint::hash::H256;
|
2017-02-10 01:07:06 +01:00
|
|
|
|
|
|
|
const LEDGER_VID: u16 = 0x2c97;
|
|
|
|
const LEDGER_PIDS: [u16; 2] = [0x0000, 0x0001]; // Nano S and Blue
|
|
|
|
const ETH_DERIVATION_PATH_BE: [u8; 17] = [ 4, 0x80, 0, 0, 44, 0x80, 0, 0, 60, 0x80, 0, 0, 0, 0, 0, 0, 0 ]; // 44'/60'/0'/0
|
|
|
|
const ETC_DERIVATION_PATH_BE: [u8; 21] = [ 5, 0x80, 0, 0, 44, 0x80, 0, 0, 60, 0x80, 0x02, 0x73, 0xd0, 0x80, 0, 0, 0, 0, 0, 0, 0 ]; // 44'/60'/160720'/0'/0
|
|
|
|
|
|
|
|
const APDU_TAG: u8 = 0x05;
|
|
|
|
const APDU_CLA: u8 = 0xe0;
|
|
|
|
|
|
|
|
#[cfg(windows)] const HID_PREFIX_ZERO: usize = 1;
|
|
|
|
#[cfg(not(windows))] const HID_PREFIX_ZERO: usize = 0;
|
|
|
|
|
|
|
|
mod commands {
|
|
|
|
pub const GET_APP_CONFIGURATION: u8 = 0x06;
|
|
|
|
pub const GET_ETH_PUBLIC_ADDRESS: u8 = 0x02;
|
|
|
|
pub const SIGN_ETH_TRANSACTION: u8 = 0x04;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Key derivation paths used on ledger wallets.
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
|
|
pub enum KeyPath {
|
|
|
|
/// Ethereum.
|
|
|
|
Ethereum,
|
|
|
|
/// Ethereum classic.
|
|
|
|
EthereumClassic,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Hardware waller error.
|
|
|
|
#[derive(Debug)]
|
|
|
|
pub enum Error {
|
|
|
|
/// Ethereum wallet protocol error.
|
|
|
|
Protocol(&'static str),
|
|
|
|
/// Hidapi error.
|
|
|
|
Usb(hidapi::HidError),
|
|
|
|
/// Device with request key is not available.
|
|
|
|
KeyNotFound,
|
|
|
|
/// Signing has been cancelled by user.
|
|
|
|
UserCancel,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl fmt::Display for Error {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
|
|
|
match *self {
|
|
|
|
Error::Protocol(ref s) => write!(f, "Ledger protocol error: {}", s),
|
|
|
|
Error::Usb(ref e) => write!(f, "USB communication error: {}", e),
|
|
|
|
Error::KeyNotFound => write!(f, "Key not found"),
|
|
|
|
Error::UserCancel => write!(f, "Operation has been cancelled"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<hidapi::HidError> for Error {
|
|
|
|
fn from(err: hidapi::HidError) -> Error {
|
|
|
|
Error::Usb(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Ledger device manager.
|
|
|
|
pub struct Manager {
|
|
|
|
usb: hidapi::HidApi,
|
|
|
|
devices: Vec<Device>,
|
|
|
|
key_path: KeyPath,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
struct Device {
|
|
|
|
path: String,
|
|
|
|
info: WalletInfo,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Manager {
|
|
|
|
/// Create a new instance.
|
|
|
|
pub fn new() -> Result<Manager, Error> {
|
|
|
|
let manager = Manager {
|
|
|
|
usb: hidapi::HidApi::new()?,
|
|
|
|
devices: Vec::new(),
|
|
|
|
key_path: KeyPath::Ethereum,
|
|
|
|
};
|
|
|
|
Ok(manager)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Re-populate device list. Only those devices that have Ethereum app open will be added.
|
|
|
|
pub fn update_devices(&mut self) -> Result<usize, Error> {
|
|
|
|
self.usb.refresh_devices();
|
|
|
|
let devices = self.usb.devices();
|
|
|
|
let mut new_devices = Vec::new();
|
|
|
|
let mut num_new_devices = 0;
|
|
|
|
for device in devices {
|
|
|
|
trace!("Checking device: {:?}", device);
|
|
|
|
if device.vendor_id != LEDGER_VID || !LEDGER_PIDS.contains(&device.product_id) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
match self.read_device_info(&device) {
|
|
|
|
Ok(info) => {
|
|
|
|
debug!("Found device: {:?}", info);
|
|
|
|
if !self.devices.iter().any(|d| d.path == info.path) {
|
|
|
|
num_new_devices += 1;
|
|
|
|
}
|
|
|
|
new_devices.push(info);
|
|
|
|
|
|
|
|
},
|
|
|
|
Err(e) => debug!("Error reading device info: {}", e),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
self.devices = new_devices;
|
|
|
|
Ok(num_new_devices)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Select key derivation path for a known chain.
|
|
|
|
pub fn set_key_path(&mut self, key_path: KeyPath) {
|
|
|
|
self.key_path = key_path;
|
|
|
|
}
|
|
|
|
|
|
|
|
fn read_device_info(&self, dev_info: &hidapi::HidDeviceInfo) -> Result<Device, Error> {
|
|
|
|
let mut handle = self.open_path(&dev_info.path)?;
|
|
|
|
let address = Self::read_wallet_address(&mut handle, self.key_path)?;
|
|
|
|
let manufacturer = dev_info.manufacturer_string.clone().unwrap_or("Unknown".to_owned());
|
|
|
|
let name = dev_info.product_string.clone().unwrap_or("Unknown".to_owned());
|
|
|
|
let serial = dev_info.serial_number.clone().unwrap_or("Unknown".to_owned());
|
|
|
|
Ok(Device {
|
|
|
|
path: dev_info.path.clone(),
|
|
|
|
info: WalletInfo {
|
|
|
|
name: name,
|
|
|
|
manufacturer: manufacturer,
|
|
|
|
serial: serial,
|
|
|
|
address: address,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
fn read_wallet_address(handle: &hidapi::HidDevice, key_path: KeyPath) -> Result<Address, Error> {
|
|
|
|
let ver = Self::send_apdu(handle, commands::GET_APP_CONFIGURATION, 0, 0, &[])?;
|
|
|
|
if ver.len() != 4 {
|
|
|
|
return Err(Error::Protocol("Version packet size mismatch"));
|
|
|
|
}
|
|
|
|
|
|
|
|
let (major, minor, patch) = (ver[1], ver[2], ver[3]);
|
|
|
|
if major < 1 || (major == 1 && minor == 0 && patch < 3) {
|
|
|
|
return Err(Error::Protocol("App version 1.0.3 is required."));
|
|
|
|
}
|
|
|
|
|
|
|
|
let eth_path = Ð_DERIVATION_PATH_BE[..];
|
|
|
|
let etc_path = &ETC_DERIVATION_PATH_BE[..];
|
|
|
|
let derivation_path = match key_path {
|
|
|
|
KeyPath::Ethereum => eth_path,
|
|
|
|
KeyPath::EthereumClassic => etc_path,
|
|
|
|
};
|
|
|
|
let key_and_address = Self::send_apdu(handle, commands::GET_ETH_PUBLIC_ADDRESS, 0, 0, derivation_path)?;
|
|
|
|
if key_and_address.len() != 107 { // 1 + 65 PK + 1 + 40 Addr (ascii-hex)
|
|
|
|
return Err(Error::Protocol("Key packet size mismatch"));
|
|
|
|
}
|
|
|
|
let address_string = ::std::str::from_utf8(&key_and_address[67..107])
|
|
|
|
.map_err(|_| Error::Protocol("Invalid address string"))?;
|
|
|
|
|
|
|
|
let address = Address::from_str(&address_string)
|
|
|
|
.map_err(|_| Error::Protocol("Invalid address string"))?;
|
|
|
|
|
|
|
|
Ok(address)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// List connected wallets. This only returns wallets that are ready to be used.
|
|
|
|
pub fn list_devices(&self) -> Vec<WalletInfo> {
|
|
|
|
self.devices.iter().map(|d| d.info.clone()).collect()
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Get wallet info.
|
|
|
|
pub fn device_info(&self, address: &Address) -> Option<WalletInfo> {
|
|
|
|
self.devices.iter().find(|d| &d.info.address == address).map(|d| d.info.clone())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Sign transaction data with wallet managing `address`.
|
|
|
|
pub fn sign_transaction(&self, address: &Address, data: &[u8]) -> Result<Signature, Error> {
|
|
|
|
let device = self.devices.iter().find(|d| &d.info.address == address)
|
|
|
|
.ok_or(Error::KeyNotFound)?;
|
|
|
|
|
|
|
|
let handle = self.open_path(&device.path)?;
|
|
|
|
|
|
|
|
let eth_path = Ð_DERIVATION_PATH_BE[..];
|
|
|
|
let etc_path = &ETC_DERIVATION_PATH_BE[..];
|
|
|
|
let derivation_path = match self.key_path {
|
|
|
|
KeyPath::Ethereum => eth_path,
|
|
|
|
KeyPath::EthereumClassic => etc_path,
|
|
|
|
};
|
|
|
|
const MAX_CHUNK_SIZE: usize = 255;
|
|
|
|
let mut chunk: [u8; MAX_CHUNK_SIZE] = [0; MAX_CHUNK_SIZE];
|
|
|
|
&mut chunk[0..derivation_path.len()].copy_from_slice(derivation_path);
|
|
|
|
let mut dest_offset = derivation_path.len();
|
|
|
|
let mut data_pos = 0;
|
|
|
|
let mut result;
|
|
|
|
loop {
|
|
|
|
let p1 = if data_pos == 0 { 0x00 } else { 0x80 };
|
|
|
|
let dest_left = MAX_CHUNK_SIZE - dest_offset;
|
|
|
|
let chunk_data_size = min(dest_left, data.len() - data_pos);
|
|
|
|
&mut chunk [dest_offset..][0..chunk_data_size].copy_from_slice(&data[data_pos..][0..chunk_data_size]);
|
|
|
|
result = Self::send_apdu(&handle, commands::SIGN_ETH_TRANSACTION, p1, 0, &chunk[0..(dest_offset + chunk_data_size)])?;
|
|
|
|
dest_offset = 0;
|
|
|
|
data_pos += chunk_data_size;
|
|
|
|
if data_pos == data.len() {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if result.len() != 65 {
|
|
|
|
return Err(Error::Protocol("Signature packet size mismatch"));
|
|
|
|
}
|
2017-02-16 17:45:12 +01:00
|
|
|
let v = (result[0] + 1) % 2;
|
2017-02-10 01:07:06 +01:00
|
|
|
let r = H256::from_slice(&result[1..33]);
|
|
|
|
let s = H256::from_slice(&result[33..65]);
|
|
|
|
Ok(Signature::from_rsv(&r, &s, v))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn open_path(&self, path: &str) -> Result<hidapi::HidDevice, Error> {
|
|
|
|
let mut err = Error::KeyNotFound;
|
|
|
|
/// Try to open device a few times.
|
|
|
|
for _ in 0..10 {
|
|
|
|
match self.usb.open_path(&path) {
|
|
|
|
Ok(handle) => return Ok(handle),
|
|
|
|
Err(e) => err = From::from(e),
|
|
|
|
}
|
|
|
|
::std::thread::sleep(Duration::from_millis(200));
|
|
|
|
}
|
|
|
|
Err(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn send_apdu(handle: &hidapi::HidDevice, command: u8, p1: u8, p2: u8, data: &[u8]) -> Result<Vec<u8>, Error> {
|
|
|
|
const HID_PACKET_SIZE: usize = 64 + HID_PREFIX_ZERO;
|
|
|
|
let mut offset = 0;
|
|
|
|
let mut chunk_index = 0;
|
|
|
|
loop {
|
|
|
|
let mut hid_chunk: [u8; HID_PACKET_SIZE] = [0; HID_PACKET_SIZE];
|
|
|
|
let mut chunk_size = if chunk_index == 0 { 12 } else { 5 };
|
|
|
|
let size = min(64 - chunk_size, data.len() - offset);
|
|
|
|
{
|
|
|
|
let mut chunk = &mut hid_chunk[HID_PREFIX_ZERO..];
|
|
|
|
&mut chunk[0..5].copy_from_slice(&[0x01, 0x01, APDU_TAG, (chunk_index >> 8) as u8, (chunk_index & 0xff) as u8 ]);
|
2017-03-11 19:58:15 +01:00
|
|
|
|
2017-02-10 01:07:06 +01:00
|
|
|
if chunk_index == 0 {
|
|
|
|
let data_len = data.len() + 5;
|
|
|
|
&mut chunk[5..12].copy_from_slice(&[ (data_len >> 8) as u8, (data_len & 0xff) as u8, APDU_CLA, command, p1, p2, data.len() as u8 ]);
|
|
|
|
}
|
2017-03-11 19:58:15 +01:00
|
|
|
|
2017-02-10 01:07:06 +01:00
|
|
|
&mut chunk[chunk_size..chunk_size + size].copy_from_slice(&data[offset..offset + size]);
|
|
|
|
offset += size;
|
|
|
|
chunk_size += size;
|
|
|
|
}
|
|
|
|
trace!("writing {:?}", &hid_chunk[..]);
|
2017-03-19 18:34:31 +01:00
|
|
|
let n = handle.write(&hid_chunk[..])?;
|
2017-02-10 01:07:06 +01:00
|
|
|
if n < chunk_size {
|
|
|
|
return Err(Error::Protocol("Write data size mismatch"));
|
|
|
|
}
|
|
|
|
if offset == data.len() {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
chunk_index += 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// read response
|
|
|
|
chunk_index = 0;
|
|
|
|
let mut message_size = 0;
|
|
|
|
let mut message = Vec::new();
|
|
|
|
loop {
|
|
|
|
let mut chunk: [u8; HID_PACKET_SIZE] = [0; HID_PACKET_SIZE];
|
|
|
|
let chunk_size = handle.read(&mut chunk)?;
|
|
|
|
trace!("read {:?}", &chunk[..]);
|
2017-02-16 17:45:12 +01:00
|
|
|
if chunk_size < 5 || chunk[0] != 0x01 || chunk[1] != 0x01 || chunk[2] != APDU_TAG {
|
2017-02-10 01:07:06 +01:00
|
|
|
return Err(Error::Protocol("Unexpected chunk header"));
|
|
|
|
}
|
|
|
|
let seq = (chunk[3] as usize) << 8 | (chunk[4] as usize);
|
|
|
|
if seq != chunk_index {
|
|
|
|
return Err(Error::Protocol("Unexpected chunk header"));
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut offset = 5;
|
|
|
|
if seq == 0 {
|
|
|
|
// read message size and status word.
|
|
|
|
if chunk_size < 7 {
|
|
|
|
return Err(Error::Protocol("Unexpected chunk header"));
|
|
|
|
}
|
|
|
|
message_size = (chunk[5] as usize) << 8 | (chunk[6] as usize);
|
|
|
|
offset += 2;
|
|
|
|
}
|
|
|
|
message.extend_from_slice(&chunk[offset..chunk_size]);
|
|
|
|
message.truncate(message_size);
|
|
|
|
if message.len() == message_size {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
chunk_index +=1;
|
|
|
|
}
|
|
|
|
if message.len() < 2 {
|
|
|
|
return Err(Error::Protocol("No status word"));
|
|
|
|
}
|
|
|
|
let status = (message[message.len() - 2] as usize) << 8 | (message[message.len() - 1] as usize);
|
|
|
|
debug!("Read status {:x}", status);
|
|
|
|
match status {
|
|
|
|
0x6700 => Err(Error::Protocol("Incorrect length")),
|
|
|
|
0x6982 => Err(Error::Protocol("Security status not satisfied (Canceled by user)")),
|
|
|
|
0x6a80 => Err(Error::Protocol("Invalid data")),
|
|
|
|
0x6a82 => Err(Error::Protocol("File not found")),
|
|
|
|
0x6a85 => Err(Error::UserCancel),
|
|
|
|
0x6b00 => Err(Error::Protocol("Incorrect parameters")),
|
|
|
|
0x6d00 => Err(Error::Protocol("Not implemented. Make sure Ethereum app is running.")),
|
|
|
|
0x6faa => Err(Error::Protocol("You Ledger need to be unplugged")),
|
|
|
|
0x6f00...0x6fff => Err(Error::Protocol("Internal error")),
|
|
|
|
0x9000 => Ok(()),
|
|
|
|
_ => Err(Error::Protocol("Unknown error")),
|
|
|
|
|
|
|
|
}?;
|
|
|
|
let new_len = message.len() - 2;
|
|
|
|
message.truncate(new_len);
|
|
|
|
Ok(message)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn smoke() {
|
2017-07-06 11:26:14 +02:00
|
|
|
use rustc_hex::FromHex;
|
2017-02-10 01:07:06 +01:00
|
|
|
let mut manager = Manager::new().unwrap();
|
|
|
|
manager.update_devices().unwrap();
|
|
|
|
for d in &manager.devices {
|
|
|
|
println!("Device: {:?}", d);
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(address) = manager.list_devices().first().map(|d| d.address.clone()) {
|
|
|
|
let tx = FromHex::from_hex("eb018504a817c80082520894a6ca2e6707f2cc189794a9dd459d5b05ed1bcd1c8703f26fcfb7a22480018080").unwrap();
|
|
|
|
let signature = manager.sign_transaction(&address, &tx);
|
|
|
|
println!("Got {:?}", signature);
|
|
|
|
assert!(signature.is_ok());
|
|
|
|
let large_tx = FromHex::from_hex("f8cb81968504e3b2920083024f279475b02a3c39710d6a3f2870d0d788299d48e790f180b8a4b61d27f6000000000000000000000000e1af840a5a1cb1efdf608a97aa632f4aa39ed199000000000000000000000000000000000000000000000000105ff43f46a9a800000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018080").unwrap();
|
|
|
|
let signature = manager.sign_transaction(&address, &large_tx);
|
|
|
|
println!("Got {:?}", signature);
|
|
|
|
assert!(signature.is_ok());
|
|
|
|
let huge_tx = FromHex::from_hex("f935e98201048505d21dba00833b82608080b935946103e86003908155620d2f00600455601460055560a060405260608190527f2e2e2e00000000000000000000000000000000000000000000000000000000006080908152600d805460008290527f2e2e2e00000000000000000000000000000000000000000000000000000000068255909260008051602062003474833981519152602060026001851615610100026000190190941693909304601f0192909204820192909190620000dc565b82800160010185558215620000dc579182015b82811115620000dc578251825591602001919060010190620000bf565b5b50620001009291505b80821115620000fc5760008155600101620000e6565b5090565b5050600e8054600360ff199182168117909255604080518082019091528281527f2e2e2e00000000000000000000000000000000000000000000000000000000006020918201908152600f80546000829052825160069516949094178155937f8d1108e10bcb7c27dddfc02ed9d693a074039d026cf4ea4240b40f7d581ac80260026101006001871615026000190190951694909404601f019290920483019290620001d7565b82800160010185558215620001d7579182015b82811115620001d7578251825591602001919060010190620001ba565b5b50620001fb9291505b80821115620000fc5760008155600101620000e6565b5090565b50506010805460ff19166001179055346200000057604051620034943803806200349483398101604090815281516020830151918301516060840151919390810191015b5b5b60068054600160a060020a0319166c01000000000000000000000000338102041790558151600d80546000829052909160008051602062003474833981519152602060026101006001861615026000190190941693909304601f908101849004820193870190839010620002c157805160ff1916838001178555620002f1565b82800160010185558215620002f1579182015b82811115620002f1578251825591602001919060010190620002d4565b5b50620003159291505b80821115620000fc5760008155600101620000e6565b5090565b505080600f9080519060200190828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106200036557805160ff191683800117855562000395565b8280016001018555821562000395579182015b828111156200039557825182559160200191906001019062000378565b5b50620003b99291505b80821115620000fc5760008155600101620000e6565b5090565b5050600980546c01000000000000000000000000808602819004600160a060020a0319928316179092556007805487840293909304929091169190911790555b505050505b613066806200040e6000396000f300606060405236156102035760e060020a600035046306fdde03811461034b578063095ea7b3146103c65780630b0b6d5b146103ed5780631b1ccc47146103fc57806320e870931461047757806323b872dd1461049657806325b29d84146104c057806327187991146104df578063277ccde2146104f15780632e1fbfcd14610510578063308b2fdc14610532578063313ce5671461055457806338cc48311461057757806340eddc4e146105a057806341f4793a146105bf578063467ed261146105de578063471ad963146105fd5780634e860ebb1461060f5780634efbe9331461061e57806354786b4e1461064257806354e35ba2146106bd57806358793ad4146106d25780635abedab21461073f5780635af2f8211461074e57806360483a3f1461076d57806360d12fa0146107da578063698f2e84146108035780636a749986146108155780636d5f66391461082a5780636e9c36831461083c57806370a082311461085e5780637a290fe5146108805780637e7541461461088f57806394c41bdb1461090a57806395d89b4114610929578063962a64cd146109a4578063a0b6533214610a09578063a9059cbb14610a2b578063ab62438f14610a52578063b63ca98114610aa9578063b7c54c6f14610abb578063c4e41b2214610ada578063ca7c4dba14610af9578063cb79e31b14610b18578063dd62ed3e14610b3a575b6103495b60006000600c546000141561021b57610000565b600354600c54670de0b6b3a764000091349091020204915060009050816001600030600160a060020a031681526020019081526020016000205410156102c557600160a060020a033016600090815260016020526040902054600c54909250828115610000570466038d7ea4c68000023403905033600160a060020a03166108fc829081150290604051809050600060405180830381858888f1935050505015156102c557610000565b5b600160a060020a03338116600081815260016020908152604080832080548801905530909416825283822080548790039055601380543487900301908190559154600c548551908152918201879052845190949293927f5a0391f2a67f11ed0034b68f8cf14e7e41d6f86e0a7622f2af5ea8f07b488396928290030190a45b5050565b005b3461000057610358610b5f565b60405180806020018281038252838181518152602001915080519060200190808383829060006004602084601f0104600302600f01f150905090810190601f1680156103b85780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34610000
|
|
|
|
let signature = manager.sign_transaction(&address, &huge_tx);
|
|
|
|
println!("Got {:?}", signature);
|
|
|
|
assert!(signature.is_ok());
|
|
|
|
}
|
|
|
|
}
|