Page MenuHomePhabricator

No OneTemporary

diff --git a/modules/chronik-client/test/integration/plugins.ts b/modules/chronik-client/test/integration/plugins.ts
index 3d95c53fc..9a79a9f25 100644
--- a/modules/chronik-client/test/integration/plugins.ts
+++ b/modules/chronik-client/test/integration/plugins.ts
@@ -1,917 +1,930 @@
// Copyright (c) 2024 The Bitcoin developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
import * as chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
import { ChildProcess } from 'node:child_process';
import { EventEmitter, once } from 'node:events';
import path from 'path';
import { ChronikClient, WsEndpoint, WsMsgClient } from '../../index';
import initializeTestRunner, {
cleanupMochaRegtest,
expectWsMsgs,
setMochaTimeout,
TestInfo,
} from '../setup/testRunner';
const expect = chai.expect;
chai.use(chaiAsPromised);
describe('chronik-client presentation of plugin entries in tx inputs, outputs and in utxos', () => {
// Define variables used in scope of this test
const testName = path.basename(__filename);
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();
let get_test_info: Promise<TestInfo>;
let chronikUrl: string[];
let chronik: ChronikClient;
let setupScriptTermination: ReturnType<typeof setTimeout>;
before(async function () {
// Initialize testRunner before mocha tests
testRunner = initializeTestRunner(testName, statusEvent);
// Handle IPC messages from the setup script
testRunner.on('message', function (message: any) {
if (message && message.test_info) {
get_test_info = new Promise(resolve => {
resolve(message.test_info);
});
}
});
await once(statusEvent, 'ready');
const testInfo = await get_test_info;
chronikUrl = [testInfo.chronik];
chronik = new ChronikClient(chronikUrl);
console.info(`chronikUrl set to ${JSON.stringify(chronikUrl)}`);
setupScriptTermination = setMochaTimeout(
this,
testName,
testInfo,
testRunner,
);
testRunner.send('next');
});
after(async () => {
await cleanupMochaRegtest(
testName,
testRunner,
setupScriptTermination,
statusEvent,
);
});
beforeEach(async () => {
await once(statusEvent, 'ready');
});
afterEach(() => {
// Reset msgCollectors after each step
msgCollectorWs1 = [];
msgCollectorWs2 = [];
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 BYTES_a = Buffer.from('a').toString('hex');
const BYTES_argo = Buffer.from('argo').toString('hex');
const BYTES_alef = Buffer.from('alef').toString('hex');
const BYTES_abc = Buffer.from('abc').toString('hex');
const BYTES_b = Buffer.from('b').toString('hex');
const BYTES_blub = Buffer.from('blub').toString('hex');
const BYTES_borg = Buffer.from('borg').toString('hex');
const BYTES_bjork = Buffer.from('bjork').toString('hex');
const BYTES_c = Buffer.from('c').toString('hex');
const BYTES_carp = Buffer.from('carp').toString('hex');
let group_c_utxo = {};
const TEST_UTXO_OUTPUTSCRIPT =
'a914da1745e9b549bd0bfa1a569971c77eba30cd5a4b87';
const FIRST_PLUGIN_TXID =
'de0975bfc6ddeb7ef76b6cc1d04e1f66b6993fe508e99c54f536ca1cdbc31788';
const SECOND_PLUGIN_TXID =
'2c4d75c55b33e121fa91efeb62b60bbad7bb97a2959b1a30731764057f32df7e';
const THIRD_PLUGIN_TXID =
'cdc4a279f7474254e93a6df3730fc600c86849d5fefa63d6774ba1246feefc4d';
const BASE_OUTPOINT = { outIdx: 1, txid: FIRST_PLUGIN_TXID };
const BASE_UTXO = {
blockHeight: -1,
isCoinbase: false,
isFinal: false,
outpoint: BASE_OUTPOINT,
script: TEST_UTXO_OUTPUTSCRIPT,
sats: 1000n,
};
const FIRST_PLUGIN_OPRETURN = '6a0454455354046172676f04616c656603616263';
const SECOND_PLUGIN_OPRETURN =
'6a045445535404626c756204626f726705626a6f726b';
const THIRD_PLUGIN_OPRETURN = '6a04544553540463617270';
it('New regtest chain', async () => {
// We get an empty utxo set if no txs exist for a plugin
const emptyPluginsUtxos = await chronik
.plugin(PLUGIN_NAME)
.utxos(BYTES_a);
expect(emptyPluginsUtxos).to.deep.equal({
utxos: [],
groupHex: BYTES_a,
pluginName: PLUGIN_NAME,
});
// We get empty history if no txs exist for a plugin
expect(
await chronik.plugin(PLUGIN_NAME).history(BYTES_a),
).to.deep.equal({
txs: [],
numPages: 0,
numTxs: 0,
});
expect(
await chronik.plugin(PLUGIN_NAME).unconfirmedTxs(BYTES_a),
).to.deep.equal({
txs: [],
numPages: 0,
numTxs: 0,
});
expect(
await chronik.plugin(PLUGIN_NAME).confirmedTxs(BYTES_a),
).to.deep.equal({
txs: [],
numPages: 0,
numTxs: 0,
});
// We throw an error if the endpoint is called with plugin name that does not exist
const nonExistentPlugin = 'doesnotexist';
await expect(
chronik.plugin(nonExistentPlugin).utxos(BYTES_a),
).to.be.rejectedWith(
Error,
`Failed getting /plugin/${nonExistentPlugin}/${BYTES_a}/utxos: 404: Plugin "${nonExistentPlugin}" not loaded`,
);
await expect(
chronik.plugin(nonExistentPlugin).history(BYTES_a),
).to.be.rejectedWith(
Error,
`Failed getting /plugin/${nonExistentPlugin}/${BYTES_a}/history?page=0&page_size=25: 404: Plugin "${nonExistentPlugin}" not loaded`,
);
await expect(
chronik.plugin(nonExistentPlugin).confirmedTxs(BYTES_a),
).to.be.rejectedWith(
Error,
`Failed getting /plugin/${nonExistentPlugin}/${BYTES_a}/confirmed-txs?page=0&page_size=25: 404: Plugin "${nonExistentPlugin}" not loaded`,
);
await expect(
chronik.plugin(nonExistentPlugin).unconfirmedTxs(BYTES_a),
).to.be.rejectedWith(
Error,
`Failed getting /plugin/${nonExistentPlugin}/${BYTES_a}/unconfirmed-txs?page=0&page_size=25: 404: Plugin "${nonExistentPlugin}" not loaded`,
);
// We throw an error if the endpoint is called with an invalid plugin group hex
const badPluginName = 'not a hex string';
await expect(
chronik.plugin(PLUGIN_NAME).utxos(badPluginName),
).to.be.rejectedWith(
Error,
`Failed getting /plugin/${PLUGIN_NAME}/${badPluginName}/utxos: 400: Invalid hex: Invalid character 'n' at position 0`,
);
await expect(
chronik.plugin(PLUGIN_NAME).history(badPluginName),
).to.be.rejectedWith(
Error,
`Failed getting /plugin/${PLUGIN_NAME}/${badPluginName}/history?page=0&page_size=25: 400: Invalid hex: Invalid character 'n' at position 0`,
);
await expect(
chronik.plugin(PLUGIN_NAME).confirmedTxs(badPluginName),
).to.be.rejectedWith(
Error,
`Failed getting /plugin/${PLUGIN_NAME}/${badPluginName}/confirmed-txs?page=0&page_size=25: 400: Invalid hex: Invalid character 'n' at position 0`,
);
await expect(
chronik.plugin(PLUGIN_NAME).unconfirmedTxs(badPluginName),
).to.be.rejectedWith(
Error,
`Failed getting /plugin/${PLUGIN_NAME}/${badPluginName}/unconfirmed-txs?page=0&page_size=25: 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 must be a string`);
expect(() =>
ws1.subscribeToPlugin(PLUGIN_NAME, undefined as unknown as string),
).to.throw(`group must be a string`);
expect(() => ws1.subscribeToPlugin(PLUGIN_NAME, 'aaa')).to.throw(
`group must have even length (complete bytes): "aaa"`,
);
expect(() =>
ws1.subscribeToPlugin(PLUGIN_NAME, 'not a hex string'),
).to.throw(
`group 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 () => {
// 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 { inputs, outputs } = firstTx;
// 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');
// We get plugin info in expected shape for outputs
expect(outputs[0]).to.deep.equal({
sats: 0n,
outputScript: FIRST_PLUGIN_OPRETURN,
// No plugins key here as no associated plugin data for this output
});
expect(outputs[1].plugins).to.deep.equal({
[PLUGIN_NAME]: {
groups: [BYTES_a],
data: [BYTES_argo],
},
});
expect(outputs[2].plugins).to.deep.equal({
[PLUGIN_NAME]: {
groups: [BYTES_a],
data: [BYTES_alef],
},
});
expect(outputs[3].plugins).to.deep.equal({
[PLUGIN_NAME]: {
groups: [BYTES_a],
data: [BYTES_abc],
},
});
// We can get utxos associated with this plugin and specified bytes
const thesePluginUtxos = await chronik
.plugin(PLUGIN_NAME)
.utxos(BYTES_a);
expect(thesePluginUtxos).to.deep.equal({
groupHex: BYTES_a,
pluginName: PLUGIN_NAME,
utxos: [
{
...BASE_UTXO,
plugins: {
[PLUGIN_NAME]: {
data: [BYTES_argo],
groups: [BYTES_a],
},
},
},
{
...BASE_UTXO,
outpoint: { ...BASE_OUTPOINT, outIdx: 2 },
plugins: {
[PLUGIN_NAME]: {
data: [BYTES_alef],
groups: [BYTES_a],
},
},
},
{
...BASE_UTXO,
outpoint: { ...BASE_OUTPOINT, outIdx: 3 },
plugins: {
[PLUGIN_NAME]: {
data: [BYTES_abc],
groups: [BYTES_a],
},
},
sats: 4999990000n,
},
],
});
expect(
await chronik.plugin(PLUGIN_NAME).unconfirmedTxs(BYTES_a),
).to.deep.equal({
txs: [firstTx],
numPages: 1,
numTxs: 1,
});
expect(
await chronik.plugin(PLUGIN_NAME).confirmedTxs(BYTES_a),
).to.deep.equal({
txs: [],
numPages: 0,
numTxs: 0,
});
expect(
await chronik.plugin(PLUGIN_NAME).history(BYTES_a),
).to.deep.equal({
txs: [firstTx],
numPages: 1,
numTxs: 1,
});
expect(
await chronik.plugin(PLUGIN_NAME).unconfirmedTxs(BYTES_b),
).to.deep.equal({
txs: [],
numPages: 0,
numTxs: 0,
});
expect(
await chronik.plugin(PLUGIN_NAME).confirmedTxs(BYTES_b),
).to.deep.equal({
txs: [],
numPages: 0,
numTxs: 0,
});
expect(
await chronik.plugin(PLUGIN_NAME).history(BYTES_b),
).to.deep.equal({
txs: [],
numPages: 0,
numTxs: 0,
});
});
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 { inputs, outputs } = secondTx;
// We have plugins in this tx's inputs, so we get an inputs key with their information
expect(inputs[0].plugins).to.deep.equal({
[PLUGIN_NAME]: {
groups: [BYTES_a],
data: [BYTES_abc],
},
});
// We get plugin info in expected shape for outputs
expect(outputs[0]).to.deep.equal({
sats: 0n,
outputScript: SECOND_PLUGIN_OPRETURN,
// No plugins key here as no associated plugin data for this output
});
expect(outputs[1].plugins).to.deep.equal({
[PLUGIN_NAME]: {
groups: [BYTES_b],
data: [BYTES_blub, BYTES_abc],
},
});
expect(outputs[2].plugins).to.deep.equal({
[PLUGIN_NAME]: {
groups: [BYTES_b],
data: [BYTES_borg],
},
});
expect(outputs[3].plugins).to.deep.equal({
[PLUGIN_NAME]: {
groups: [BYTES_b],
data: [BYTES_bjork],
},
});
// We can get utxos associated with this plugin and specified bytes
const thesePluginUtxos = await chronik
.plugin(PLUGIN_NAME)
.utxos(BYTES_b);
expect(thesePluginUtxos).to.deep.equal({
groupHex: BYTES_b,
pluginName: PLUGIN_NAME,
utxos: [
{
...BASE_UTXO,
outpoint: { txid: SECOND_PLUGIN_TXID, outIdx: 1 },
plugins: {
[PLUGIN_NAME]: {
data: [BYTES_blub, BYTES_abc],
groups: [BYTES_b],
},
},
},
{
...BASE_UTXO,
outpoint: { txid: SECOND_PLUGIN_TXID, outIdx: 2 },
plugins: {
[PLUGIN_NAME]: {
data: [BYTES_borg],
groups: [BYTES_b],
},
},
},
{
...BASE_UTXO,
outpoint: { txid: SECOND_PLUGIN_TXID, outIdx: 3 },
plugins: {
[PLUGIN_NAME]: {
data: [BYTES_bjork],
groups: [BYTES_b],
},
},
sats: 4999980000n,
},
],
});
// Update firstTx, as now it has a spent output
const firstTx = await chronik.tx(FIRST_PLUGIN_TXID);
- // unconfirmed txs are sorted by timeFirstSeen
+ // Unconfirmed txs are sorted first by time first seen, then by txid.
+ const txsSortedUnconfirmed = [firstTx, secondTx].sort(
+ (a, b) =>
+ a.timeFirstSeen - b.timeFirstSeen ||
+ a.txid.localeCompare(b.txid),
+ );
+
expect(
await chronik.plugin(PLUGIN_NAME).unconfirmedTxs(BYTES_a),
).to.deep.equal({
- txs: [secondTx, firstTx],
+ txs: txsSortedUnconfirmed,
numPages: 1,
numTxs: 2,
});
expect(
await chronik.plugin(PLUGIN_NAME).confirmedTxs(BYTES_a),
).to.deep.equal({
txs: [],
numPages: 0,
numTxs: 0,
});
- // Note that the history endpoint keeps unconfirmed txs in reverse-chronological order
- // Opposite order of unconfirmedTxs
+ // History is sorted first by reverse time first seen, then by reverse txid.
+ // We don't account for the pagination glitches here:
+ // https://github.com/Bitcoin-ABC/bitcoin-abc/blob/a18387188c0d1235eca81791919458fec2433345/chronik/chronik-indexer/src/query/group_history.rs#L171
+ const txsSortedHistory = [firstTx, secondTx].sort(
+ (b, a) =>
+ a.timeFirstSeen - b.timeFirstSeen ||
+ a.txid.localeCompare(b.txid),
+ );
+
expect(
await chronik.plugin(PLUGIN_NAME).history(BYTES_a),
).to.deep.equal({
- txs: [firstTx, secondTx],
+ txs: txsSortedHistory,
numPages: 1,
numTxs: 2,
});
expect(
await chronik.plugin(PLUGIN_NAME).unconfirmedTxs(BYTES_b),
).to.deep.equal({
txs: [secondTx],
numPages: 1,
numTxs: 1,
});
expect(
await chronik.plugin(PLUGIN_NAME).confirmedTxs(BYTES_b),
).to.deep.equal({
txs: [],
numPages: 0,
numTxs: 0,
});
expect(
await chronik.plugin(PLUGIN_NAME).history(BYTES_b),
).to.deep.equal({
txs: [secondTx],
numPages: 1,
numTxs: 1,
});
});
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
const firstTx = await chronik.tx(FIRST_PLUGIN_TXID);
const { inputs, outputs } = firstTx;
// 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');
// We get plugin info in expected shape for outputs
expect(outputs[0]).to.deep.equal({
sats: 0n,
outputScript: FIRST_PLUGIN_OPRETURN,
// No plugins key here as no associated plugin data for this output
});
expect(outputs[1].plugins).to.deep.equal({
[PLUGIN_NAME]: {
groups: [BYTES_a],
data: [BYTES_argo],
},
});
expect(outputs[2].plugins).to.deep.equal({
[PLUGIN_NAME]: {
groups: [BYTES_a],
data: [BYTES_alef],
},
});
expect(outputs[3].plugins).to.deep.equal({
[PLUGIN_NAME]: {
groups: [BYTES_a],
data: [BYTES_abc],
},
});
// Update txs as they now have block keys
// Note that firstTx was already updated above
const secondTx = await chronik.tx(SECOND_PLUGIN_TXID);
// Sort alphabetical by txid, as this is how confirmed txs will be sorted
// aka lexicographic sorting
const txsSortedByTxid = [firstTx, secondTx].sort((a, b) =>
a.txid.localeCompare(b.txid),
);
- // History sorting is more complicated
- // Since timeFirstSeen here is constant, we end up getting "reverse-txid" order
+ // History is sorted first by reverse time first seen, then by reverse txid.
+ // We don't account for the pagination glitches here:
// https://github.com/Bitcoin-ABC/bitcoin-abc/blob/a18387188c0d1235eca81791919458fec2433345/chronik/chronik-indexer/src/query/group_history.rs#L171
- const txsSortedByTxidReverse = [firstTx, secondTx].sort((a, b) =>
- b.txid.localeCompare(a.txid),
+ const txsSortedHistory = [firstTx, secondTx].sort(
+ (b, a) =>
+ a.timeFirstSeen - b.timeFirstSeen ||
+ a.txid.localeCompare(b.txid),
);
expect(
await chronik.plugin(PLUGIN_NAME).unconfirmedTxs(BYTES_a),
).to.deep.equal({
txs: [],
numPages: 0,
numTxs: 0,
});
expect(
await chronik.plugin(PLUGIN_NAME).confirmedTxs(BYTES_a),
).to.deep.equal({
txs: txsSortedByTxid,
numPages: 1,
numTxs: 2,
});
expect(
await chronik.plugin(PLUGIN_NAME).history(BYTES_a),
).to.deep.equal({
- txs: txsSortedByTxidReverse,
+ txs: txsSortedHistory,
numPages: 1,
numTxs: 2,
});
expect(
await chronik.plugin(PLUGIN_NAME).unconfirmedTxs(BYTES_b),
).to.deep.equal({
txs: [],
numPages: 0,
numTxs: 0,
});
expect(
await chronik.plugin(PLUGIN_NAME).confirmedTxs(BYTES_b),
).to.deep.equal({
txs: [secondTx],
numPages: 1,
numTxs: 1,
});
expect(
await chronik.plugin(PLUGIN_NAME).history(BYTES_b),
).to.deep.equal({
txs: [secondTx],
numPages: 1,
numTxs: 1,
});
});
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 { inputs, outputs } = thirdTx;
// We have plugins in this tx's inputs, so we get an inputs key with their information
expect(inputs[0].plugins).to.deep.equal({
[PLUGIN_NAME]: {
groups: [BYTES_b],
data: [BYTES_blub, BYTES_abc],
},
});
expect(inputs[1].plugins).to.deep.equal({
[PLUGIN_NAME]: {
groups: [BYTES_b],
data: [BYTES_bjork],
},
});
// We get plugin info in expected shape for outputs
expect(outputs[0]).to.deep.equal({
sats: 0n,
outputScript: THIRD_PLUGIN_OPRETURN,
// No plugins key here as no associated plugin data for this output
});
expect(outputs[1].plugins).to.deep.equal({
[PLUGIN_NAME]: {
groups: [BYTES_c],
data: [BYTES_carp, BYTES_blub, BYTES_abc],
},
});
// We can get utxos associated with this plugin and specified bytes
const thesePluginUtxos = await chronik
.plugin(PLUGIN_NAME)
.utxos(BYTES_c);
group_c_utxo = {
...BASE_UTXO,
outpoint: { txid: THIRD_PLUGIN_TXID, outIdx: 1 },
plugins: {
[PLUGIN_NAME]: {
groups: [BYTES_c],
data: [BYTES_carp, BYTES_blub, BYTES_abc],
},
},
sats: 4999970000n,
};
expect(thesePluginUtxos).to.deep.equal({
groupHex: BYTES_c,
pluginName: PLUGIN_NAME,
utxos: [group_c_utxo],
});
// Update secondTx as now an output is spent
const secondTx = await chronik.tx(SECOND_PLUGIN_TXID);
- // Sort alphabetical by txid, as this is how confirmed txs will be sorted
- // aka lexicographic sorting
- const txsSortedByTxid = [secondTx, thirdTx].sort((a, b) =>
- a.txid.localeCompare(b.txid),
- );
-
- // History sorting is more complicated
- // Since timeFirstSeen here is constant, we end up getting "reverse-txid" order
+ // History is sorted first by reverse time first seen, then by reverse txid.
+ // We don't account for the pagination glitches here:
// https://github.com/Bitcoin-ABC/bitcoin-abc/blob/a18387188c0d1235eca81791919458fec2433345/chronik/chronik-indexer/src/query/group_history.rs#L171
- const txsSortedByTxidReverse = txsSortedByTxid.reverse();
+ const txsSortedHistory = [secondTx, thirdTx].sort(
+ (b, a) =>
+ a.timeFirstSeen - b.timeFirstSeen ||
+ a.txid.localeCompare(b.txid),
+ );
expect(
await chronik.plugin(PLUGIN_NAME).unconfirmedTxs(BYTES_b),
).to.deep.equal({
txs: [thirdTx],
numPages: 1,
numTxs: 1,
});
expect(
await chronik.plugin(PLUGIN_NAME).confirmedTxs(BYTES_b),
).to.deep.equal({
txs: [secondTx],
numPages: 1,
numTxs: 1,
});
expect(
await chronik.plugin(PLUGIN_NAME).history(BYTES_b),
).to.deep.equal({
- txs: txsSortedByTxidReverse,
+ txs: txsSortedHistory,
numPages: 1,
numTxs: 2,
});
});
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
const thesePluginUtxos = await chronik
.plugin(PLUGIN_NAME)
.utxos(BYTES_c);
expect(thesePluginUtxos).to.deep.equal({
groupHex: BYTES_c,
pluginName: PLUGIN_NAME,
utxos: [{ ...group_c_utxo, blockHeight: 103 }],
});
// Get the second tx for this scope
const secondTx = await chronik.tx(SECOND_PLUGIN_TXID);
// Update third tx as it now has a block key
const thirdTx = await chronik.tx(THIRD_PLUGIN_TXID);
// Sort alphabetical by txid, as this is how confirmed txs will be sorted
// aka lexicographic sorting
const txsSortedByTxid = [secondTx, thirdTx].sort((a, b) =>
a.txid.localeCompare(b.txid),
);
- // History sorting is more complicated
- // Since timeFirstSeen here is constant, we end up getting "reverse-txid" order
+ // History is sorted first by reverse time first seen, then by reverse txid.
+ // We don't account for the pagination glitches here:
// https://github.com/Bitcoin-ABC/bitcoin-abc/blob/a18387188c0d1235eca81791919458fec2433345/chronik/chronik-indexer/src/query/group_history.rs#L171
- const txsSortedTxidReverse = [secondTx, thirdTx].sort(
+ const txsSortedHistory = [secondTx, thirdTx].sort(
(b, a) =>
a.timeFirstSeen - b.timeFirstSeen ||
a.txid.localeCompare(b.txid),
);
expect(
await chronik.plugin(PLUGIN_NAME).unconfirmedTxs(BYTES_b),
).to.deep.equal({
txs: [],
numPages: 0,
numTxs: 0,
});
expect(
await chronik.plugin(PLUGIN_NAME).confirmedTxs(BYTES_b),
).to.deep.equal({
txs: txsSortedByTxid,
numPages: 1,
numTxs: 2,
});
expect(
await chronik.plugin(PLUGIN_NAME).history(BYTES_b),
).to.deep.equal({
- txs: txsSortedTxidReverse,
+ txs: txsSortedHistory,
numPages: 1,
numTxs: 2,
});
});
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
const thesePluginUtxos = await chronik
.plugin(PLUGIN_NAME)
.utxos(BYTES_c);
expect(thesePluginUtxos).to.deep.equal({
groupHex: BYTES_c,
pluginName: PLUGIN_NAME,
utxos: [group_c_utxo],
});
});
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
const secondTx = await chronik.tx(SECOND_PLUGIN_TXID);
const { inputs, outputs } = secondTx;
// We have plugins in this tx's inputs, so we get an inputs key with their information
expect(inputs[0].plugins).to.deep.equal({
[PLUGIN_NAME]: {
groups: [BYTES_a],
data: [BYTES_abc],
},
});
// We get plugin info in expected shape for outputs
expect(outputs[0]).to.deep.equal({
sats: 0n,
outputScript: SECOND_PLUGIN_OPRETURN,
// No plugins key here as no associated plugin data for this output
});
expect(outputs[1].plugins).to.deep.equal({
[PLUGIN_NAME]: {
groups: [BYTES_b],
data: [BYTES_blub, BYTES_abc],
},
});
expect(outputs[2].plugins).to.deep.equal({
[PLUGIN_NAME]: {
groups: [BYTES_b],
data: [BYTES_borg],
},
});
expect(outputs[3].plugins).to.deep.equal({
[PLUGIN_NAME]: {
groups: [BYTES_b],
data: [BYTES_bjork],
},
});
});
});
diff --git a/test/functional/setup_scripts/chronik-client_plugins.py b/test/functional/setup_scripts/chronik-client_plugins.py
index bf3bb7e44..383a1efa5 100644
--- a/test/functional/setup_scripts/chronik-client_plugins.py
+++ b/test/functional/setup_scripts/chronik-client_plugins.py
@@ -1,374 +1,396 @@
# Copyright (c) 2024 The Bitcoin developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""
Setup script to exercise the chronik-client js library endpoints for checking plugins in
endpoints with outputs that include Tx[] type
Based on test/functional/chronik_plugins.py
"""
import os
import time
+from functools import cmp_to_key
import pathmagic # noqa
from setup_framework import SetupFramework
from test_framework.address import (
ADDRESS_ECREG_P2SH_OP_TRUE,
ADDRESS_ECREG_UNSPENDABLE,
P2SH_OP_TRUE,
SCRIPTSIG_OP_TRUE,
)
from test_framework.blocktools import COINBASE_MATURITY
from test_framework.messages import COutPoint, CTransaction, CTxIn, CTxOut
from test_framework.script import OP_RETURN, CScript
from test_framework.txtools import pad_tx
from test_framework.util import assert_equal, chronik_sub_plugin
class ChronikClientPlugins(SetupFramework):
def set_test_params(self):
self.setup_clean_chain = True
self.num_nodes = 1
self.extra_args = [["-chronik"]]
def skip_test_if_missing_module(self):
self.skip_if_no_chronik_plugins()
def run_test(self):
from test_framework.chronik.client import pb
node = self.nodes[0]
# Set the mocktime so we don't have to account for the time first seen
# sorting when checking the transactions
now = int(time.time())
node.setmocktime(now)
yield True
chronik = node.get_chronik_client()
def ws_msg(txid: str, msg_type):
return pb.WsMsg(
tx=pb.MsgTx(
msg_type=msg_type,
txid=bytes.fromhex(txid)[::-1],
)
)
def assert_start_raises(*args, **kwargs):
node.assert_start_raises_init_error(["-chronik"], *args, **kwargs)
# Without a plugins.toml, setting up a plugin context is skipped
plugins_toml = os.path.join(node.datadir, "plugins.toml")
plugins_dir = os.path.join(node.datadir, "plugins")
# Plugin that colors outputs with the corresponding PUSHDATA of the OP_RETURN,
# concatenated with the existing plugin data of the corresponding input
with open(plugins_toml, "w", encoding="utf-8") as f:
print("[regtest.plugin.my_plugin]", file=f)
os.mkdir(plugins_dir)
plugin_module = os.path.join(plugins_dir, "my_plugin.py")
with open(plugin_module, "w", encoding="utf-8") as f:
print(
"""
from chronik_plugin.plugin import Plugin, PluginOutput
from chronik_plugin.script import OP_RETURN
class MyPluginPlugin(Plugin):
def lokad_id(self):
return b'TEST'
def version(self):
return '0.1.0'
def run(self, tx):
ops = list(tx.outputs[0].script)
if ops[0] != OP_RETURN:
return []
if ops[1] != b'TEST':
return []
outputs = []
for idx, (op, _) in enumerate(zip(ops[2:], tx.outputs[1:])):
data = [op]
groups = []
if op:
groups = [op[:1]]
if idx < len(tx.inputs):
tx_input = tx.inputs[idx]
if 'my_plugin' in tx_input.plugin:
data += tx_input.plugin['my_plugin'].data
outputs.append(
PluginOutput(idx=idx + 1, data=data, groups=groups)
)
return outputs
""",
file=f,
)
with node.assert_debug_log(
[
"Plugin context initialized Python",
'Loaded plugin my_plugin.MyPluginPlugin (version 0.1.0) with LOKAD IDs [b"TEST"]',
]
):
self.restart_node(0, ["-chronik", "-chronikreindex"])
# Init and websockets here so we can confirm msgs are sent server-side
ws1 = chronik.ws()
ws2 = chronik.ws()
coinblockhash = self.generatetoaddress(node, 1, ADDRESS_ECREG_P2SH_OP_TRUE)[0]
coinblock = node.getblock(coinblockhash)
cointx = coinblock["tx"][0]
self.log.info("Step 1: Empty regtest chain")
yield True
self.log.info("Step 2: Send a tx to create plugin utxos in group 'a'")
self.generatetoaddress(node, COINBASE_MATURITY, ADDRESS_ECREG_UNSPENDABLE)
# Subscribe to websockets in test script to support timing match in chronik-client integration tests
chronik_sub_plugin(ws1, node, "my_plugin", b"a")
chronik_sub_plugin(ws2, node, "my_plugin", b"b")
plugin = chronik.plugin("my_plugin")
coinvalue = 5000000000
tx1 = CTransaction()
tx1.vin = [CTxIn(COutPoint(int(cointx, 16), 0), SCRIPTSIG_OP_TRUE)]
tx1.vout = [
CTxOut(0, CScript([OP_RETURN, b"TEST", b"argo", b"alef", b"abc"])),
CTxOut(1000, P2SH_OP_TRUE),
CTxOut(1000, P2SH_OP_TRUE),
CTxOut(coinvalue - 10000, P2SH_OP_TRUE),
]
pad_tx(tx1)
node.sendrawtransaction(tx1.serialize().hex())
assert_equal(ws1.recv(), ws_msg(tx1.hash, pb.TX_ADDED_TO_MEMPOOL))
# Plugin ran on the mempool tx
# Note: we must perform these assertions here before yield True
# Ensures that plugins are properly indexed before we query for them
proto_tx1 = chronik.tx(tx1.hash).ok()
tx1_plugin_outputs = [
{},
{"my_plugin": pb.PluginEntry(data=[b"argo"], groups=[b"a"])},
{"my_plugin": pb.PluginEntry(data=[b"alef"], groups=[b"a"])},
{"my_plugin": pb.PluginEntry(data=[b"abc"], groups=[b"a"])},
]
assert_equal([inpt.plugins for inpt in proto_tx1.inputs], [{}])
assert_equal(
[output.plugins for output in proto_tx1.outputs],
tx1_plugin_outputs,
)
proto_utxos1 = plugin.utxos(b"a").ok().utxos
assert_equal(
[utxo.plugins for utxo in proto_utxos1],
tx1_plugin_outputs[1:],
)
assert_equal(list(plugin.unconfirmed_txs(b"a").ok().txs), [proto_tx1])
assert_equal(list(plugin.confirmed_txs(b"a").ok().txs), [])
assert_equal(list(plugin.history(b"a").ok().txs), [proto_tx1])
assert_equal(list(plugin.unconfirmed_txs(b"b").ok().txs), [])
assert_equal(list(plugin.confirmed_txs(b"b").ok().txs), [])
assert_equal(list(plugin.history(b"b").ok().txs), [])
yield True
self.log.info("Step 3: Send a second tx to create plugin utxos in group 'b'")
-
tx2 = CTransaction()
tx2.vin = [CTxIn(COutPoint(tx1.sha256, 3), SCRIPTSIG_OP_TRUE)]
tx2.vout = [
CTxOut(0, CScript([OP_RETURN, b"TEST", b"blub", b"borg", b"bjork"])),
CTxOut(1000, P2SH_OP_TRUE),
CTxOut(1000, P2SH_OP_TRUE),
CTxOut(coinvalue - 20000, P2SH_OP_TRUE),
]
pad_tx(tx2)
node.sendrawtransaction(tx2.serialize().hex())
proto_tx2 = chronik.tx(tx2.hash).ok()
tx2_plugin_inputs = [tx1_plugin_outputs[3]]
tx2_plugin_outputs = [
{},
{"my_plugin": pb.PluginEntry(data=[b"blub", b"abc"], groups=[b"b"])},
{"my_plugin": pb.PluginEntry(data=[b"borg"], groups=[b"b"])},
{"my_plugin": pb.PluginEntry(data=[b"bjork"], groups=[b"b"])},
]
assert_equal(
[inpt.plugins for inpt in proto_tx2.inputs],
tx2_plugin_inputs,
)
assert_equal(
[output.plugins for output in proto_tx2.outputs],
tx2_plugin_outputs,
)
proto_utxos1 = plugin.utxos(b"a").ok().utxos
assert_equal(
[utxo.plugins for utxo in proto_utxos1],
[tx1_plugin_outputs[1], tx1_plugin_outputs[2]], # "abc" spent
)
proto_utxos2 = plugin.utxos(b"b").ok().utxos
assert_equal(
[utxo.plugins for utxo in proto_utxos2],
tx2_plugin_outputs[1:],
)
assert_equal(ws1.recv(), ws_msg(tx2.hash, pb.TX_ADDED_TO_MEMPOOL))
assert_equal(ws2.recv(), ws_msg(tx2.hash, pb.TX_ADDED_TO_MEMPOOL))
+ def compare_unconfirmed_txs(tx_a, tx_b):
+ # Sort first by time_first_seen then by txid
+ if tx_a.time_first_seen != tx_b.time_first_seen:
+ return tx_a.time_first_seen - tx_b.time_first_seen
+ return int.from_bytes(tx_a.txid, byteorder="little") - int.from_bytes(
+ tx_b.txid, byteorder="little"
+ )
+
+ def compare_history_txs(tx_a, tx_b):
+ # Sort first by revert time_first_seen then by reverse txid
+ if tx_a.time_first_seen != tx_b.time_first_seen:
+ return tx_b.time_first_seen - tx_a.time_first_seen
+ return int.from_bytes(tx_b.txid, byteorder="little") - int.from_bytes(
+ tx_a.txid, byteorder="little"
+ )
+
proto_tx1 = chronik.tx(tx1.hash).ok()
- txs = sorted([proto_tx1, proto_tx2], key=lambda t: t.txid[::-1])
- assert_equal(list(plugin.unconfirmed_txs(b"a").ok().txs), txs)
+ unconf_txs = sorted(
+ [proto_tx1, proto_tx2], key=cmp_to_key(compare_unconfirmed_txs)
+ )
+ hist_txs = sorted([proto_tx1, proto_tx2], key=cmp_to_key(compare_history_txs))
+ assert_equal(list(plugin.unconfirmed_txs(b"a").ok().txs), unconf_txs)
assert_equal(list(plugin.confirmed_txs(b"a").ok().txs), [])
- assert_equal(list(plugin.history(b"a").ok().txs), txs[::-1])
+ assert_equal(list(plugin.history(b"a").ok().txs), hist_txs)
assert_equal(list(plugin.unconfirmed_txs(b"b").ok().txs), [proto_tx2])
assert_equal(list(plugin.confirmed_txs(b"b").ok().txs), [])
assert_equal(list(plugin.history(b"b").ok().txs), [proto_tx2])
yield True
self.log.info("Step 4: Mine these first two transactions")
# Mine tx1 and tx2
block1 = self.generatetoaddress(node, 1, ADDRESS_ECREG_UNSPENDABLE)[-1]
# Lexicographic order
txids = sorted([tx1.hash, tx2.hash])
assert_equal(ws1.recv(), ws_msg(txids[0], pb.TX_CONFIRMED))
assert_equal(ws1.recv(), ws_msg(txids[1], pb.TX_CONFIRMED))
assert_equal(ws2.recv(), ws_msg(tx2.hash, pb.TX_CONFIRMED))
proto_tx1 = chronik.tx(tx1.hash).ok()
assert_equal([inpt.plugins for inpt in proto_tx1.inputs], [{}])
assert_equal(
[output.plugins for output in proto_tx1.outputs],
tx1_plugin_outputs,
)
proto_tx2 = chronik.tx(tx2.hash).ok()
assert_equal(
[inpt.plugins for inpt in proto_tx2.inputs],
tx2_plugin_inputs,
)
assert_equal(
[output.plugins for output in proto_tx2.outputs],
tx2_plugin_outputs,
)
proto_utxos1 = chronik.plugin("my_plugin").utxos(b"a").ok().utxos
assert_equal(
[utxo.plugins for utxo in proto_utxos1],
[tx1_plugin_outputs[1], tx1_plugin_outputs[2]], # "abc" spent
)
proto_utxos2 = chronik.plugin("my_plugin").utxos(b"b").ok().utxos
assert_equal(
[utxo.plugins for utxo in proto_utxos2],
tx2_plugin_outputs[1:],
)
txs = sorted([proto_tx1, proto_tx2], key=lambda t: t.txid[::-1])
+ hist_txs = sorted([proto_tx1, proto_tx2], key=cmp_to_key(compare_history_txs))
assert_equal(list(plugin.unconfirmed_txs(b"a").ok().txs), [])
assert_equal(list(plugin.confirmed_txs(b"a").ok().txs), txs)
- assert_equal(list(plugin.history(b"a").ok().txs), txs[::-1])
+ assert_equal(list(plugin.history(b"a").ok().txs), hist_txs)
assert_equal(list(plugin.unconfirmed_txs(b"b").ok().txs), [])
assert_equal(list(plugin.confirmed_txs(b"b").ok().txs), [proto_tx2])
assert_equal(list(plugin.history(b"b").ok().txs), [proto_tx2])
yield True
self.log.info("Step 5: Send a third tx to create plugin utxos in group 'c'")
tx3 = CTransaction()
tx3.vin = [
CTxIn(COutPoint(tx2.sha256, 1), SCRIPTSIG_OP_TRUE),
CTxIn(COutPoint(tx2.sha256, 3), SCRIPTSIG_OP_TRUE),
]
tx3.vout = [
CTxOut(0, CScript([OP_RETURN, b"TEST", b"carp"])),
CTxOut(coinvalue - 30000, P2SH_OP_TRUE),
]
pad_tx(tx3)
node.sendrawtransaction(tx3.serialize().hex())
assert_equal(ws2.recv(), ws_msg(tx3.hash, pb.TX_ADDED_TO_MEMPOOL))
proto_tx3 = chronik.tx(tx3.hash).ok()
tx3_plugin_inputs = [tx2_plugin_outputs[1], tx2_plugin_outputs[3]]
tx3_plugin_outputs = [
{},
{
"my_plugin": pb.PluginEntry(
data=[b"carp", b"blub", b"abc"], groups=[b"c"]
),
},
]
assert_equal(
[inpt.plugins for inpt in proto_tx3.inputs],
tx3_plugin_inputs,
)
assert_equal(
[output.plugins for output in proto_tx3.outputs],
tx3_plugin_outputs,
)
proto_utxos2 = plugin.utxos(b"b").ok().utxos
assert_equal(
[utxo.plugins for utxo in proto_utxos2],
[tx2_plugin_outputs[2]], # only "borg" remaining
)
proto_utxos3 = plugin.utxos(b"c").ok().utxos
assert_equal(
[utxo.plugins for utxo in proto_utxos3],
tx3_plugin_outputs[1:],
)
proto_tx2 = chronik.tx(tx2.hash).ok()
txs = sorted([proto_tx2, proto_tx3], key=lambda t: t.txid[::-1])
+ hist_txs = sorted([proto_tx2, proto_tx3], key=cmp_to_key(compare_history_txs))
assert_equal(list(plugin.unconfirmed_txs(b"b").ok().txs), [proto_tx3])
assert_equal(list(plugin.confirmed_txs(b"b").ok().txs), [proto_tx2])
- assert_equal(list(plugin.history(b"b").ok().txs), txs[::-1])
+ assert_equal(list(plugin.history(b"b").ok().txs), hist_txs)
yield True
self.log.info("Step 6: Mine this tx")
# Mine tx3
block2 = self.generatetoaddress(node, 1, ADDRESS_ECREG_UNSPENDABLE)[-1]
assert_equal(ws2.recv(), ws_msg(tx3.hash, pb.TX_CONFIRMED))
proto_tx3 = chronik.tx(tx3.hash).ok()
txs = sorted([proto_tx2, proto_tx3], key=lambda t: t.txid[::-1])
+ hist_txs = sorted([proto_tx2, proto_tx3], key=cmp_to_key(compare_history_txs))
assert_equal(list(plugin.unconfirmed_txs(b"b").ok().txs), [])
assert_equal(list(plugin.confirmed_txs(b"b").ok().txs), txs)
- assert_equal(list(plugin.history(b"b").ok().txs), txs[::-1])
+ assert_equal(list(plugin.history(b"b").ok().txs), hist_txs)
yield True
self.log.info("Step 7: Invalidate the block with the third tx")
# Disconnect block2, inputs + outputs still work
node.invalidateblock(block2)
assert_equal(ws2.recv(), ws_msg(tx3.hash, pb.TX_ADDED_TO_MEMPOOL))
yield True
self.log.info("Step 8: Invalidate the block with the first two txs")
node.invalidateblock(block1)
# Topological order
assert_equal(ws1.recv(), ws_msg(tx1.hash, pb.TX_ADDED_TO_MEMPOOL))
assert_equal(ws1.recv(), ws_msg(tx2.hash, pb.TX_ADDED_TO_MEMPOOL))
# Reorg first clears the mempool and then adds back in topological order
assert_equal(ws2.recv(), ws_msg(tx3.hash, pb.TX_REMOVED_FROM_MEMPOOL))
assert_equal(ws2.recv(), ws_msg(tx2.hash, pb.TX_ADDED_TO_MEMPOOL))
assert_equal(ws2.recv(), ws_msg(tx3.hash, pb.TX_ADDED_TO_MEMPOOL))
yield True
if __name__ == "__main__":
ChronikClientPlugins().main()

File Metadata

Mime Type
text/x-diff
Expires
Wed, May 21, 21:52 (1 d, 15 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5865584
Default Alt Text
(50 KB)

Event Timeline