factom
Version:
Library to build applications on the Factom blockchain
484 lines (395 loc) • 20.2 kB
JavaScript
const { FactomCli, FactomEventEmitter, FACTOM_EVENT } = require('../src/factom'),
{ FactoidBlock, AdminBlock, EntryCreditBlock, EntryBlock } = require('../src/blocks'),
mockDirectoryBlock = require('./data/directory-block.json'),
assert = require('chai').assert,
sinon = require('sinon');
const INTERVAL = 50;
const mockTransactionId = '337a32712f14c5df0b57a64bd6c321a043081688ecd4f33fd8319470da2256b1';
const getMockPendingTransaction = (txid = mockTransactionId) => [
{
transactionid: txid,
status: 'TransactionACK',
inputs: [
{
amount: 100012000,
address: '646f3e8750c550e4582eca5047546ffef89c13a175985e320232bacac81cc428',
useraddress: 'FA2jK2HcLnRdS94dEcU27rF3meoJfpUcZPSinpb7AwQvPRY6RL1Q',
},
],
outputs: [
{
amount: 100000000,
address: '27c0a471c6c9b6da3fce0af7b1119ac2cbc68723657a9e2860d83c6776bbd6ff',
useraddress: 'FA29eyMVJaZ2tbGqJ3M49gANaXMXCjgfKcJGe5mx8p4iQFCvFDAC',
},
],
ecoutputs: [],
fees: 12000,
},
];
describe('Test FactomEventEmitter', () => {
const cli = new FactomCli({
factomd: {
protocol: process.env.FACTOMD_PROTOCOL,
rejectUnauthorized: false,
path: process.env.FACTOMD_PATH,
user: process.env.FACTOMD_USER,
password: process.env.FACTOMD_PASSWORD,
host: process.env.FACTOMD_HOST,
port: process.env.FACTOMD_PORT,
},
});
// Create mocks for methods used to start polling and trigger new block height.
sinon.stub(cli, 'getDirectoryBlockHead').resolves(mockDirectoryBlock);
sinon.stub(cli, 'getHeights').resolves({ directoryBlockHeight: mockDirectoryBlock.height - 1 });
it('should add then remove a directory block listener', (done) => {
// Set short interval so the test does not timeout.
const emitter = new FactomEventEmitter(cli, { interval: INTERVAL });
const listener = (dBlock) => {
assert.isString(dBlock.keyMR);
assert.isTrue(emitter.isPolling);
assert.lengthOf(emitter.listeners('newDirectoryBlock'), 1);
emitter.removeListener(FACTOM_EVENT.newDirectoryBlock, listener);
assert.isFalse(emitter.isPolling);
assert.lengthOf(emitter.listeners('newDirectoryBlock'), 0);
done();
};
emitter.on('error', (err) => done(err));
emitter.on(FACTOM_EVENT.newDirectoryBlock, listener);
});
it('should add then remove a factoid block listener', (done) => {
const emitter = new FactomEventEmitter(cli, { interval: INTERVAL });
const listener = (fBlock) => {
assert.instanceOf(fBlock, FactoidBlock);
assert.isTrue(emitter.isPolling);
assert.lengthOf(emitter.listeners('newFactoidBlock'), 1);
emitter.removeListener('newFactoidBlock', listener);
assert.isFalse(emitter.isPolling);
assert.lengthOf(emitter.listeners('newFactoidBlock'), 0);
done();
};
emitter.on('error', (err) => done(err));
emitter.on(FACTOM_EVENT.newFactoidBlock, listener);
});
it('should add then remove an admin block listener', (done) => {
const emitter = new FactomEventEmitter(cli, { interval: INTERVAL });
const listener = (aBlock) => {
assert.instanceOf(aBlock, AdminBlock);
assert.isTrue(emitter.isPolling);
assert.lengthOf(emitter.listeners('newAdminBlock'), 1);
emitter.removeListener('newAdminBlock', listener);
assert.isFalse(emitter.isPolling);
assert.lengthOf(emitter.listeners('newAdminBlock'), 0);
done();
};
emitter.on('error', (err) => done(err));
emitter.on(FACTOM_EVENT.newAdminBlock, listener);
});
it('should add then remove an entry credit block listener', (done) => {
const emitter = new FactomEventEmitter(cli, { interval: INTERVAL });
const listener = (ecBlock) => {
assert.instanceOf(ecBlock, EntryCreditBlock);
assert.isTrue(emitter.isPolling);
assert.lengthOf(emitter.listeners('newEntryCreditBlock'), 1);
emitter.removeListener('newEntryCreditBlock', listener);
assert.isFalse(emitter.isPolling);
assert.lengthOf(emitter.listeners('newEntryCreditBlock'), 0);
done();
};
emitter.on('error', (err) => done(err));
emitter.on(FACTOM_EVENT.newEntryCreditBlock, listener);
});
it('should add then remove entry chain listener', (done) => {
const emitter = new FactomEventEmitter(cli, { interval: INTERVAL });
const chain = '3392f9df84cae30d97962641600546d7aafd3a29667d1dd356280a54c9070bcb';
const listener = (eBlock) => {
assert.instanceOf(eBlock, EntryBlock);
assert.isTrue(emitter.isPolling);
assert.lengthOf(emitter.listeners(chain), 1);
emitter.removeListener(chain, listener);
assert.isFalse(emitter.isPolling);
assert.lengthOf(emitter.listeners(chain), 0);
done();
};
emitter.on('error', (err) => done(err));
emitter.on(chain, listener);
});
it('should add then remove factoid address listener', (done) => {
const emitter = new FactomEventEmitter(cli, { interval: INTERVAL });
const address = 'FA29eyMVJaZ2tbGqJ3M49gANaXMXCjgfKcJGe5mx8p4iQFCvFDAC';
const factoidBlockRef = 'cad832bab1d83c74bff8e1092fcf70e298cd7cdf35dee1956ae8879e749195ac';
// Stub resolves directory block with specific factoidBlockRef
cli.getDirectoryBlockHead.resolves({ ...mockDirectoryBlock, factoidBlockRef });
const listener = (tx) => {
assert.strictEqual(
tx.id,
'c538ffb37d5dab837f3ea781b01bae481d8d6e77ceb0471442c1d85576a00e01'
);
assert.isTrue(emitter.isPolling);
assert.lengthOf(emitter.listeners(address), 1);
emitter.removeListener(address, listener);
assert.isFalse(emitter.isPolling);
assert.lengthOf(emitter.listeners(address), 0);
done();
};
emitter.on('error', (err) => done(err));
emitter.on(address, listener);
});
it('should add then remove factoid pending transaction listener', (done) => {
const emitter = new FactomEventEmitter(cli, { interval: INTERVAL });
const pendingTransaction = {
eventType: 'newPendingTransaction',
topic: 'FA29eyMVJaZ2tbGqJ3M49gANaXMXCjgfKcJGe5mx8p4iQFCvFDAC',
};
const tokenizedPendingTransaction =
FactomEventEmitter.getSubscriptionToken(pendingTransaction);
const mockPendingTransaction = getMockPendingTransaction();
const listener = (tx) => {
assert.isTrue(emitter.isPolling);
assert.lengthOf(emitter.listeners(tokenizedPendingTransaction), 1);
assert.strictEqual(tx.transactionid, mockPendingTransaction[0].transactionid);
emitter.removeListener(tokenizedPendingTransaction, listener);
assert.isFalse(emitter.isPolling);
assert.lengthOf(emitter.listeners(tokenizedPendingTransaction), 0);
cli.factomdApi.restore();
done();
};
emitter.on('error', (err) => done(err));
emitter.on(tokenizedPendingTransaction, listener);
sinon.stub(cli, 'factomdApi').resolves(mockPendingTransaction);
});
it('should add then remove new chain listener', (done) => {
const emitter = new FactomEventEmitter(cli, { interval: INTERVAL });
const newChain = {
chainId: '626ae1be1e36c6fd589e50c55638c7a18b551dfc6b63da30842722d448fc6db2',
keyMR: 'a4883d0e53ce79d70401a839b087c91f1611bd332e3d988a412230fddd27c22d',
};
mockDirectoryBlock.entryBlockRefs.push(newChain);
const listener = (eBlock) => {
assert.instanceOf(eBlock, EntryBlock);
assert.strictEqual(eBlock.sequenceNumber, 0);
assert.isTrue(emitter.isPolling);
assert.strictEqual(eBlock.keyMR, newChain.keyMR);
assert.lengthOf(emitter.listeners('newChain'), 1);
emitter.removeListener('newChain', listener);
assert.isFalse(emitter.isPolling);
assert.lengthOf(emitter.listeners('newChain'), 0);
done();
};
emitter.on('error', (err) => done(err));
emitter.on(FACTOM_EVENT.newChain, listener);
});
it('should stop polling after emitting once', (done) => {
const emitter = new FactomEventEmitter(cli, { interval: INTERVAL });
const listener = (dBlock) => {
assert.isString(dBlock.keyMR);
assert.isFalse(emitter.isPolling);
assert.lengthOf(emitter.listeners('newDirectoryBlock'), 0);
done();
};
emitter.on('error', (err) => done(err));
emitter.once(FACTOM_EVENT.newDirectoryBlock, listener);
});
it('should not stop polling if there are listeners of a different type still active', (done) => {
const emitter = new FactomEventEmitter(cli, { interval: INTERVAL });
const nullListener = () => {};
const listener = (aBlock) => {
// assert that adding multiple listeners results in healthy state
assert.instanceOf(aBlock, AdminBlock);
assert.isTrue(emitter.isPolling);
assert.lengthOf(emitter.listeners('newEntryCreditBlock'), 1);
assert.lengthOf(emitter.listeners('newAdminBlock'), 1);
// assert that the removal of a listener does not stop polling if newDirectoryBlock still has dependent listeners
emitter.removeListener('newAdminBlock', listener);
assert.isTrue(emitter.isPolling);
assert.lengthOf(emitter.listeners('newEntryCreditBlock'), 1);
assert.lengthOf(emitter.listeners('newAdminBlock'), 0);
// assert that the removal of the final dependent listeners stops polling and removes all listeners
emitter.removeListener('newEntryCreditBlock', nullListener);
assert.isFalse(emitter.isPolling);
assert.lengthOf(emitter.listeners('newEntryCreditBlock'), 0);
done();
};
emitter.on('error', (err) => done(err));
emitter.on(FACTOM_EVENT.newEntryCreditBlock, nullListener);
emitter.on(FACTOM_EVENT.newAdminBlock, listener);
});
it('should not stop polling if there are listeners of the same type still active', (done) => {
const emitter = new FactomEventEmitter(cli, { interval: INTERVAL });
const nullListener = () => {};
const listener = (fBlock) => {
// assert that adding multiple listeners results in healthy state
assert.instanceOf(fBlock, FactoidBlock);
assert.isTrue(emitter.isPolling);
assert.lengthOf(emitter.listeners('newFactoidBlock'), 2);
// assert that the removal of a listener does not stop polling if newDirectoryBlock still has dependent listeners
emitter.removeListener('newFactoidBlock', listener);
assert.isTrue(emitter.isPolling);
assert.lengthOf(emitter.listeners('newFactoidBlock'), 1);
// assert that the removal of the final dependent listeners stops polling and removes all listeners
emitter.removeListener('newFactoidBlock', nullListener);
assert.isFalse(emitter.isPolling);
assert.lengthOf(emitter.listeners('newFactoidBlock'), 0);
done();
};
emitter.on('error', (err) => done(err));
emitter.on(FACTOM_EVENT.newFactoidBlock, nullListener);
emitter.on(FACTOM_EVENT.newFactoidBlock, listener);
});
it('should add two chain ID listeners for the same chain ID then remove one without affecting the other', (done) => {
const emitter = new FactomEventEmitter(cli, { interval: INTERVAL });
const chainId = '4060c0192a421ca121ffff935889ef55a64574a6ef0e69b2b4f8a0ab919b2ca4';
const nullListener = () => {};
const listener = (eBlock) => {
assert.instanceOf(eBlock, EntryBlock);
assert.isTrue(emitter.isPolling);
assert.lengthOf(emitter.listeners(chainId), 2);
assert.lengthOf(emitter.chainSubscriptions, 1);
emitter.removeListener(chainId, nullListener);
assert.isTrue(emitter.isPolling);
assert.lengthOf(emitter.listeners(chainId), 1);
assert.lengthOf(emitter.chainSubscriptions, 1);
emitter.removeListener(chainId, listener);
assert.isFalse(emitter.isPolling);
assert.lengthOf(emitter.listeners(chainId), 0);
assert.lengthOf(emitter.chainSubscriptions, 0);
done();
};
emitter.on('error', (err) => done(err));
emitter.on(chainId, nullListener);
emitter.on(chainId, listener);
});
it('should add two factoid address listeners for the same address then remove one without affecting the other', (done) => {
const emitter = new FactomEventEmitter(cli, { interval: INTERVAL });
const address = 'FA29eyMVJaZ2tbGqJ3M49gANaXMXCjgfKcJGe5mx8p4iQFCvFDAC';
const factoidBlockRef = 'cad832bab1d83c74bff8e1092fcf70e298cd7cdf35dee1956ae8879e749195ac';
cli.getDirectoryBlockHead.resolves({ ...mockDirectoryBlock, factoidBlockRef });
const nullListener = () => {};
const listener = (tx) => {
assert.strictEqual(
tx.id,
'c538ffb37d5dab837f3ea781b01bae481d8d6e77ceb0471442c1d85576a00e01'
);
assert.isTrue(emitter.isPolling);
assert.lengthOf(emitter.listeners(address), 2);
assert.lengthOf(emitter.factoidAddressSubscriptions, 1);
emitter.removeListener(address, nullListener);
assert.isTrue(emitter.isPolling);
assert.lengthOf(emitter.listeners(address), 1);
assert.lengthOf(emitter.factoidAddressSubscriptions, 1);
emitter.removeListener(address, listener);
assert.isFalse(emitter.isPolling);
assert.lengthOf(emitter.listeners(address), 0);
assert.lengthOf(emitter.factoidAddressSubscriptions, 0);
done();
};
emitter.on('error', (err) => done(err));
emitter.on(address, nullListener);
emitter.on(address, listener);
});
it('should add two listeners for the same pending transaction subscription then remove one without affecting the other', (done) => {
const emitter = new FactomEventEmitter(cli, { interval: INTERVAL });
const pendingTransaction = {
eventType: 'newPendingTransaction',
topic: 'FA29eyMVJaZ2tbGqJ3M49gANaXMXCjgfKcJGe5mx8p4iQFCvFDAC',
};
const tokenizedPendingTransaction =
FactomEventEmitter.getSubscriptionToken(pendingTransaction);
const mockPendingTransaction = getMockPendingTransaction();
const nullListener = () => {};
const listener = (tx) => {
assert.strictEqual(tx.transactionid, mockPendingTransaction[0].transactionid);
assert.isTrue(emitter.isPolling);
assert.lengthOf(emitter.listeners(tokenizedPendingTransaction), 2);
assert.lengthOf(emitter.factoidAddressPendingTransactionSubscriptions, 1);
emitter.removeListener(tokenizedPendingTransaction, nullListener);
assert.lengthOf(emitter.listeners(tokenizedPendingTransaction), 1);
assert.lengthOf(emitter.factoidAddressPendingTransactionSubscriptions, 1);
assert.isTrue(emitter.isPolling);
emitter.removeListener(tokenizedPendingTransaction, listener);
assert.isFalse(emitter.isPolling);
assert.lengthOf(emitter.listeners(tokenizedPendingTransaction), 0);
assert.lengthOf(emitter.factoidAddressPendingTransactionSubscriptions, 0);
cli.factomdApi.restore();
done();
};
emitter.on('error', (err) => done(err));
emitter.on(tokenizedPendingTransaction, nullListener);
emitter.on(tokenizedPendingTransaction, listener);
sinon.stub(cli, 'factomdApi').resolves(mockPendingTransaction);
});
it('should add two pending transaction listeners and should not emit existing pending transactions', (done) => {
const emitter = new FactomEventEmitter(cli, { interval: INTERVAL });
const pendingTransaction = {
eventType: 'newPendingTransaction',
topic: 'FA29eyMVJaZ2tbGqJ3M49gANaXMXCjgfKcJGe5mx8p4iQFCvFDAC',
};
const tokenizedPendingTransaction =
FactomEventEmitter.getSubscriptionToken(pendingTransaction);
const mockPendingTransaction = getMockPendingTransaction();
let counter = 1;
const pre_listener = (tx) => {
assert.strictEqual(
tx.transactionid,
mockPendingTransaction[0].transactionid,
'Emits expected transaction'
);
assert.strictEqual(counter, 1, 'Emitter not triggered twice');
// setup second listener after pending transaction has occurred
emitter.on(tokenizedPendingTransaction, post_listener);
counter++;
};
const post_listener = () => {
assert.fail('Fail: The pending transaction is not a new pending transaction.');
};
emitter.on('error', (err) => done(err));
emitter.on(tokenizedPendingTransaction, pre_listener);
sinon.stub(cli, 'factomdApi').resolves(mockPendingTransaction);
// timeout to allow emitter to poll at least twice
setTimeout(() => {
emitter.removeListener(tokenizedPendingTransaction, pre_listener);
emitter.removeListener(tokenizedPendingTransaction, post_listener);
cli.factomdApi.restore();
done();
}, INTERVAL * 4);
});
it('should add a pending transaction listener and emit on new pending transactions', (done) => {
const emitter = new FactomEventEmitter(cli, { interval: INTERVAL });
const pendingTransaction = {
eventType: 'newPendingTransaction',
topic: 'FA29eyMVJaZ2tbGqJ3M49gANaXMXCjgfKcJGe5mx8p4iQFCvFDAC',
};
const tokenizedPendingTransaction =
FactomEventEmitter.getSubscriptionToken(pendingTransaction);
const firstTransaction = getMockPendingTransaction();
const secondTransaction = getMockPendingTransaction('abc123');
const allTransactions = [...firstTransaction, ...secondTransaction];
const expectedTransactionIds = [
firstTransaction[0].transactionid,
secondTransaction[0].transactionid,
];
let actualTransactionIds = [];
const listener = (tx) => {
assert(
expectedTransactionIds.includes(tx.transactionid),
'Emits expected pending transaction'
);
assert(
!actualTransactionIds.includes(tx.transactionid),
'Pending transaction should not emit twice'
);
actualTransactionIds.push(tx.transactionid);
cli.factomdApi.restore();
sinon.stub(cli, 'factomdApi').resolves(allTransactions);
};
emitter.on('error', (err) => done(err));
emitter.on(tokenizedPendingTransaction, listener);
sinon.stub(cli, 'factomdApi').resolves(firstTransaction);
// timeout to allow emitter to poll at least twice
setTimeout(() => {
assert.sameDeepOrderedMembers(actualTransactionIds, expectedTransactionIds);
emitter.removeListener(tokenizedPendingTransaction, listener);
done();
}, INTERVAL * 10);
});
});