Created
August 8, 2017 01:07
-
-
Save chjj/1f740a9dd29c7b625febc89a88fdf236 to your computer and use it in GitHub Desktop.
Sweep BCC from trezor multisig
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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