Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F14864716
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
50 KB
Subscribers
None
View Options
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
Details
Attached
Mime Type
text/x-diff
Expires
Wed, May 21, 21:52 (1 d, 18 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5865584
Default Alt Text
(50 KB)
Attached To
rABC Bitcoin ABC
Event Timeline
Log In to Comment