src/lib/dagchain/dagchain-interface.js
import { config, median, multihashFromHex } from './../../utils';
import { validateTransaction } from './../transaction.js';
import { TransactionError } from './../errors.js';
import { DAGBlock, createDAGNode, validate } from './dagblock';
import { encode, decode } from 'bs58';
import { GENESISBLOCK } from '../../params';
import { resolvePeers } from '../network/peernet';
import IPFS from 'ipfs-api';
const ipfs = new IPFS();
global.chain = global.chain || [
GENESISBLOCK
];
export const chain = (() => global.chain)();
export const mempool = (() => [])();
/**
* Get the transactions for the next Block
*
* @return {object} transactions
*/
export const nextBlockTransactions = () => {
const unspent = getUnspent(false);
return mempool.filter((transaction) => {
try {
return validateTransaction(transaction, unspent);
} catch (e) {
if (! (e instanceof TransactionError)) throw e;
}
});
};
export const getTransactions = (withMempool = true) => {
let transactions = chain.reduce((transactions, block) => transactions.concat(block.transactions), []);
if (withMempool) transactions = transactions.concat(mempool);
return transactions;
};
export const getTransactionsForAddress = address => {
return getTransactions(false).filter(tx => tx.inputs.find(i => i.address === address) ||
tx.outputs.find(o => o.address === address));
};
export const getUnspent = (withMempool = false) => {
const transactions = getTransactions(withMempool);
// Find all inputs with their tx ids
const inputs = transactions.reduce((inputs, tx) => inputs.concat(tx.inputs), []);
// Find all outputs with their tx ids
const outputs = transactions.reduce((outputs, tx) =>
outputs.concat(tx.outputs.map(output => Object.assign({}, output, {tx: tx.id}))), []);
// Figure out which outputs are unspent
const unspent = outputs.filter(output =>
typeof inputs.find(input => input.tx === output.tx && input.index === output.index && input.amount === output.amount) === 'undefined');
return unspent;
};
export const getUnspentForAddress = address => {
return getUnspent(true).filter(u => u.address === address);
};
export const getBalanceForAddress = address => {
return getUnspentForAddress(address).reduce((acc, u) => acc + u.amount , 0);
};
export const difficulty = () => {
// TODO: lower difficulty when transactionpool contain more then 500 tx ?
// TODO: raise difficulty when pool is empty
// get the last 128 blocks
const start = chain.length >= 128 ? (chain.length - 128) : 0;
const blocks = chain.slice(start, (chain.length - 1)).reverse();
const stamps = [];
for (var i = 0; i < blocks.length; i++) {
if (blocks[i + 1]) {
stamps.push(blocks[i].time - blocks[i + 1].time);
}
}
if (stamps.length === 0) {
stamps.push(10);
}
let blocksMedian = median(stamps) || 10;
const offset = blocksMedian / 10
// offset for quick recovery
if (blocksMedian < 9) {
blocksMedian -= offset;
} else if (blocksMedian > 11) {
blocksMedian += offset;
}
console.log(`Average Block Time: ${blocksMedian}`);
console.log(`Difficulty: ${10 / blocksMedian}`);
return 10000 / (10 / blocksMedian); // should result in a block every 10 seconds
};
export const transformBlock = ({multihash, data}) => {
data = JSON.parse(data.toString());
data.hash = hashFromMultihash(multihash);
return data;
};
// TODO: global peerlist
export const longestChain = () => new Promise(async (resolve, reject) => {
try {
const peers = await resolvePeers();
// transform peers into valid ipfs addresses
const addrs = peers.map(({addr, peer}) => {
return {
addr: addr.toString(),
id: peer.toB58String()
}
});
// peers.push(id);
const stats = [];
for (const { addr, id } of addrs) {
try {
await ipfs.swarm.connect(`${addr.toString()}/ipfs/${id}`);
const ref = await ipfs.name.resolve(id);
const hash = ref.replace('/ipfs/', '');
// get chain stats for every peer
const stat = await ipfs.object.stat(hash);
// push chain length & hash
stats.push({height: stat.NumLinks - 1, hash: stat.Hash});
} catch (e) {
if (e.code === 'ECONNREFUSED') {
reject(e);
}
console.log(`Ignoring ${id}`)
}
}
// reduce to longest chain
// TODO: consider using canditates for validating
// canditates.push({hash, height})
// if c.height > p.height => newCanditatesSet ...
const stat = stats.reduce((p, c) => {
if (c.height > p.height || c.height === p.height) return c;
else return p;
}, {height: 0, hash: null});
resolve(stat);
} catch (e) {
reject(e);
}
});
export const lastBlock = async () => {
const { hash, height } = await longestChain();
const { links } = await ipfs.object.get(hash); // retrieve links
// TODO: syncChain if needed
links.sort((a,b) => a.name - b.name); // sort index
// get block using its ipfs ref
return await new DAGBlock(encode(links[height].multihash));
};
export const nextBlock = async address => {
const transactions = nextBlockTransactions();
const previousBlock = chain[chain.length - 1];
return await new DAGBlock({transactions, previousBlock, address});
};
/**
* Create a new genesis block
*/
export const newGenesisDAGNode = async difficulty => {
let dagnode;
const block = {
index: 0,
prevHash: '0',
time: Math.floor(new Date().getTime() / 1000),
transactions: [],
nonce: 0
};
dagnode = await createDAGNode(block);
block.hash = dagnode.multihash.toString('hex').substring(4);
while (parseInt(block.hash.substring(0, 8), 16) >= difficulty) {
block.nonce++;
dagnode = await createDAGNode(block);
block.hash = dagnode.multihash.toString('hex').substring(4);
}
return dagnode;
}