Skip to content

Instantly share code, notes, and snippets.

@gabmontes
Last active August 21, 2020 14:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save gabmontes/18db8eed844867dd58d3d2df92da8a14 to your computer and use it in GitHub Desktop.
Save gabmontes/18db8eed844867dd58d3d2df92da8a14 to your computer and use it in GitHub Desktop.
Analize a set of bitcoin addresses and find other related addresses
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)
{
"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