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 | |||||