melis-fork-claimer
Version:
Code to claim coins on bitcoin forks
1,144 lines (1,042 loc) • 34.8 kB
JavaScript
//
// Written by Alessandro Polverini for Melis Wallet -- melis.io
//
const isNode = (require('detect-node') && !('electron' in global.process.versions))
const nodeFetch = require('node-fetch')
const bs58check = require('bs58check')
const MELIS = require('melis-api-js')
const varuint = require('varuint-bitcoin')
const Bitcoin = MELIS.Bitcoin
const bcrypto = Bitcoin.crypto
//const bscript = Bitcoin.script
// Address converter tests
// P2SH 3L9XoqFjwWCiqpDVvHFXroNKPuyy87v143 -> BTCP bxuZxSTk47esMjk5b8DytwyYdjGxVudZ2rq
// P2PKH: 1JH6b8DcfMW4eX9ir865zPaZJnoqzpeE5M -> BTCP b1MkJGK4cQWYAmoUHoGX4atZEw4WvFaqPH1
// P2PKH: 17BfWLPhL8T6cee6TkJ513wPfR79Ddca6T -> BTCP b1AesBXEh5HVCjvxfQtj3bYv5HgpDUozDBs
const MIGRATED_ACCOUNT_NAME_SUFFIX = " Claimed Coins"
const FORKS = {
BCD: {
fork: 495866,
name: "Bitcoin Diamond",
version: 12,
// pubKeyHash: Buffer.from("00", 'hex'),
// scriptHash: Buffer.from("05", 'hex'),
signtype: 0x01,
signid: 0x01,
bcdGarbage: Buffer.from("FF".repeat(32), 'hex'),
coinratio: 10,
utxoApi: bcdQueryUtxo
},
BCI: {
fork: 505083,
name: "Bitcoin Interest",
pubKeyHash: Buffer.from("66", 'hex'),
scriptHash: Buffer.from("17", 'hex'),
signtype: 0x41,
signid: 0x41 | (0x4F << 8),
bip143: true,
insightApi: "https://explorer.bitcoininterest.io/api/"
},
BCX: {
fork: 498888,
name: "BitcoinX",
pubKeyHash: Buffer.from("4b", 'hex'),
scriptHash: Buffer.from("3f", 'hex'),
signtype: 0x11,
signid: 0x11,
bip143: true,
coinratio: 10000,
insightApi: "https://bcx.info/insight-api/"
},
BTCP: {
fork: 511346,
name: "Bitcoin Private",
pubKeyHash: Buffer.from("\x13\x25"),
scriptHash: Buffer.from("\x13\xaf", 'ascii'),
signtype: 0x41,
signid: 0x41 | (42 << 8),
insightApi: "http://explorer.btcprivate.org/api/"
},
BTG: {
fork: 491407,
name: "Bitcoin Gold",
version: 2,
pubKeyHash: Buffer.from("26", 'hex'),
scriptHash: Buffer.from("17", 'hex'),
signtype: 0x41,
signid: 0x41 | (79 << 8),
bip143: true,
insightApi: "https://explorer.bitcoingold.org/insight-api/"
},
BTX: {
fork: 492820,
name: "BitCore",
pubKeyHash: Buffer.from("03", 'hex'),
scriptHash: Buffer.from("7D", 'hex'),
signtype: 0x01,
signid: 0x01,
insightApi: "https://insight.bitcore.cc/api/"
},
LBTC: {
fork: 499999,
name: "Lightning Bitcoin",
signtype: 0x01,
signid: 0x01,
//utxoApi: lbtcQueryUtxo
},
SBTC: {
fork: 498888,
name: "Super Bitcoin",
signtype: 0x41,
signid: 0x41,
extraBytes: Buffer.from("0473627463", 'hex'),
insightApi: "http://block.superbtc.org/insight-api/"
},
BSV: {
forkedCoin: 'BCH',
fork: 478558,
name: "Bitcoin SV",
bip143: true,
signtype: 0x41,
signid: 0x41,
requiresAddressConversion: true,
insightApi: "https://bchsvexplorer.com/api/",
blockchair: "bitcoin-sv"
},
ABC: {
forkedCoin: 'BCH',
fork: 661648,
name: "Bitcoin ABC",
bip143: true,
signtype: 0x41,
signid: 0x41,
blockchair: "bitcoin-abc"
}
/*
BTC2: {
fork: 507850,
name "Bitcoin2"
}
B2X: {
fork: 501451,
name: "Segwit2X",
signtype: 0x31, // 0x31 = 49
signid: 0x62, // 0x31<<1
isB2X: true,
// signtype: 0,
// signid: 0x31 << 8, // 0x31 49
insightApi: "https://explorer.b2x-segwit.io/b2x-insight-api/"
},
BTW: {
fork: 499777,
name: "Bitcoin World"
},
BTC2: {
fork: 507850,
name: "Bitcoin 2" // https://www.bitc2.org/
},
BTF: {
fork: 500000,
name: "Bitcoin Faith"
},
BTSQ: {
fork: 506066,
name: "Bitcoin Community"
},
UBTC: {
fork: 498777,
name: "United Bitcoin" // Explorer non funzionante, e non insight
},
WBTC: {
fork: 503888,
name: "World Bitcoin"
},
BPA: {
fork: 501888,
name: "Bitcoin Pizza" // ANDATO
},
BTN: {
fork: 501000,
name: "Bitcoin New"
},
BTH: {
fork: 498848,
name: "Bitcoin Hot"
},
BTT: {
fork: 501118,
name: "Bitcoin Top"
},
BTP: {
fork: 499345,
name: "Bitcoin Pay"
},
BCK: {
fork: 499999,
name: "Bitcoin King"
},
CDY: {
fork: 512666,
name: "Bitcoin Candy",
forkOfCash: true
},
// ANDATI
BTV: {
fork: 505050,
name: "Bitcoin Vote",
signtype: 0x41,
signid: 0x41 | (50 << 8),
insightApi: "https://block.bitvote.one/insight-api/"
},
*/
}
async function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function myJsonFetch(url, options) {
const fetch = isNode ? nodeFetch : window.fetch
if (!options)
options = {}
//options['no-cors'] = true
//options['mode'] = 'no-cors'
if (!options.timeout)
options.timeout = 30000
//if (options.doDebug)
console.log("isNode: " + isNode + " JSONFETCH " + url)
return fetch(url, options).catch(err => {
console.error('myJsonFetch err: ' + err.message)
}).catch(err => {
console.log("Fetch error", err)
return null
}).then(res => {
if (!res || !res.ok) {
console.log(`Invalid fetch response from ${url} status: ${res.status} ${res.statusText}`)
return null
}
return res.json()
}).catch(err => {
console.log("JSON conversion error", err)
return null
}).then(json => {
if (options.doDebug)
console.log("json: ", json)
return json
})
}
async function loadJsonUrl(url, params, options) {
if (!params)
params = {}
if (!params.timeout)
params.timeout = 30000
if (!options)
options = {}
if (options.doDebug)
console.log("Loading JSON from url: " + url)
// params['no-cors'] = true
// params['mode'] = 'no-cors'
try {
const res = await fetch(url, params)
if (options.doDebug)
console.log("api result:", res)
if (res.status !== 200)
if (!options || !options.okErrorCodes || !options.okErrorCodes.includes(res.status)) {
console.log("URL returned error " + res.status + ": " + JSON.stringify(res))
return null
}
return res.json()
} catch (ex) {
console.log("Unable to read from " + url + JSON.stringify(ex))
return null
}
}
function varSliceSize(someScript) {
const length = someScript.length
return varuint.encodingLength(length) + length
}
function writeUInt64LE(buffer, value, offset) {
//verifuint(value, 0x001fffffffffffff)
buffer.writeInt32LE(value & -1, offset)
buffer.writeUInt32LE(Math.floor(value / 0x100000000), offset + 4)
return offset + 8
}
function deriveAddressKeys(melis, account, unspents) {
unspents.forEach(u => {
const aa = u.aa
u.key = melis.deriveAddressKey(account.num, aa.chain, aa.hdindex)
})
}
// Explorers for unspents:
// BTCP: http://explorer.btcprivate.org/api/addr/b1Cg6wsJaaDACjnoLGmmm75bAYCEzN1GTvF/utxo
// BTG : https://explorer.bitcoingold.org/insight-api/addr/GVN41nnJW52MgNYvy4X1P4FTRrBXNNyb6V/utxo
// SBTC: http://block.superbtc.org/insight-api/addr/16xi7CHukkBndzVj4R7knquy3YatXgmVG4/utxo
// BCD : http://explorer.btcd.io/#/address?loading=true&address=19XcSCwrSFDPSRcCvmBu4cVV7gnv75nwQL
const fetchOptions = {
headers: {
"user-agent": "dev tool"
}
}
// https://api.lbtc.io/gettxinfo?param=a4a39ff43ba9d82216668ef4550c5e6818ae880a697e3308727f4abeadad03fb
async function lbtcQueryUtxo(addrs, options) {
const baseAddrUrl = "https://api.lbtc.io/"
const goodOnes = []
// https://api.lbtc.io/getaddressbalance?param=1NwbkGPq8rEWA9V6YiWWmpbEBxL94voj97
// https://api.lbtc.io/v2/gettxbyaddr?param=1HPaSweSRNizErABfPmKPp5fVw66omr9oP
// Prima controlliamo quali indirizzi hanno ancora balance
if (options && options.doDebug) {
fetchOptions.doDebug = true
console.log("Checking " + addrs.length + " addresses")
}
for (let i = 0; i < addrs.length; i++) {
const address = addrs[i]
const url = baseAddrUrl + "getaddressbalance?param=" + address
console.log("Loading " + url)
const addrStats = await myJsonFetch(url, fetchOptions)
if (!addrStats) {
console.log("Invalid response from LBTC")
return []
}
//await sleep(1100)
if (addrStats.result > 1)
goodOnes.push({
address: address,
//txCount: data.tx_count,
balance: addrStats.result
})
}
let tot = 0
goodOnes.forEach(a => {
console.log(a.address.padStart(35) + (" " + a.balance).padStart(10))
tot += a.balance
})
console.log("Total: " + tot + " = " + (tot / 100000000) + " LBTC")
return []
// Poi prendiamo tutte le transazioni su quell'indirizzo?
//https://api.lbtc.io/v2/gettxbyaddr?param=1HPaSweSRNizErABfPmKPp5fVw66omr9oP
}
async function bcdQueryUtxo(addrs, options) {
const baseAddrUrl = "http://20.184.13.116/v2/addr/"
const goodOnes = []
// Prima controlliamo quali indirizzi hanno ancora balance
if (options && options.doDebug) {
fetchOptions.doDebug = true
console.log("Checking " + addrs.length + " addresses")
}
for (let i = 0; i < addrs.length; i++) {
const address = addrs[i]
const url = baseAddrUrl + address
console.log("Loading " + url)
const addrStats = await myJsonFetch(url, fetchOptions)
if (!addrStats) {
console.log("Invalid response from BCD explorer")
return []
}
const data = addrStats.data
//console.log(data)
await sleep(1100)
if (!data) {
console.log("Unable to load info for " + address)
continue
}
if (data.balance)
goodOnes.push({
address: address,
txCount: data.tx_count,
balance: data.balance
})
}
// Poi cerchiamo gli unspent relativi
const unspents = []
const pageSize = 50 // max is 50
console.log("# BCD addresses with balance: " + goodOnes.length)
for (let i = 0; i < goodOnes.length; i++) {
const data = goodOnes[i]
const address = data.address
const basePageUrl = baseAddrUrl + address + "/history?verbose=3&pagesize=" + pageSize + "&page="
let page = 0
while (page * pageSize < data.txCount) {
const url = basePageUrl + (page + 1)
console.log("Loading " + url)
const info = await myJsonFetch(url, fetchOptions)
if (!info || !info.data || !info.data.list) {
console.log("Unable to load BCD info for " + address)
continue
}
await sleep(1000)
const txData = info.data
const list = txData.list
//console.log("TX list size: " + list.length, list)
list.forEach(l => {
const item = l[0]
//console.log("Checking item ", item)
item.outputs.forEach(out => {
//console.log("Checking out", out)
if (out.addresses[0] === address && out.spent_n === -1 && out.value > 10) {
const unspent = {
address: address,
txid: item.txid,
vout: out.n,
satoshis: out.value
}
console.log("Unspent added:", unspent)
unspents.push(unspent)
}
})
})
page++
}
}
return unspents
}
const BLOCKCHAIR_BASE_URL = "https://api.blockchair.com/"
async function blockchairQueryUtxo(coinName, addrs, options) {
const maxQuerySize = 100
const unspents = []
let fromIndex = 0
if (!options)
options = {}
options.okErrorCodes = [404]
while (fromIndex < addrs.length) {
const slice = addrs.slice(fromIndex, fromIndex + maxQuerySize)
fromIndex += maxQuerySize
const url = BLOCKCHAIR_BASE_URL + coinName + "/dashboards/addresses/" + slice.join(',') + "?limit=1000"
console.log("Querying " + slice.length + " addrs in batch mode from blockchair.com. URL len: " + url.length)
const res = await loadJsonUrl(url, {}, options)
if (!res)
return null
if (!res.data)
continue
const utxo = res.data.utxo
utxo.forEach(o => {
const unspent = {
txid: o.transaction_hash,
vout: o.index,
satoshis: o.value,
address: o.address,
height: o.block_id
}
unspents.push(unspent)
})
if (fromIndex < addrs.length)
await sleep(2000)
}
return unspents
}
/*
Res: array di
{
address: "GK18bp4UzC6wqYKKNLkaJ3hzQazTc3TWBw",
txid: "c023f27a94e218c96302d2d7652b4cf57a54fa921f9c945c802c661c9ebf6dc4",
vout: 0,
scriptPubKey: "76a9140cb60a52559620e5de9a297612d49f55f7fd14ea88ac",
amount: 0.73881797,
satoshis: 73881797,
confirmations: 0,
ts: 1611588708
}
*/
async function insightQueryUtxo(baseApi, addrs, options) {
const maxPerQuery = 50
let res = []
let todo = addrs
while (todo.length > 0) {
let slice = todo
if (todo.length > maxPerQuery) {
slice = todo.slice(0, maxPerQuery)
todo = todo.slice(maxPerQuery)
} else {
todo = []
}
const url = baseApi + "addrs/" + slice.join(',') + "/utxo?noCache=1"
const x = await myJsonFetch(url, options)
if (!x)
console.log("Invalid response for insight api: " + url)
else
res = res.concat(x)
}
return res
}
async function insightBroadcastTx(baseApi, tx) {
const url = baseApi + "tx/send"
options = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
rawtx: tx
})
}
console.log("POSTING TO " + url, options)
return nodeFetch(url, options).catch(err => {
console.error('insightBroadcastTx err: ' + err.message)
}).then(res => {
console.log("SEND TX RES status:" + res.status + " " + res.statusText)
return res.json()
})
}
async function btcComGetTx(hash) {
const url = "https://chain.api.btc.com/v3/tx/" + hash.toLowerCase() + "?verbose=3"
return myJsonFetch(url)
}
function toScriptSignature(sig, hashType) {
const hashTypeBuffer = Buffer.alloc(1)
hashTypeBuffer.writeUInt8(hashType, 0)
return Buffer.concat([sig.toDER(), hashTypeBuffer])
}
class ForkClaimer {
constructor(claimedCoin) {
this.claimedCoin = claimedCoin
if (!FORKS[claimedCoin])
throw "Unsupported coin: " + claimedCoin
this.forkInfo = FORKS[this.claimedCoin]
if (!this.forkInfo.pubKeyHash)
this.forkInfo.pubKeyHash = Buffer.from("00", 'hex')
if (!this.forkInfo.scriptHash)
this.forkInfo.scriptHash = Buffer.from("05", 'hex')
if (!this.forkInfo.version)
this.forkInfo.version = 1
if (!this.forkInfo.coinratio)
this.forkInfo.coinratio = 1
}
static getMelis() { return MELIS }
static getCoins() {
return Object.keys(FORKS).filter(c => c !== 'BSV' && c !== 'ABC')
}
static accountsForPrinting(accounts) {
let res = "#Accounts: " + Object.keys(accounts).length
Object.keys(accounts).forEach(pubId => {
const a = accounts[pubId]
const name = a.meta ? a.meta.name : "[no name]"
res += "\n" + pubId + " " + a.coin + " " + a.type + " " + name
})
return res
}
getCoin() {
return this.claimedCoin
}
getCoinRatio() {
return this.forkInfo.coinratio
}
toString() {
return "ForkClaimer[" + this.claimedCoin + "]"
}
getForkHeight() {
return this.forkInfo.fork
}
supportsBroadcastTx() {
const forkInfo = this.forkInfo
return !!forkInfo.insightApi
}
async broadcastTx(tx) {
const insightApi = this.forkInfo.insightApi
const res = await insightBroadcastTx(insightApi, tx)
return res.txid
}
supportsUtxoQuery() {
const forkInfo = this.forkInfo
return !!forkInfo.blockchair || !!forkInfo.insightApi || !!forkInfo.utxoApi
}
async queryUtxos(addrs, options) {
const blockchair = this.forkInfo.blockchair
const insightApi = this.forkInfo.insightApi
const utxoApi = this.forkInfo.utxoApi
if (blockchair) {
const res = await blockchairQueryUtxo(blockchair, addrs, options)
return res.filter(u => u.satoshis > this.forkInfo.coinratio)
} else if (insightApi) {
const res = await insightQueryUtxo(insightApi, addrs, options)
if (Array.isArray(res)) {
res.forEach(o => {
if (!o.satoshis)
o.satoshis = Number.parseInt((o.amount * 100000000).toFixed(0))
})
}
return res.filter(u => u.satoshis > this.forkInfo.coinratio)
} else if (utxoApi)
return utxoApi(addrs, options)
else
throw ("No UTXO provider for " + this.forkInfo.name)
}
async getPossibleUnspents(melis, account) {
return await melis.getUnspentsAtBlock(account, this.getForkHeight())
}
convertToCoinAddress(btcAddress) {
if (!btcAddress.match('^[13].+'))
return btcAddress
//console.log("Request to convert " + btcAddress + " to format " + this.forkInfo.name + " format")
const {
version,
hash
} = Bitcoin.address.fromBase58Check(btcAddress)
//console.log("Address " + btcAddress + " version: " + version + " hash: " + hash.toString('hex'))
let prefix
if (version == Bitcoin.networks.bitcoin.scriptHash) {
prefix = this.forkInfo.scriptHash
} else if (version == Bitcoin.networks.bitcoin.pubKeyHash) {
prefix = this.forkInfo.pubKeyHash
} else
throw "Unexpected address version: " + version
const payload = Buffer.allocUnsafe(prefix.length + hash.length)
prefix.copy(payload, 0)
hash.copy(payload, prefix.length)
return bs58check.encode(payload)
}
decodeCoinAddress(coinAddress) {
const decoded = bs58check.decode(coinAddress)
console.log(coinAddress + " decoded:" + decoded.toString('hex'))
const pubKeyHash = this.forkInfo.pubKeyHash
if (!pubKeyHash.equals(decoded.slice(0, pubKeyHash.length)))
throw "Coin address is not pubKeyHash: " + coinAddress
return decoded.slice(pubKeyHash.length)
}
async findUnspentTxOuts(txouts, doDebug) {
const self = this
const infos = {}
const addrs = []
txouts.forEach(u => {
//console.log("Working on ", u)
const btcAddress = u.aa.address
const coinAddress = this.convertToCoinAddress(btcAddress)
if (doDebug)
console.log("Checking " + btcAddress + " -> " + coinAddress + " of unspent " + u.tx + " / " + u.n)
const keyAddress = coinAddress
if (!addrs.includes(keyAddress)) {
addrs.push(keyAddress)
infos[keyAddress] = {
coinAddress,
btcAddress,
key: u.key,
aa: u.aa,
origTx: u.tx,
origVout: u.n,
redeemScript: u.redeemScript,
totalAmount: u.amount,
numTxs: 1
}
} else {
infos[keyAddress].totalAmount += u.amount
infos[keyAddress].numTxs++
}
})
if (doDebug) {
console.log("Unique found addresses: " + addrs.length)
addrs.forEach(address => {
console.log(address + " #txs: " + infos[address].numTxs + " total: " + infos[address].totalAmount)
})
}
if (!addrs.length)
return { unspents: [], infos: {} }
if (self.supportsUtxoQuery())
return await self.queryUtxos(addrs, {
doDebug
}).then(nativeUnspents => {
return { unspents: nativeUnspents, infos }
})
else {
// TODO: Usare un explorer per verificare se effettivamente unspent
const unspents = []
txouts.forEach(u => {
unspents.push({
address: u.aa.address,
txid: u.tx,
vout: u.n,
satoshis: u.amount
})
})
return { unspents, infos }
}
}
hashForSignature_legacy(ins, inIndex, inScript, outs, hashType) {
const buffer = Buffer.allocUnsafe(2000)
let offset = 0
function writeSlice(slice) {
offset += slice.copy(buffer, offset)
}
function writeUInt8(i) {
offset = buffer.writeUInt8(i, offset)
}
function writeUInt32(i) {
offset = buffer.writeUInt32LE(i, offset)
}
function writeInt32(i) {
offset = buffer.writeInt32LE(i, offset)
}
function writeUInt64(i) {
offset = writeUInt64LE(buffer, i, offset)
}
function writeVarInt(i) {
varuint.encode(i, buffer, offset)
offset += varuint.encode.bytes
}
function writeVarSlice(slice) {
writeVarInt(slice.length);
writeSlice(slice)
}
//function writeVector(vector) { writeVarInt(vector.length); vector.forEach(writeVarSlice) }
// Version
writeInt32(this.forkInfo.version)
// bcdGarbage, if any
if (this.forkInfo.bcdGarbage)
writeSlice(this.forkInfo.bcdGarbage)
// Number of inputs
writeVarInt(ins.length)
for (let i = 0; i < ins.length; i++) {
const txIn = ins[i]
writeSlice(txIn.hash)
writeUInt32(txIn.index)
if (i == inIndex)
writeVarSlice(inScript)
writeUInt32(Bitcoin.Transaction.DEFAULT_SEQUENCE)
}
// ins.forEach(function (txIn) {
// writeSlice(txIn.hash)
// writeUInt32(txIn.index)
// writeVarSlice(txIn.script)
// writeUInt32(txIn.sequence)
// })
writeVarInt(outs.length)
outs.forEach(out => {
if (!out.valueBuffer) {
writeUInt64(out.value)
} else {
writeSlice(out.valueBuffer)
}
writeVarSlice(out.script)
})
// LockTime
writeUInt32(0)
// hash type
writeUInt32(this.forkInfo.signid)
//writeUInt32(this.forkInfo.signid | hashType)
// extrabytes, if any
if (this.forkInfo.extraBytes)
writeSlice(this.forkInfo.extraBytes)
console.log("toHash offset: " + offset + " " + buffer.slice(0, offset).toString('hex'))
return bcrypto.hash256(buffer.slice(0, offset))
}
hashForSignature_BIP143(ins, inputToSign, inScript, outs, hashType) {
const lockTime = 0
var tbuffer, toffset
function writeSlice(slice) {
toffset += slice.copy(tbuffer, toffset)
}
function writeUInt32(i) {
toffset = tbuffer.writeUInt32LE(i, toffset)
}
function writeUInt64(i) {
toffset = writeUInt64LE(tbuffer, i, toffset)
}
function writeVarInt(i) {
varuint.encode(i, tbuffer, toffset)
toffset += varuint.encode.bytes
}
function writeVarSlice(slice) {
writeVarInt(slice.length);
writeSlice(slice)
}
// hashPrevOuts
tbuffer = Buffer.allocUnsafe(36 * ins.length)
toffset = 0
ins.forEach(txIn => {
writeSlice(txIn.hash)
writeUInt32(txIn.index)
})
const hashPrevouts = bcrypto.hash256(tbuffer)
// hashSequence
tbuffer = Buffer.allocUnsafe(4 * ins.length)
toffset = 0
ins.forEach(txIn => writeUInt32(txIn.sequence))
const hashSequence = bcrypto.hash256(tbuffer)
// hashOutputs
const txOutsSize = outs.reduce(function (sum, out) {
return sum + 8 + varSliceSize(out.script)
}, 0)
tbuffer = Buffer.allocUnsafe(txOutsSize)
toffset = 0
outs.forEach(out => {
writeUInt64(out.value)
writeVarSlice(out.script)
//console.log("[REMOVEME] out amount: " + out.value + " script: " + out.script.toString('hex'));
})
//console.log("[REMOVEME] outputsToHash: " + tbuffer.toString('hex'));
const hashOutputs = bcrypto.hash256(tbuffer)
tbuffer = Buffer.allocUnsafe(156 + varSliceSize(inScript))
toffset = 0
const input = ins[inputToSign]
writeUInt32(this.forkInfo.version)
writeSlice(hashPrevouts)
//console.log("[REMOVEME] getPrevoutHash: " + hashPrevouts.toString('hex'));
writeSlice(hashSequence)
//console.log("[REMOVEME] getSequenceHash: " + hashSequence.toString('hex'));
writeSlice(input.hash)
writeUInt32(input.index)
//console.log("[REMOVEME] serializedInput: " + input.hash.toString('hex') + " " + input.index);
writeVarSlice(inScript)
//console.log("[REMOVEME] inputScriptBytes: " + inScript.toString('hex'));
writeUInt64(input.value)
writeUInt32(input.sequence)
writeSlice(hashOutputs)
//console.log("[REMOVEME] getOutputsHash: " + hashOutputs.toString('hex'));
writeUInt32(lockTime)
writeUInt32(this.forkInfo.signid)
//writeUInt32(this.forkInfo.signid | hashType)
console.log("input #" + inputToSign + "BIP143 hashForSig: " + tbuffer.toString('hex'))
return bcrypto.hash256(tbuffer)
}
hashForSignature(ins, inputToSign, inScript, outs, hashType) {
if (this.forkInfo.bip143)
return this.hashForSignature_BIP143(ins, inputToSign, inScript, outs, hashType)
else
return this.hashForSignature_legacy(ins, inputToSign, inScript, outs, hashType)
}
// TODO: Needs to check a lot more things now
verifyClaimProposal(claim, unspent, targetAddress) {
const unspents = claim.unspents
const recipients = claim.recipients
const inputAmount = unspent.amount
const outputAmount = recipients.reduce(function (sum, output) {
return sum + output.amount
}, 0)
console.log("[analyze] unspent:", unspent)
console.log("[analyze] unspents:", unspents)
console.log("[analyze] recipient:", recipients)
const networkFees = inputAmount - outputAmount
const hasTargetAddress = recipients.some(out => out.address === targetAddress)
let unspentsMatch = unspents.length === 1
const u = unspents[0]
unspentsMatch = unspentsMatch && (u.hash === unspent.hash && u.n === unspent.n)
console.log("[verifyClaimProposal] HasTargetAddres: " + hasTargetAddress + " fees: " + networkFees + " unspentsMatch: " + unspentsMatch)
return unspentsMatch && hasTargetAddress
}
prepareSignaturesForClaim(melis, claim) {
const tx = new Bitcoin.Transaction()
const unspents = claim.unspents
const recipients = claim.recipients
//console.log("[prepareSignaturesForClaim] unspents:", unspents)
let inputAmount = 0
unspents.forEach(u => {
tx.addInput(Buffer.from(u.hash, 'hex').reverse(), u.n, Bitcoin.Transaction.DEFAULT_SEQUENCE)
inputAmount += u.amount
})
//console.log("[prepareSignaturesForClaim] recipients:", recipients)
let outputAmount = 0
recipients.forEach(recipient => {
const script = Buffer.from(recipient.outScript, 'base64')
//console.log("[REMOVEME] base64 " + recipient.outScript + " -> " + script.toString('hex'))
tx.addOutput(script, recipient.amount)
outputAmount += recipient.amount
})
const networkFees = inputAmount - outputAmount
console.log("networkFees: " + networkFees)
const ins = []
unspents.forEach(u => ins.push({
hash: Buffer.from(u.hash, 'hex').reverse(),
index: u.n,
value: u.amount,
sequence: Bitcoin.Transaction.DEFAULT_SEQUENCE
}))
const account = claim.account
console.log("# of inputs to sign: " + unspents.length + " account:", account)
const signatures = []
for (let i = 0; i < unspents.length; i++) {
const u = unspents[i]
const aa = u.aa
if (aa) {
console.log("Preparing signature for input#:" + i, u)
const inputScript = Buffer.from(u.inputScript, 'base64')
//console.log("Input Script: " + inputScript.toString('hex'))
const hashForSignature = this.hashForSignature(ins, i, inputScript, tx.outs, Bitcoin.Transaction.SIGHASH_ALL)
//console.log("input #" + i + " hashForSig: ", hashForSignature.toString('hex'))
const key = melis.deriveAddressKey(account.num, aa.chain, aa.hdindex)
const signature = key.sign(hashForSignature)
signatures.push(signature.toDER().toString('hex'))
}
}
//console.log("TX:", tx)
return {
id: claim.id,
signatures
}
}
async scanFork(melis, accounts, doDebug) {
const claimer = this
const result = []
for (let i = 0; i < accounts.length; i++) {
const account = accounts[i]
console.log("scanning account " + account.pubId + " type: " + account.type + " meta: " + JSON.stringify(account.meta))
if (account.type !== '2' && account.type !== 'H') {
console.log("Skipping account " + account.pubId + " of unsupported type " + account.type)
continue
}
const possibleUnspents = await claimer.getPossibleUnspents(melis, account)
const candidates = possibleUnspents.unspents
console.log("scanning " + account.pubId + " " + account.type + " with #candidates: " + candidates.length)
if (!candidates || !candidates.length)
continue
deriveAddressKeys(melis, account, candidates)
const res = await claimer.findUnspentTxOuts(candidates, doDebug)
const unspents = res.unspents
const infos = res.infos
if (!unspents || !unspents.length)
continue
const totSatoshis = unspents.reduce((acc, o) => acc + parseInt(o.satoshis), 0)
const claimable = []
unspents.forEach(unspent => {
const info = infos[unspent.address]
//console.log("info:", info)
//console.log("unspent:", unspent)
claimable.push({
hash: unspent.txid,
n: unspent.vout,
amount: unspent.satoshis,
aa: info.aa,
address: info.btcAddress
})
})
result.push({
claimer,
claimable,
account,
unspents,
infos,
totSatoshis,
numCandidates: candidates.length
})
console.log("Account #" + i + ": " + account.pubId + " type: " + account.type + " #candidates: " + candidates.length +
" #Unspents: " + unspents.length + " total satoshis: " + totSatoshis + " " + (totSatoshis / 100000000) + " " + claimer.getCoin())
}
return result
}
async redeem(melis, params) {
const account = params.account
const targetCoin = params.targetCoin
const unspents = params.unspents
const targetAddress = params.targetAddress
const claimProposal = await melis.prepareUnspentForkClaim({
account,
targetCoin,
targetAddress,
unspents
})
console.log("Claim proposal: ", claimProposal)
// TODO: Riabilitare il controllo?
// const unspent = unspents[0]
// const verified = this.verifyClaimProposal(claimProposal, unspent, targetAddress)
// if (!verified)
// throw "Claim does not meet minimum sanity checks"
// //if (!params.ignoreVerification)
// // doExit()
const claimSignatures = this.prepareSignaturesForClaim(melis, claimProposal)
const broadcastResult = await melis.submitForkClaim(claimSignatures.id, claimSignatures.signatures)
//const broadcastResult = await submitClaim(claimSignatures)
console.log("Transaction submitted correctly:", broadcastResult)
return broadcastResult.hash
}
async findExistingMigratedAccount(melis, claimedCoin) {
const accounts = melis.peekAccounts()
//console.log("PEEKED ACCOUNTS:\n", ForkClaimer.accountsForPrinting(accounts))
const pubId = Object.keys(accounts).find(pubId =>
accounts[pubId].coin === claimedCoin &&
accounts[pubId].meta && accounts[pubId].meta.migrationInfo === claimedCoin + MIGRATED_ACCOUNT_NAME_SUFFIX
)
console.log("[findExistingMigratedAccount] existing " + claimedCoin + " account: ", pubId)
if (pubId)
return pubId
const creationOptions = {
coin: claimedCoin,
type: MELIS.C.TYPE_PLAIN_HD,
meta: {
name: "Claimed " + claimedCoin,
migrationInfo: claimedCoin + MIGRATED_ACCOUNT_NAME_SUFFIX
}
}
console.log("Creating account with options:", creationOptions)
const res = await melis.accountCreate(creationOptions)
console.log("Created " + claimedCoin + " account: ", res)
return res.account.pubId
}
async bchForkScan(melis, accounts, doDebug, dustLimitPar) {
const bchDriver = melis.getCoinDriver('BCH')
const maxSliceSize = 20
const res = {}
const dustLimit = dustLimitPar ? dustLimitPar : 5000
for (let i = 0; i < accounts.length; i++) {
const account = accounts[i]
if (account.coin !== 'BCH') {
console.log("Skipping account " + account.pubId + " " + account.coin)
continue
}
if (account.type !== '2' && account.type !== 'H') {
console.log("Skipping account " + account.pubId + " of unsupported type " + account.type)
continue
}
console.log("scanning account " + account.pubId + " type: " + account.type + " meta: " + JSON.stringify(account.meta))
let numCheckedAddrs = 0
let slicePage = 0
let unspents = []
do {
const slice = await melis.addressesGet(account, { page: slicePage++, size: maxSliceSize })
// console.log("slicePage: " + slicePage + " maxSliceSize: " + maxSliceSize + " slice result: ", slice)
if (!slice.list) {
console.log("No addresses on melis for account " + account.pubId)
break
}
const addrs = slice.list.map(aa => this.requiresAddressConversion ? bchDriver.toLegacyAddress(aa.address) : aa.address)
if (doDebug)
console.log("slicePage: " + slicePage + " maxSliceSize: " + maxSliceSize + " #Addresses: " + addrs.length + "\n", addrs)
numCheckedAddrs += addrs.length
const utxos = await this.queryUtxos(addrs, { doDebug })
if (doDebug)
console.log("utxos:", utxos)
if (utxos.length)
unspents = unspents.concat(utxos)
if (!slice.hasNext)
break
} while (true)
const filteredUnspents = unspents.filter(u => u.satoshis >= dustLimit)
console.log("#unspents found: " + unspents.length + " #aboveDust(" + dustLimit + "): " + filteredUnspents.length
+ " #checkedAddrs: " + numCheckedAddrs + " slices used: " + slicePage + " (sliceSize:" + maxSliceSize + ")")
if (unspents.length)
res[account.pubId] = {
utxos: filteredUnspents,
account
}
}
return res
}
async redeemBchFork(melis, params) {
const claimedCoin = this.claimedCoin
const coinDriver = melis.getCoinDriver(claimedCoin)
const account = params.account
const utxos = params.utxos
const doDebug = params.doDebug
let targetAddress = params.targetAddress
if (doDebug)
console.log("[" + claimedCoin + " CLAIM] utxos: ", utxos)
if (!account)
throw ("Missing account in parameters")
if (!utxos || !utxos.length)
throw ("Missing utxos in parameters")
if (!targetAddress) {
const pubId = await this.findExistingMigratedAccount(melis, claimedCoin)
const aa = await melis.getPaymentAddressViaRest(pubId, { info: claimedCoin + ' Redeemed coins' })
if (doDebug)
console.log("Created new aa: ", aa)
targetAddress = this.requiresAddressConversion ? coinDriver.toLegacyAddress(aa.address) : aa.address
}
console.log("Target " + claimedCoin + " address: ", targetAddress)
const unspents = utxos.map(u => ({
hash: u.txid,
n: u.vout,
address: u.address
}))
const res = await this.redeem(melis, {
account,
targetCoin: claimedCoin,
targetAddress,
unspents
})
return res
}
}
module.exports = ForkClaimer