test/specs/generic/api/Client.spec.js
describe('Client', () => {
/** @type {FullConsensus} */
let otherConsensus;
let testChain;
let clients = {};
function getConsensus(consensus) {
const name = 'volatile' + consensus.charAt(0).toUpperCase() + consensus.slice(1);
const promise = Consensus[name]();
promise.then((c) => {
Log.d('Client.spec', `${consensus}-consensus uses ${c.network.config.peerAddress}`);
});
return promise;
}
function startClient(consensus) {
return new Client(Client.Configuration.builder().build(), getConsensus(consensus));
}
async function startOtherNode(numBlocks = 5) {
const netconfig = Dummy.NETCONFIG;
const consensus = await Consensus.volatileFull(netconfig);
testChain = await TestBlockchain.createVolatileTest(numBlocks);
for (const block of (await testChain.getBlocks(consensus.blockchain.headHash))) {
await consensus.blockchain.pushBlock(await testChain.getBlock(block.hash(), true, true));
}
consensus.network.connect();
return consensus;
}
beforeAll(async function () {
MockClock.install();
MockNetwork.install();
ConstantHelper.instance.set('BaseMiniConsensusAgent.MEMPOOL_DELAY_MIN', 5);
ConstantHelper.instance.set('BaseMiniConsensusAgent.MEMPOOL_DELAY_MAX', 5);
ConstantHelper.instance.set('FullConsensusAgent.MEMPOOL_DELAY_MIN', 5);
ConstantHelper.instance.set('FullConsensusAgent.MEMPOOL_DELAY_MAX', 5);
ConstantHelper.instance.set('BaseConsensusAgent.TRANSACTION_RELAY_INTERVAL', 10);
ConstantHelper.instance.set('BaseConsensusAgent.FREE_TRANSACTION_RELAY_INTERVAL', 10);
ConstantHelper.instance.set('PicoConsensus.MIN_SYNCED_NODES', 1);
otherConsensus = await startOtherNode();
for (const consensus of ['pico', 'nano', 'light', 'full']) {
clients[consensus] = startClient(consensus);
}
});
afterAll(function () {
otherConsensus.network.disconnect();
ConstantHelper.instance.resetAll();
MockClock.uninstall();
MockNetwork.uninstall();
});
function allit(name, fn) {
for (const consensus of ['pico', 'nano', 'light', 'full']) {
it(`${name} (${consensus})`, async (done) => {
fn(done, await clients[consensus], consensus);
});
}
}
function established(name, fn) {
allit(name, (done, client, consensus) => {
client
.waitForConsensusEstablished()
.then(() => fn(done, client, consensus));
});
}
established('can establish and announce consensus', async (done, client) => {
done();
});
established('can be used to fetch head height', async (done, client) => {
expect(await client.getHeadHeight()).toBe(otherConsensus.blockchain.height);
done();
});
established('can be used to fetch head hash', async (done, client) => {
expect((await client.getHeadHash()).equals(otherConsensus.blockchain.headHash)).toBeTruthy();
done();
});
established('can be used to fetch light head block', async (done, client) => {
expect((await client.getHeadBlock(false)).toLight().equals(otherConsensus.blockchain.head.toLight())).toBeTruthy();
done();
});
established('can be used to fetch full head block', async (done, client) => {
expect((await client.getHeadBlock(true)).equals(otherConsensus.blockchain.head)).toBeTruthy();
done();
});
established('can be used to fetch light block by hash', async (done, client) => {
const block = await otherConsensus.blockchain.getBlockAt(2);
expect((await client.getBlock(block.hash(), false)).toLight().equals(block.toLight())).toBeTruthy();
done();
});
established('can be used to fetch full block by hash', async (done, client) => {
const block = await otherConsensus.blockchain.getBlockAt(2);
expect((await client.getBlock(block.hash(), true)).toLight().equals(block)).toBeTruthy();
done();
});
established('can be used to fetch light block by height', async (done, client) => {
const block = await otherConsensus.blockchain.getBlockAt(2);
expect((await client.getBlockAt(block.height, false)).toLight().equals(block.toLight())).toBeTruthy();
done();
});
established('can be used to fetch full block by height', async (done, client) => {
const block = await otherConsensus.blockchain.getBlockAt(2);
expect((await client.getBlockAt(block.height, true)).toLight().equals(block)).toBeTruthy();
done();
});
allit('reports head changed', async (done, _, consensus) => {
const client = startClient(consensus);
let handle;
handle = await client.addHeadChangedListener(hash => {
if (hash.equals(otherConsensus.blockchain.headHash)) {
client.removeListener(handle);
done();
}
});
});
established('can subscribe to mempool tx updates', async (done, /** @type {Client} */ client) => {
const a = new Uint8Array(20);
CryptoWorker.lib.getRandomValues(a);
const newTx = TestBlockchain.createTransaction(testChain.users[0].publicKey, new Address(a), 1, 500, 1, testChain.users[0].privateKey);
let handle;
handle = await client.addTransactionListener((/** @type {TransactionDetails} */ tx) => {
if (tx.transaction.equals(newTx)) {
client.removeListener(handle);
done();
}
}, [newTx.recipient]);
await otherConsensus.mempool.pushTransaction(newTx);
});
established('can send transaction', async (done, client) => {
const a = new Uint8Array(20);
CryptoWorker.lib.getRandomValues(a);
const newTx = TestBlockchain.createTransaction(testChain.users[0].publicKey, new Address(a), 1, 500, 1, testChain.users[0].privateKey);
otherConsensus.mempool.on('transaction-added', (tx) => {
if (tx.equals(newTx)) {
done();
}
});
/** @type {Client.TransactionDetails} */
const tx = await client.sendTransaction(newTx);
if (tx.state !== Client.TransactionState.PENDING && tx.state !== Client.TransactionState.NEW) {
expect(false).toBeTruthy();
done();
}
});
it('can replace consensus at runtime (nano to light)', async (done) => {
const client = startClient('nano');
let establishedNano = false, syncingLight = false;
client.addConsensusChangedListener((state) => {
if (!establishedNano && state === Client.ConsensusState.ESTABLISHED) {
establishedNano = true;
client._replaceConsensus(getConsensus('light'));
} else if (establishedNano && state === Client.ConsensusState.SYNCING) {
syncingLight = true;
} else if (establishedNano && state === Client.ConsensusState.ESTABLISHED) {
expect(syncingLight).toBeTruthy();
done();
}
});
});
it('can replace consensus at runtime (pico failure)', async (done) => {
const client = startClient('pico');
let syncingNano = false;
await client.waitForConsensusEstablished();
await client.addConsensusChangedListener(async (state) => {
if (state !== Client.ConsensusState.ESTABLISHED) {
syncingNano = true;
} else if (state === Client.ConsensusState.ESTABLISHED) {
expect(syncingNano).toBeTruthy();
done();
}
});
client._onConsensusFailed();
});
});