Last active
August 21, 2020 14:38
-
-
Save gabmontes/18db8eed844867dd58d3d2df92da8a14 to your computer and use it in GitHub Desktop.
Analize a set of bitcoin addresses and find other related addresses
This file contains 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
const got = require('got') | |
const { | |
concat, | |
difference, | |
flatten, | |
identity, | |
map, | |
memoize, | |
padEnd, | |
sortBy, | |
sum, | |
uniq, | |
uniqBy, | |
without | |
} = require('lodash') | |
const pRetry = require('p-retry') | |
const addresses = uniq([ | |
'1address1', | |
'1address2' | |
// other addresses | |
]) | |
const INSIGHT_API = 'https://insight.bitpay.com/api' | |
const getAddress = memoize(function (address) { | |
console.log('Getting address info', address) | |
return got(`${INSIGHT_API}/addr/${address}`, { json: true }) | |
.then(res => res.body) | |
}) | |
const getTx = memoize(function (txid) { | |
console.log('Getting tx info', txid) | |
return pRetry( | |
() => got(`${INSIGHT_API}/tx/${txid}`, { json: true }), | |
{ retries: 20 } | |
) | |
.then(res => res.body) | |
}) | |
function btcToSat (btc) { | |
return btc * 100000000 | |
} | |
function btcStrToSat (str) { | |
const [int, dec] = str.split('.') | |
return btcToSat(Number.parseInt(int, 10)) + | |
Number.parseInt(padEnd(dec, 8, '0'), 10) | |
} | |
function processAddress (state) { | |
const address = state.addresses[0] | |
const rest = state.addresses.slice(1) | |
return getAddress(address) | |
.then(function (info) { | |
return Promise.all( | |
info.transactions.map(tx => getTx(tx) | |
.then(function (tx) { | |
const txid = tx.txid | |
const time = tx.time | |
const fees = btcToSat(tx.fees) | |
const vins = tx.vin.map(vin => ({ | |
address: vin.addr, | |
value: -vin.valueSat, | |
fees, | |
txid, | |
time, | |
timeStr: new Date(time * 1000).toISOString(), | |
type: 'in' | |
})) | |
const vouts = tx.vout.map(vout => ({ | |
address: vout.scriptPubKey.addresses | |
? vout.scriptPubKey.addresses[0] | |
: '', | |
value: btcStrToSat(vout.value), | |
fees, | |
txid, | |
time, | |
timeStr: new Date(time * 1000).toISOString(), | |
type: 'out' | |
})).filter(identity) | |
return { | |
othersOwned: map(vins, 'address').includes(address) | |
? without(map(vins, 'address'), address) | |
: [], | |
relationships: concat(vins, vouts) | |
} | |
}) | |
) | |
) | |
.then(results => ({ | |
othersOwned: uniq(flatten(map(results, 'othersOwned'))), | |
processed: { | |
address, | |
balance: info.balanceSat, | |
relationships: flatten(map(results, 'relationships')) | |
} | |
})) | |
}) | |
.then(({ othersOwned, processed }) => ({ | |
addresses: uniq(concat(rest, difference(othersOwned, state.processed))), | |
results: concat(state.results, processed), | |
processed: concat(state.processed, address) | |
})) | |
} | |
function processAddresses (state) { | |
return state.addresses.length | |
? processAddress(state).then(processAddresses) | |
: state | |
} | |
const satToBtc = sat => sat / 100000000 | |
const getBalance = (results, address) => | |
results.find(a => a.address === address).balance | |
function postProcessing ({ results, processed }) { | |
const managed = processed.sort() | |
.map(address => ({ address, balance: getBalance(results, address) })) | |
const balance = sum(map(results, 'balance')) | |
const inbound = difference(uniq(map( | |
flatten(map(results, 'relationships')).filter(r => r.type === 'in'), | |
'address' | |
)), processed).sort() | |
const outbound = difference(uniq(map( | |
flatten(map(results, 'relationships')).filter(r => r.type === 'out'), | |
'address' | |
)), processed).sort() | |
const incoming = sortBy(uniqBy(flatten(inbound.map(inb => results | |
.filter(rel => rel.relationships | |
.find(leg => leg.address === inb && leg.type === 'in') | |
) | |
.map(function (addr) { | |
const txids = map(addr.relationships | |
.filter(leg => leg.address === inb && leg.type === 'in'), 'txid') | |
return addr.relationships | |
.filter(leg => leg.type === 'out' && txids.includes(leg.txid)) | |
.find(out => map(managed, 'address').includes(out.address)) | |
}) | |
)), 'txid'), 'time') | |
const outRels = flatten(results | |
.filter(r => r.relationships.find(rel => outbound.includes(rel.address))) | |
.map(r => r.relationships) | |
) | |
const outTxs = uniq(map(outRels, 'txid')) | |
.filter(txid => outRels | |
.find(r => | |
r.txid === txid && | |
r.type === 'in' && | |
map(managed, 'address').includes(r.address) | |
) | |
) | |
const outgoing = sortBy(uniqBy(outRels.filter(r => | |
outTxs.includes(r.txid) && | |
r.type === 'out' && | |
!map(managed, 'address').includes(r.address) | |
), 'address'), 'time') | |
return { | |
results, | |
managed, | |
balance, | |
incoming, | |
outgoing | |
} | |
} | |
function printResults ({ results, managed, balance, incoming, outgoing }) { | |
console.log('\nAll relationships found:') | |
console.log(JSON.stringify(results, null, 2)) | |
console.log('\nManaged addresses:') | |
console.log(managed.map(({ address, balance }) => | |
`${address}: ${satToBtc(balance)} BTC` | |
).join('\n')) | |
console.log(`\nManaged balance: ${satToBtc(balance)} BTC`) | |
console.log('\nIncoming:') | |
console.log(incoming.map(inc => | |
`${satToBtc(inc.value).toFixed(8)} BTC into ${inc.address} on ${inc.timeStr.substr(0, 10)}` | |
).join('\n')) | |
console.log('\nOutging:') | |
console.log(outgoing.map(out => | |
`${satToBtc(out.value).toFixed(8)} BTC to ${out.address} on ${out.timeStr.substr(0, 10)}` | |
).join('\n')) | |
} | |
function printError (err) { | |
console.warn('ERR', err.message, err.stack) | |
} | |
processAddresses({ addresses, results: [], processed: [] }) | |
.then(postProcessing) | |
.then(printResults) | |
.catch(printError) |
This file contains 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
{ | |
"dependencies": { | |
"got": "^8.3.0", | |
"lodash": "^4.17.5", | |
"p-retry": "^1.0.0", | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment