Changeset View
Changeset View
Standalone View
Standalone View
modules/chronik-client/test/integration/plugins.ts
// Copyright (c) 2024 The Bitcoin developers | // Copyright (c) 2024 The Bitcoin developers | ||||
// Distributed under the MIT software license, see the accompanying | // Distributed under the MIT software license, see the accompanying | ||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php. | // file COPYING or http://www.opensource.org/licenses/mit-license.php. | ||||
import * as chai from 'chai'; | import * as chai from 'chai'; | ||||
import chaiAsPromised from 'chai-as-promised'; | import chaiAsPromised from 'chai-as-promised'; | ||||
import { ChildProcess } from 'node:child_process'; | import { ChildProcess } from 'node:child_process'; | ||||
import { EventEmitter, once } from 'node:events'; | import { EventEmitter, once } from 'node:events'; | ||||
import path from 'path'; | import path from 'path'; | ||||
import { ChronikClient } from '../../index'; | import { ChronikClient, WsMsgClient, WsEndpoint } from '../../index'; | ||||
import initializeTestRunner, { | import initializeTestRunner, { | ||||
cleanupMochaRegtest, | cleanupMochaRegtest, | ||||
setMochaTimeout, | setMochaTimeout, | ||||
TestInfo, | TestInfo, | ||||
expectWsMsgs, | |||||
} from '../setup/testRunner'; | } from '../setup/testRunner'; | ||||
const expect = chai.expect; | const expect = chai.expect; | ||||
chai.use(chaiAsPromised); | chai.use(chaiAsPromised); | ||||
describe('chronik-client presentation of plugin entries in tx inputs, outputs and in utxos', () => { | describe('chronik-client presentation of plugin entries in tx inputs, outputs and in utxos', () => { | ||||
// Define variables used in scope of this test | // Define variables used in scope of this test | ||||
const testName = path.basename(__filename); | const testName = path.basename(__filename); | ||||
let testRunner: ChildProcess; | let testRunner: ChildProcess; | ||||
// Collect websocket msgs in an array for analysis in each step | |||||
let msgCollectorWs1: Array<WsMsgClient> = []; | |||||
let msgCollectorWs2: Array<WsMsgClient> = []; | |||||
const statusEvent = new EventEmitter(); | const statusEvent = new EventEmitter(); | ||||
let get_test_info: Promise<TestInfo>; | let get_test_info: Promise<TestInfo>; | ||||
let chronikUrl: string[]; | let chronikUrl: string[]; | ||||
let chronik: ChronikClient; | let chronik: ChronikClient; | ||||
let setupScriptTermination: ReturnType<typeof setTimeout>; | let setupScriptTermination: ReturnType<typeof setTimeout>; | ||||
before(async function () { | before(async function () { | ||||
// Initialize testRunner before mocha tests | // Initialize testRunner before mocha tests | ||||
Show All 35 Lines | after(async () => { | ||||
); | ); | ||||
}); | }); | ||||
beforeEach(async () => { | beforeEach(async () => { | ||||
await once(statusEvent, 'ready'); | await once(statusEvent, 'ready'); | ||||
}); | }); | ||||
afterEach(() => { | afterEach(() => { | ||||
// Reset msgCollectors after each step | |||||
msgCollectorWs1 = []; | |||||
msgCollectorWs2 = []; | |||||
testRunner.send('next'); | testRunner.send('next'); | ||||
}); | }); | ||||
let ws1: WsEndpoint; | |||||
let ws2: WsEndpoint; | |||||
const BASE_ADDEDTOMEMPOOL_WSMSG: WsMsgClient = { | |||||
type: 'Tx', | |||||
msgType: 'TX_ADDED_TO_MEMPOOL', | |||||
txid: '1111111111111111111111111111111111111111111111111111111111111111', | |||||
}; | |||||
const BASE_CONFIRMED_WSMSG: WsMsgClient = { | |||||
type: 'Tx', | |||||
msgType: 'TX_CONFIRMED', | |||||
txid: '1111111111111111111111111111111111111111111111111111111111111111', | |||||
}; | |||||
const PLUGIN_NAME = 'my_plugin'; | const PLUGIN_NAME = 'my_plugin'; | ||||
const BYTES_a = Buffer.from('a').toString('hex'); | const BYTES_a = Buffer.from('a').toString('hex'); | ||||
const BYTES_argo = Buffer.from('argo').toString('hex'); | const BYTES_argo = Buffer.from('argo').toString('hex'); | ||||
const BYTES_alef = Buffer.from('alef').toString('hex'); | const BYTES_alef = Buffer.from('alef').toString('hex'); | ||||
const BYTES_abc = Buffer.from('abc').toString('hex'); | const BYTES_abc = Buffer.from('abc').toString('hex'); | ||||
const BYTES_b = Buffer.from('b').toString('hex'); | const BYTES_b = Buffer.from('b').toString('hex'); | ||||
const BYTES_blub = Buffer.from('blub').toString('hex'); | const BYTES_blub = Buffer.from('blub').toString('hex'); | ||||
▲ Show 20 Lines • Show All 53 Lines • ▼ Show 20 Lines | it('New regtest chain', async () => { | ||||
// We throw an error if the endpoint is called with an invalid plugin group hex | // We throw an error if the endpoint is called with an invalid plugin group hex | ||||
await expect( | await expect( | ||||
chronik.plugin(PLUGIN_NAME).utxos('not a hex string'), | chronik.plugin(PLUGIN_NAME).utxos('not a hex string'), | ||||
).to.be.rejectedWith( | ).to.be.rejectedWith( | ||||
Error, | Error, | ||||
`Failed getting /plugin/${PLUGIN_NAME}/not a hex string/utxos: 400: Invalid hex: Invalid character 'n' at position 0`, | `Failed getting /plugin/${PLUGIN_NAME}/not a hex string/utxos: 400: Invalid hex: Invalid character 'n' at position 0`, | ||||
); | ); | ||||
// Connect to the websocket with a testable onMessage handler | |||||
ws1 = chronik.ws({ | |||||
onMessage: msg => { | |||||
msgCollectorWs1.push(msg); | |||||
}, | |||||
}); | |||||
await ws1.waitForOpen(); | |||||
// We can subscribe to a plugin | |||||
ws1.subscribeToPlugin(PLUGIN_NAME, BYTES_a); | |||||
expect(ws1.subs.plugins).to.deep.equal([ | |||||
{ pluginName: PLUGIN_NAME, group: BYTES_a }, | |||||
]); | |||||
// We can subscribe to multiple plugins | |||||
ws1.subscribeToPlugin(PLUGIN_NAME, BYTES_b); | |||||
expect(ws1.subs.plugins).to.deep.equal([ | |||||
{ pluginName: PLUGIN_NAME, group: BYTES_a }, | |||||
{ pluginName: PLUGIN_NAME, group: BYTES_b }, | |||||
]); | |||||
// We can unsubscribe from a plugin we are subscribed to | |||||
ws1.unsubscribeFromPlugin(PLUGIN_NAME, BYTES_b); | |||||
expect(ws1.subs.plugins).to.deep.equal([ | |||||
{ pluginName: PLUGIN_NAME, group: BYTES_a }, | |||||
]); | |||||
// We cannot unsubscribe from a plugin if we are not currently subscribed to it | |||||
expect(() => ws1.unsubscribeFromPlugin(PLUGIN_NAME, BYTES_b)).to.throw( | |||||
`No existing sub at pluginName="${PLUGIN_NAME}", group="${BYTES_b}"`, | |||||
); | |||||
// We cannot subscribe to an invalid plugin | |||||
expect(() => | |||||
ws1.subscribeToPlugin(undefined as unknown as string, BYTES_a), | |||||
).to.throw(`pluginName (first parameter) must be a string`); | |||||
expect(() => | |||||
ws1.subscribeToPlugin(PLUGIN_NAME, undefined as unknown as string), | |||||
).to.throw(`group (second parameter) must be a string`); | |||||
expect(() => ws1.subscribeToPlugin(PLUGIN_NAME, 'aaa')).to.throw( | |||||
`group (second parameter) must have even length (complete bytes): "aaa"`, | |||||
); | |||||
expect(() => | |||||
ws1.subscribeToPlugin(PLUGIN_NAME, 'not a hex string'), | |||||
).to.throw( | |||||
`group (second parameter) must be a valid lowercase hex string: "not a hex string"`, | |||||
); | |||||
// Initialize a second websocket to confirm we match the behavior of the chronik test script | |||||
ws2 = chronik.ws({ | |||||
onMessage: msg => { | |||||
msgCollectorWs2.push(msg); | |||||
}, | |||||
}); | |||||
await ws2.waitForOpen(); | |||||
ws2.subscribeToPlugin(PLUGIN_NAME, BYTES_b); | |||||
expect(ws2.subs.plugins).to.deep.equal([ | |||||
{ pluginName: PLUGIN_NAME, group: BYTES_b }, | |||||
]); | |||||
}); | }); | ||||
it('After broadcasting a tx with plugin utxos in group "a"', async () => { | it('After broadcasting a tx with plugin utxos in group "a"', async () => { | ||||
// Wait for expected msg | |||||
await expectWsMsgs(1, msgCollectorWs1); | |||||
// We get ADDED_TO_MEMPOOL websocket msg at ws1 | |||||
expect(msgCollectorWs1).to.deep.equal([ | |||||
{ ...BASE_ADDEDTOMEMPOOL_WSMSG, txid: FIRST_PLUGIN_TXID }, | |||||
]); | |||||
// We get no websocket msg at ws2 | |||||
expect(msgCollectorWs2).to.deep.equal([]); | |||||
const firstTx = await chronik.tx(FIRST_PLUGIN_TXID); | const firstTx = await chronik.tx(FIRST_PLUGIN_TXID); | ||||
const { inputs, outputs } = firstTx; | const { inputs, outputs } = firstTx; | ||||
// As we have no plugins in this tx's inputs, we have no plugins key in tx inputs | // As we have no plugins in this tx's inputs, we have no plugins key in tx inputs | ||||
expect(typeof inputs[0].plugins).to.eql('undefined'); | expect(typeof inputs[0].plugins).to.eql('undefined'); | ||||
// We get plugin info in expected shape for outputs | // We get plugin info in expected shape for outputs | ||||
expect(outputs[0]).to.deep.equal({ | expect(outputs[0]).to.deep.equal({ | ||||
▲ Show 20 Lines • Show All 57 Lines • ▼ Show 20 Lines | it('After broadcasting a tx with plugin utxos in group "a"', async () => { | ||||
}, | }, | ||||
}, | }, | ||||
value: 4999990000, | value: 4999990000, | ||||
}, | }, | ||||
], | ], | ||||
}); | }); | ||||
}); | }); | ||||
it('After broadcasting a tx with plugin utxos in group "b"', async () => { | it('After broadcasting a tx with plugin utxos in group "b"', async () => { | ||||
// Wait for expected msg at ws1 | |||||
await expectWsMsgs(1, msgCollectorWs1); | |||||
// We get ADDED_TO_MEMPOOL websocket msg at ws1 | |||||
expect(msgCollectorWs1).to.deep.equal([ | |||||
{ ...BASE_ADDEDTOMEMPOOL_WSMSG, txid: SECOND_PLUGIN_TXID }, | |||||
]); | |||||
// Wait for expected msg a ws2 | |||||
await expectWsMsgs(1, msgCollectorWs2); | |||||
// We get ADDED_TO_MEMPOOL websocket msg at ws2 | |||||
expect(msgCollectorWs2).to.deep.equal([ | |||||
{ ...BASE_ADDEDTOMEMPOOL_WSMSG, txid: SECOND_PLUGIN_TXID }, | |||||
]); | |||||
const secondTx = await chronik.tx(SECOND_PLUGIN_TXID); | const secondTx = await chronik.tx(SECOND_PLUGIN_TXID); | ||||
const { inputs, outputs } = secondTx; | const { inputs, outputs } = secondTx; | ||||
// We have plugins in this tx's inputs, so we get an inputs key with their information | // We have plugins in this tx's inputs, so we get an inputs key with their information | ||||
expect(inputs[0].plugins).to.deep.equal({ | expect(inputs[0].plugins).to.deep.equal({ | ||||
[PLUGIN_NAME]: { | [PLUGIN_NAME]: { | ||||
groups: [BYTES_a], | groups: [BYTES_a], | ||||
▲ Show 20 Lines • Show All 64 Lines • ▼ Show 20 Lines | it('After broadcasting a tx with plugin utxos in group "b"', async () => { | ||||
}, | }, | ||||
}, | }, | ||||
value: 4999980000, | value: 4999980000, | ||||
}, | }, | ||||
], | ], | ||||
}); | }); | ||||
}); | }); | ||||
it('After mining a block with these first 2 txs', async () => { | it('After mining a block with these first 2 txs', async () => { | ||||
await expectWsMsgs(2, msgCollectorWs1); | |||||
// We get ADDED_TO_MEMPOOL websocket msg at ws1 | |||||
expect(msgCollectorWs1).to.deep.equal([ | |||||
{ ...BASE_CONFIRMED_WSMSG, txid: SECOND_PLUGIN_TXID }, | |||||
{ ...BASE_CONFIRMED_WSMSG, txid: FIRST_PLUGIN_TXID }, | |||||
]); | |||||
await expectWsMsgs(1, msgCollectorWs2); | |||||
// We get ADDED_TO_MEMPOOL websocket msg at ws2 | |||||
expect(msgCollectorWs2).to.deep.equal([ | |||||
{ ...BASE_CONFIRMED_WSMSG, txid: SECOND_PLUGIN_TXID }, | |||||
]); | |||||
// The plugin info in a tx returned by chronik-client is not changed by a block confirming | // The plugin info in a tx returned by chronik-client is not changed by a block confirming | ||||
const firstTx = await chronik.tx(FIRST_PLUGIN_TXID); | const firstTx = await chronik.tx(FIRST_PLUGIN_TXID); | ||||
const { inputs, outputs } = firstTx; | const { inputs, outputs } = firstTx; | ||||
// As we have no plugins in this tx's inputs, we have no plugins key in tx inputs | // As we have no plugins in this tx's inputs, we have no plugins key in tx inputs | ||||
expect(typeof inputs[0].plugins).to.eql('undefined'); | expect(typeof inputs[0].plugins).to.eql('undefined'); | ||||
// We get plugin info in expected shape for outputs | // We get plugin info in expected shape for outputs | ||||
Show All 17 Lines | it('After mining a block with these first 2 txs', async () => { | ||||
expect(outputs[3].plugins).to.deep.equal({ | expect(outputs[3].plugins).to.deep.equal({ | ||||
[PLUGIN_NAME]: { | [PLUGIN_NAME]: { | ||||
groups: [BYTES_a], | groups: [BYTES_a], | ||||
data: [BYTES_abc], | data: [BYTES_abc], | ||||
}, | }, | ||||
}); | }); | ||||
}); | }); | ||||
it('After broadcasting a tx with plugin utxos in group "c"', async () => { | it('After broadcasting a tx with plugin utxos in group "c"', async () => { | ||||
// We get no websocket msgs at ws1 | |||||
expect(msgCollectorWs1).to.deep.equal([]); | |||||
await expectWsMsgs(1, msgCollectorWs2); | |||||
// We get ADDED_TO_MEMPOOL websocket msg at ws2 | |||||
expect(msgCollectorWs2).to.deep.equal([ | |||||
{ ...BASE_ADDEDTOMEMPOOL_WSMSG, txid: THIRD_PLUGIN_TXID }, | |||||
]); | |||||
const thirdTx = await chronik.tx(THIRD_PLUGIN_TXID); | const thirdTx = await chronik.tx(THIRD_PLUGIN_TXID); | ||||
const { inputs, outputs } = thirdTx; | const { inputs, outputs } = thirdTx; | ||||
// We have plugins in this tx's inputs, so we get an inputs key with their information | // We have plugins in this tx's inputs, so we get an inputs key with their information | ||||
expect(inputs[0].plugins).to.deep.equal({ | expect(inputs[0].plugins).to.deep.equal({ | ||||
[PLUGIN_NAME]: { | [PLUGIN_NAME]: { | ||||
groups: [BYTES_b], | groups: [BYTES_b], | ||||
Show All 38 Lines | it('After broadcasting a tx with plugin utxos in group "c"', async () => { | ||||
}; | }; | ||||
expect(thesePluginUtxos).to.deep.equal({ | expect(thesePluginUtxos).to.deep.equal({ | ||||
groupHex: BYTES_c, | groupHex: BYTES_c, | ||||
pluginName: PLUGIN_NAME, | pluginName: PLUGIN_NAME, | ||||
utxos: [group_c_utxo], | utxos: [group_c_utxo], | ||||
}); | }); | ||||
}); | }); | ||||
it('After mining a block with this third tx', async () => { | it('After mining a block with this third tx', async () => { | ||||
// We get expected ws confirmed msg | |||||
await expectWsMsgs(1, msgCollectorWs2); | |||||
expect(msgCollectorWs2).to.deep.equal([ | |||||
{ ...BASE_CONFIRMED_WSMSG, txid: THIRD_PLUGIN_TXID }, | |||||
]); | |||||
// Plugin output is not changed by mining the block | // Plugin output is not changed by mining the block | ||||
const thesePluginUtxos = await chronik | const thesePluginUtxos = await chronik | ||||
.plugin(PLUGIN_NAME) | .plugin(PLUGIN_NAME) | ||||
.utxos(BYTES_c); | .utxos(BYTES_c); | ||||
expect(thesePluginUtxos).to.deep.equal({ | expect(thesePluginUtxos).to.deep.equal({ | ||||
groupHex: BYTES_c, | groupHex: BYTES_c, | ||||
pluginName: PLUGIN_NAME, | pluginName: PLUGIN_NAME, | ||||
utxos: [{ ...group_c_utxo, blockHeight: 103 }], | utxos: [{ ...group_c_utxo, blockHeight: 103 }], | ||||
}); | }); | ||||
}); | }); | ||||
it('After invalidating the mined block with the third tx', async () => { | it('After invalidating the mined block with the third tx', async () => { | ||||
await expectWsMsgs(1, msgCollectorWs2); | |||||
expect(msgCollectorWs2).to.deep.equal([ | |||||
{ ...BASE_ADDEDTOMEMPOOL_WSMSG, txid: THIRD_PLUGIN_TXID }, | |||||
]); | |||||
// Plugin output is not changed by invalidating the block | // Plugin output is not changed by invalidating the block | ||||
const thesePluginUtxos = await chronik | const thesePluginUtxos = await chronik | ||||
.plugin(PLUGIN_NAME) | .plugin(PLUGIN_NAME) | ||||
.utxos(BYTES_c); | .utxos(BYTES_c); | ||||
expect(thesePluginUtxos).to.deep.equal({ | expect(thesePluginUtxos).to.deep.equal({ | ||||
groupHex: BYTES_c, | groupHex: BYTES_c, | ||||
pluginName: PLUGIN_NAME, | pluginName: PLUGIN_NAME, | ||||
utxos: [group_c_utxo], | utxos: [group_c_utxo], | ||||
}); | }); | ||||
}); | }); | ||||
it('After invalidating the mined block with the first two txs', async () => { | it('After invalidating the mined block with the first two txs', async () => { | ||||
await expectWsMsgs(2, msgCollectorWs1); | |||||
// We get ADDED_TO_MEMPOOL websocket msgs at ws1 | |||||
expect(msgCollectorWs1).to.deep.equal([ | |||||
{ ...BASE_ADDEDTOMEMPOOL_WSMSG, txid: FIRST_PLUGIN_TXID }, | |||||
{ ...BASE_ADDEDTOMEMPOOL_WSMSG, txid: SECOND_PLUGIN_TXID }, | |||||
]); | |||||
await expectWsMsgs(1, msgCollectorWs2); | |||||
// We get websocket msgs at ws2 in lexicographic order | |||||
expect(msgCollectorWs2).to.deep.equal([ | |||||
{ | |||||
...BASE_ADDEDTOMEMPOOL_WSMSG, | |||||
txid: THIRD_PLUGIN_TXID, | |||||
msgType: 'TX_REMOVED_FROM_MEMPOOL', | |||||
}, | |||||
{ | |||||
...BASE_ADDEDTOMEMPOOL_WSMSG, | |||||
txid: SECOND_PLUGIN_TXID, | |||||
}, | |||||
{ | |||||
...BASE_ADDEDTOMEMPOOL_WSMSG, | |||||
txid: THIRD_PLUGIN_TXID, | |||||
}, | |||||
]); | |||||
// The plugin info in a chronik tx is not changed by a block invalidating | // The plugin info in a chronik tx is not changed by a block invalidating | ||||
const secondTx = await chronik.tx(SECOND_PLUGIN_TXID); | const secondTx = await chronik.tx(SECOND_PLUGIN_TXID); | ||||
const { inputs, outputs } = secondTx; | const { inputs, outputs } = secondTx; | ||||
// We have plugins in this tx's inputs, so we get an inputs key with their information | // We have plugins in this tx's inputs, so we get an inputs key with their information | ||||
expect(inputs[0].plugins).to.deep.equal({ | expect(inputs[0].plugins).to.deep.equal({ | ||||
[PLUGIN_NAME]: { | [PLUGIN_NAME]: { | ||||
Show All 31 Lines |