Skip to content

Instantly share code, notes, and snippets.

@chjj
Created August 8, 2017 01:07
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chjj/1f740a9dd29c7b625febc89a88fdf236 to your computer and use it in GitHub Desktop.
Save chjj/1f740a9dd29c7b625febc89a88fdf236 to your computer and use it in GitHub Desktop.
Sweep BCC from trezor multisig
'use strict';
// Sweep bcc from trezor multisig.
const assert = require('assert');
const util = require('util');
const trezor = require('trezor.js-node');
const HDPublicKey = require('bcoin/lib/hd/public');
const HARDENED = 0x80000000;
process.on('unhandledRejection', (err) => {
throw err;
});
const list = new trezor.DeviceList({ debug: false });
function createSweep(options) {
const purpose = options.purpose || 44;
const type = options.type || 0;
const account = options.account || 0;
const inputs = [];
const outputs = [];
let nodes = null;
if (options.multisig) {
const {keys, m} = options.multisig;
assert(Array.isArray(keys));
assert(typeof m === 'number' && m > 0);
nodes = keys.map(xpub => convertKey(xpub));
}
let total = 0;
for (const inp of options.inputs) {
const [branch, index] = inp.path;
const input = {
prev_hash: inp.hash,
prev_index: inp.index,
amount: inp.value,
address_n: [
(purpose | HARDENED) >>> 0,
(type | HARDENED) >>> 0,
(account | HARDENED) >>> 0,
branch,
index
],
script_type: undefined,
multisig: undefined
};
if (nodes) {
const sorted = nodes.slice().sort((a, b) => {
const k1 = a.key.derive(branch).derive(index);
const k2 = b.key.derive(branch).derive(index);
return k1.publicKey.compare(k2.publicKey);
});
input.script_type = 'SPENDMULTISIG';
input.multisig = {
pubkeys: sorted.map((hd) => {
return {
node: cloneNode(hd.node),
address_n: [branch, index]
};
}),
signatures: [],
m: options.multisig.m
};
for (let i = 0; i < nodes.length; i++)
input.multisig.signatures.push('');
}
inputs.push(input);
total += inp.value;
}
if (options.outputs) {
for (const out of options.outputs) {
const output = {
amount: out.value,
address: out.address,
script_type: 'PAYTOADDRESS'
};
outputs.push(output);
}
} else {
const output = {
address: options.to,
amount: Math.max(0, total - options.fee),
script_type: 'PAYTOADDRESS'
};
outputs.push(output);
}
return {
inputs: inputs,
outputs: outputs
};
}
function convertKey(xpub) {
const key = HDPublicKey.fromBase58(xpub);
return {
node: {
depth: key.depth,
child_num: key.childIndex,
fingerprint: key.parentFingerPrint.readUInt32BE(0, true),
public_key: key.publicKey.toString('hex'),
chain_code: key.chainCode.toString('hex')
},
key: key
};
}
function cloneNode(node) {
return {
depth: node.depth,
child_num: node.child_num,
fingerprint: node.fingerprint,
public_key: node.public_key,
chain_code: node.chain_code
};
}
list.on('connect', async (device) => {
console.log('Connected device ' + device.features.label);
device.on('button', (code) => {
handleButton(device.features.label, code);
});
device.on('passphrase', handlePassphrase);
device.on('pin', handlePin);
device.on('disconnect', () => {
console.log('Disconnected an opened device');
});
if (device.isBootloader())
throw new Error('Device is in bootloader mode, re-connected it');
// Example
const tx = createSweep({
purpose: 48,
account: 0,
inputs: [
{
hash: '0000000000000000000000000000000000000000000000000000000000000000',
index: 0,
value: 100000000,
path: [0, 0]
}
],
to: '1111111111111111111114oLvT2',
fee: 100000,
multisig: {
m: 2,
keys: [
'xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6LBpB85b3D2yc8sfvZU521AAwdZafEz7mnzBBsz4wKY5e4cp9LB',
'xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6LBpB85b3D2yc8sfvZU521AAwdZafEz7mnzBBsz4wKY5e4cp9LB',
'xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6LBpB85b3D2yc8sfvZU521AAwdZafEz7mnzBBsz4wKY5e4cp9LB'
]
}
});
await device.waitForSessionAndRun(async (session) => {
const coin = device.getCoin('Bcash');
const {message} = await session.signTx(tx.inputs, tx.outputs, [], coin);
const {serialized} = message;
const sigs = serialized.signatures;
const hex = serialized.serialized_tx;
console.log('');
console.log('Signatures:');
console.log(sigs);
console.log('');
console.log('Serialized TX:');
console.log(hex);
console.log('');
});
});
list.on('disconnect', (device) => {
console.log('Disconnected device ' + device.features.label);
});
list.on('error', (err) => {
console.error('List error:', err);
});
list.on('connectUnacquired', async (device) => {
await wait(1000);
await device.steal();
console.log('steal done. now wait for another connect');
});
function wait(t) {
return new Promise(r => setTimeout(r, t));
}
function handleButton(label, code) {
console.log('Look at device ' + label + ' and press the button, human.');
}
function handlePassphrase(callback) {
console.log('Please enter passphrase.');
process.stdin.resume();
process.stdin.on('data', (buffer) => {
var text = buffer.toString('utf8').replace(/\n$/, '');
process.stdin.pause();
callback(null, text);
});
}
const TABLE = {
'a': '7',
'b': '8',
'c': '9',
'd': '4',
'e': '5',
'f': '6',
'g': '1',
'h': '2',
'i': '3',
'A': '7',
'B': '8',
'C': '9',
'D': '4',
'E': '5',
'F': '6',
'G': '1',
'H': '2',
'I': '3'
};
function handlePin(type, callback) {
console.log('Please enter PIN. The positions:');
console.log('A B C');
console.log('D E F');
console.log('G H I');
process.stdin.resume();
process.stdin.on('data', (data) => {
process.stdin.pause();
const text = data.toString('ascii').trim();
let pin = '';
for (let i = 0; i < text.length; i++) {
const ch = TABLE[text[i]];
if (ch == null)
continue;
pin += ch;
}
callback(null, pin);
});
}
process.on('exit', () {
list.onbeforeunload();
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment