Changeset View
Changeset View
Standalone View
Standalone View
modules/chronik-client/test/integration/script_endpoints.ts
// Copyright (c) 2023-2024 The Bitcoin developers | // Copyright (c) 2023-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 cashaddr from 'ecashaddrjs'; | import cashaddr from 'ecashaddrjs'; | ||||
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 { ChronikClientNode, ScriptType_InNode } from '../../index'; | import { ChronikClientNode, ScriptType_InNode, Tx_InNode } from '../../index'; | ||||
import initializeTestRunner, { | import initializeTestRunner, { | ||||
cleanupMochaRegtest, | cleanupMochaRegtest, | ||||
setMochaTimeout, | setMochaTimeout, | ||||
TestInfo, | TestInfo, | ||||
} from '../setup/testRunner'; | } from '../setup/testRunner'; | ||||
const expect = chai.expect; | const expect = chai.expect; | ||||
chai.use(chaiAsPromised); | chai.use(chaiAsPromised); | ||||
▲ Show 20 Lines • Show All 179 Lines • ▼ Show 20 Lines | it('New regtest chain', async () => { | ||||
expect(chronik.script('p2sh', p2shAddressHash)).to.deep.equal( | expect(chronik.script('p2sh', p2shAddressHash)).to.deep.equal( | ||||
chronik.address(p2shAddress), | chronik.address(p2shAddress), | ||||
); | ); | ||||
// We get the validation error from ecashaddrjs if we call chronik.address with an invalid address | // We get the validation error from ecashaddrjs if we call chronik.address with an invalid address | ||||
expect(() => chronik.address('notAnAddress')).to.throw( | expect(() => chronik.address('notAnAddress')).to.throw( | ||||
'Invalid address: notAnAddress.', | 'Invalid address: notAnAddress.', | ||||
); | ); | ||||
const checkEmptyHistoryAndUtxos = async ( | const checkEmptyScriptMethods = async ( | ||||
chronik: ChronikClientNode, | chronik: ChronikClientNode, | ||||
type: ScriptType_InNode, | type: ScriptType_InNode, | ||||
payload: string, | payload: string, | ||||
expectedOutputScript: string, | expectedOutputScript: string, | ||||
) => { | ) => { | ||||
const chronikScript = chronik.script(type, payload); | const chronikScript = chronik.script(type, payload); | ||||
const history = await chronikScript.history(); | const history = await chronikScript.history(); | ||||
const confirmedTxs = await chronikScript.confirmedTxs(); | |||||
const unconfirmedTxs = await chronikScript.unconfirmedTxs(); | |||||
const utxos = await chronikScript.utxos(); | const utxos = await chronikScript.utxos(); | ||||
// Expect empty history | // Expect empty history | ||||
expect(history).to.deep.equal({ txs: [], numPages: 0, numTxs: 0 }); | expect(history).to.deep.equal({ txs: [], numPages: 0, numTxs: 0 }); | ||||
// Expect empty confirmed txs | |||||
expect(confirmedTxs).to.deep.equal({ | |||||
txs: [], | |||||
numPages: 0, | |||||
numTxs: 0, | |||||
}); | |||||
// Expect empty unconfirmed txs | |||||
expect(unconfirmedTxs).to.deep.equal({ | |||||
txs: [], | |||||
numPages: 0, | |||||
numTxs: 0, | |||||
}); | |||||
// Hash is returned at the outputScript key, no utxos | // Hash is returned at the outputScript key, no utxos | ||||
expect(utxos).to.deep.equal({ | expect(utxos).to.deep.equal({ | ||||
outputScript: expectedOutputScript, | outputScript: expectedOutputScript, | ||||
utxos: [], | utxos: [], | ||||
}); | }); | ||||
console.log('\x1b[32m%s\x1b[0m', `✔ ${type}`); | console.log('\x1b[32m%s\x1b[0m', `✔ ${type}`); | ||||
}; | }; | ||||
// p2pkh | // p2pkh | ||||
await checkEmptyHistoryAndUtxos( | await checkEmptyScriptMethods( | ||||
chronik, | chronik, | ||||
'p2pkh', | 'p2pkh', | ||||
p2pkhAddressHash, | p2pkhAddressHash, | ||||
cashaddr.getOutputScriptFromAddress(p2pkhAddress), | cashaddr.getOutputScriptFromAddress(p2pkhAddress), | ||||
); | ); | ||||
// p2sh | // p2sh | ||||
await checkEmptyHistoryAndUtxos( | await checkEmptyScriptMethods( | ||||
chronik, | chronik, | ||||
'p2sh', | 'p2sh', | ||||
p2shAddressHash, | p2shAddressHash, | ||||
cashaddr.getOutputScriptFromAddress(p2shAddress), | cashaddr.getOutputScriptFromAddress(p2shAddress), | ||||
); | ); | ||||
// p2pk | // p2pk | ||||
p2pkScriptBytecountHex = (p2pkScript.length / 2).toString(16); | p2pkScriptBytecountHex = (p2pkScript.length / 2).toString(16); | ||||
await checkEmptyHistoryAndUtxos( | await checkEmptyScriptMethods( | ||||
chronik, | chronik, | ||||
'p2pk', | 'p2pk', | ||||
p2pkScript, | p2pkScript, | ||||
`${p2pkScriptBytecountHex}${p2pkScript}ac`, | `${p2pkScriptBytecountHex}${p2pkScript}ac`, | ||||
); | ); | ||||
// other | // other | ||||
await checkEmptyHistoryAndUtxos( | await checkEmptyScriptMethods( | ||||
chronik, | chronik, | ||||
'other', | 'other', | ||||
otherScript, | otherScript, | ||||
otherScript, | otherScript, | ||||
); | ); | ||||
// Expected errors | // Expected errors | ||||
const checkExpectedErrors = async ( | const checkExpectedErrors = async ( | ||||
chronik: ChronikClientNode, | chronik: ChronikClientNode, | ||||
type: ScriptType_InNode, | type: ScriptType_InNode, | ||||
) => { | ) => { | ||||
const nonHexPayload = 'justsomestring'; | const nonHexPayload = 'justsomestring'; | ||||
const chronikScriptNonHexPayload = chronik.script( | const chronikScriptNonHexPayload = chronik.script( | ||||
type, | type, | ||||
nonHexPayload, | nonHexPayload, | ||||
); | ); | ||||
await expect( | await expect( | ||||
chronikScriptNonHexPayload.history(), | chronikScriptNonHexPayload.history(), | ||||
).to.be.rejectedWith( | ).to.be.rejectedWith( | ||||
Error, | Error, | ||||
`Failed getting /script/${type}/${nonHexPayload}/history?page=0&page_size=25 (): 400: Invalid hex: Invalid character '${nonHexPayload[0]}' at position 0`, | `Failed getting /script/${type}/${nonHexPayload}/history?page=0&page_size=25 (): 400: Invalid hex: Invalid character '${nonHexPayload[0]}' at position 0`, | ||||
); | ); | ||||
await expect( | |||||
chronikScriptNonHexPayload.confirmedTxs(), | |||||
).to.be.rejectedWith( | |||||
Error, | |||||
`Failed getting /script/${type}/${nonHexPayload}/confirmed-txs?page=0&page_size=25 (): 400: Invalid hex: Invalid character '${nonHexPayload[0]}' at position 0`, | |||||
); | |||||
await expect( | |||||
chronikScriptNonHexPayload.unconfirmedTxs(), | |||||
).to.be.rejectedWith( | |||||
Error, | |||||
`Failed getting /script/${type}/${nonHexPayload}/unconfirmed-txs?page=0&page_size=25 (): 400: Invalid hex: Invalid character '${nonHexPayload[0]}' at position 0`, | |||||
); | |||||
await expect(chronikScriptNonHexPayload.utxos()).to.be.rejectedWith( | await expect(chronikScriptNonHexPayload.utxos()).to.be.rejectedWith( | ||||
Error, | Error, | ||||
`Failed getting /script/${type}/${nonHexPayload}/utxos (): 400: Invalid hex: Invalid character '${nonHexPayload[0]}' at position 0`, | `Failed getting /script/${type}/${nonHexPayload}/utxos (): 400: Invalid hex: Invalid character '${nonHexPayload[0]}' at position 0`, | ||||
); | ); | ||||
const hexPayload = 'deadbeef'; | const hexPayload = 'deadbeef'; | ||||
const chronikScriptHexPayload = chronik.script(type, hexPayload); | const chronikScriptHexPayload = chronik.script(type, hexPayload); | ||||
if (type === 'p2pkh' || type == 'p2sh') { | if (type === 'p2pkh' || type == 'p2sh') { | ||||
await expect( | await expect( | ||||
chronikScriptHexPayload.history(), | chronikScriptHexPayload.history(), | ||||
).to.be.rejectedWith( | ).to.be.rejectedWith( | ||||
Error, | Error, | ||||
`Failed getting /script/${type}/${hexPayload}/history?page=0&page_size=25 (): 400: Invalid payload for ${type.toUpperCase()}: Invalid length, expected 20 bytes but got 4 bytes`, | `Failed getting /script/${type}/${hexPayload}/history?page=0&page_size=25 (): 400: Invalid payload for ${type.toUpperCase()}: Invalid length, expected 20 bytes but got 4 bytes`, | ||||
); | ); | ||||
await expect( | await expect( | ||||
chronikScriptHexPayload.confirmedTxs(), | |||||
).to.be.rejectedWith( | |||||
Error, | |||||
`Failed getting /script/${type}/${hexPayload}/confirmed-txs?page=0&page_size=25 (): 400: Invalid payload for ${type.toUpperCase()}: Invalid length, expected 20 bytes but got 4 bytes`, | |||||
); | |||||
await expect( | |||||
chronikScriptHexPayload.unconfirmedTxs(), | |||||
).to.be.rejectedWith( | |||||
Error, | |||||
`Failed getting /script/${type}/${hexPayload}/unconfirmed-txs?page=0&page_size=25 (): 400: Invalid payload for ${type.toUpperCase()}: Invalid length, expected 20 bytes but got 4 bytes`, | |||||
); | |||||
await expect( | |||||
chronikScriptHexPayload.utxos(), | chronikScriptHexPayload.utxos(), | ||||
).to.be.rejectedWith( | ).to.be.rejectedWith( | ||||
Error, | Error, | ||||
`Failed getting /script/${type}/${hexPayload}/utxos (): 400: Invalid payload for ${type.toUpperCase()}: Invalid length, expected 20 bytes but got 4 bytes`, | `Failed getting /script/${type}/${hexPayload}/utxos (): 400: Invalid payload for ${type.toUpperCase()}: Invalid length, expected 20 bytes but got 4 bytes`, | ||||
); | ); | ||||
} | } | ||||
if (type === 'p2pk') { | if (type === 'p2pk') { | ||||
await expect( | await expect( | ||||
chronikScriptHexPayload.history(), | chronikScriptHexPayload.history(), | ||||
).to.be.rejectedWith( | ).to.be.rejectedWith( | ||||
Error, | Error, | ||||
`Failed getting /script/${type}/${hexPayload}/history?page=0&page_size=25 (): 400: Invalid payload for ${type.toUpperCase()}: Invalid length, expected one of [33, 65] but got 4 bytes`, | `Failed getting /script/${type}/${hexPayload}/history?page=0&page_size=25 (): 400: Invalid payload for ${type.toUpperCase()}: Invalid length, expected one of [33, 65] but got 4 bytes`, | ||||
); | ); | ||||
await expect( | await expect( | ||||
chronikScriptHexPayload.confirmedTxs(), | |||||
).to.be.rejectedWith( | |||||
Error, | |||||
`Failed getting /script/${type}/${hexPayload}/confirmed-txs?page=0&page_size=25 (): 400: Invalid payload for ${type.toUpperCase()}: Invalid length, expected one of [33, 65] but got 4 bytes`, | |||||
); | |||||
await expect( | |||||
chronikScriptHexPayload.unconfirmedTxs(), | |||||
).to.be.rejectedWith( | |||||
Error, | |||||
`Failed getting /script/${type}/${hexPayload}/unconfirmed-txs?page=0&page_size=25 (): 400: Invalid payload for ${type.toUpperCase()}: Invalid length, expected one of [33, 65] but got 4 bytes`, | |||||
); | |||||
await expect( | |||||
chronikScriptHexPayload.utxos(), | chronikScriptHexPayload.utxos(), | ||||
).to.be.rejectedWith( | ).to.be.rejectedWith( | ||||
Error, | Error, | ||||
`Failed getting /script/${type}/${hexPayload}/utxos (): 400: Invalid payload for ${type.toUpperCase()}: Invalid length, expected one of [33, 65] but got 4 bytes`, | `Failed getting /script/${type}/${hexPayload}/utxos (): 400: Invalid payload for ${type.toUpperCase()}: Invalid length, expected one of [33, 65] but got 4 bytes`, | ||||
); | ); | ||||
} | } | ||||
console.log( | console.log( | ||||
'\x1b[32m%s\x1b[0m', | '\x1b[32m%s\x1b[0m', | ||||
`✔ ${type} throws expected errors`, | `✔ ${type} throws expected errors`, | ||||
); | ); | ||||
}; | }; | ||||
await checkExpectedErrors(chronik, 'p2pkh'); | await checkExpectedErrors(chronik, 'p2pkh'); | ||||
await checkExpectedErrors(chronik, 'p2sh'); | await checkExpectedErrors(chronik, 'p2sh'); | ||||
await checkExpectedErrors(chronik, 'p2pk'); | await checkExpectedErrors(chronik, 'p2pk'); | ||||
await checkExpectedErrors(chronik, 'other'); | await checkExpectedErrors(chronik, 'other'); | ||||
// 'other' endpoint will not throw an error on ridiculously long valid hex | // 'other' endpoint will not throw an error on ridiculously long valid hex | ||||
// 440 bytes | // 440 bytes | ||||
const outTherePayload = | const outTherePayload = | ||||
'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef'; | 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef'; | ||||
await checkEmptyHistoryAndUtxos( | await checkEmptyScriptMethods( | ||||
chronik, | chronik, | ||||
'other', | 'other', | ||||
outTherePayload, | outTherePayload, | ||||
outTherePayload, | outTherePayload, | ||||
); | ); | ||||
}); | }); | ||||
it('After some txs have been broadcast', async () => { | it('After some txs have been broadcast', async () => { | ||||
txsBroadcast = parseInt(await get_txs_broadcast); | txsBroadcast = parseInt(await get_txs_broadcast); | ||||
const chronik = new ChronikClientNode(chronikUrl); | const chronik = new ChronikClientNode(chronikUrl); | ||||
const checkHistoryAndUtxosInMempool = async ( | const checkScriptMethodsInMempool = async ( | ||||
chronik: ChronikClientNode, | chronik: ChronikClientNode, | ||||
type: ScriptType_InNode, | type: ScriptType_InNode, | ||||
payload: string, | payload: string, | ||||
expectedOutputScript: string, | expectedOutputScript: string, | ||||
broadcastTxids: string[], | broadcastTxids: string[], | ||||
) => { | ) => { | ||||
const chronikScript = chronik.script(type, payload); | const chronikScript = chronik.script(type, payload); | ||||
// Use broadcastTxids.length for page size, so that we can be sure the first page has all the txs | // Use broadcastTxids.length for page size, so that we can be sure the first page has all the txs | ||||
// Test pagination separately | // Test pagination separately | ||||
const history = await chronikScript.history( | const history = await chronikScript.history( | ||||
0, | 0, | ||||
broadcastTxids.length, | broadcastTxids.length, | ||||
); | ); | ||||
// within history txs, confirmed txs are sorted in block order, unconfirmed txs are sorted by timeFirstSeen | |||||
// i.e., history.txs[0] will have the highest timeFirstSeen | |||||
// For txs with the same timeFirstSeen, the alphabetically-last txs appears first | |||||
const historyClone: Tx_InNode[] = JSON.parse( | |||||
JSON.stringify(history.txs), | |||||
); | |||||
// Sort historyClone by timeFirstSeen and then by txid | |||||
historyClone.sort( | |||||
(b, a) => | |||||
a.timeFirstSeen - b.timeFirstSeen || | |||||
a.txid.localeCompare(b.txid), | |||||
); | |||||
expect(history.txs).to.deep.equal(historyClone); | |||||
const confirmedTxs = await chronikScript.confirmedTxs( | |||||
0, | |||||
broadcastTxids.length, | |||||
); | |||||
const unconfirmedTxs = await chronikScript.unconfirmedTxs( | |||||
0, | |||||
broadcastTxids.length, | |||||
); | |||||
// If all txs are in the mempool, unconfirmedTxs matches what we get for history | |||||
// unconfirmed txs are sorted in chronological order, tiebreaker txid alphabetical | |||||
// NB we also expec the exact txs as the history endpoint, so we sort that output for our comparison | |||||
historyClone.sort( | |||||
// Note the order of a,b, now we sort chronologically and alphabetically (not reverse of both) | |||||
(a, b) => | |||||
a.timeFirstSeen - b.timeFirstSeen || | |||||
a.txid.localeCompare(b.txid), | |||||
); | |||||
expect(unconfirmedTxs.txs).to.deep.equal(historyClone); | |||||
const utxos = await chronikScript.utxos(); | const utxos = await chronikScript.utxos(); | ||||
// fetched history tx count is the same as txids broadcast to this address | // fetched history tx count is the same as txids broadcast to this address | ||||
expect(history.numTxs).to.eql(broadcastTxids.length); | expect(history.numTxs).to.eql(broadcastTxids.length); | ||||
const historyTxids = []; | const historyTxids = []; | ||||
for (const fetchedHistoryTx of history.txs) { | for (const fetchedHistoryTx of history.txs) { | ||||
historyTxids.push(fetchedHistoryTx.txid); | historyTxids.push(fetchedHistoryTx.txid); | ||||
// The 'block' key is undefined, denoting an unconfirmed tx | // The 'block' key is undefined, denoting an unconfirmed tx | ||||
expect(typeof fetchedHistoryTx.block).to.eql('undefined'); | expect(typeof fetchedHistoryTx.block).to.eql('undefined'); | ||||
} | } | ||||
// txids fetched from history match what the node broadcast | // txids fetched from history match what the node broadcast | ||||
expect(historyTxids).to.have.members(broadcastTxids); | expect(historyTxids).to.have.members(broadcastTxids); | ||||
// If all txs are in the mempool, confirmedTxs is empty | |||||
expect(confirmedTxs).to.deep.equal({ | |||||
txs: [], | |||||
numPages: 0, | |||||
numTxs: 0, | |||||
}); | |||||
// The returned outputScript matches the calling script hash | // The returned outputScript matches the calling script hash | ||||
expect(utxos.outputScript).to.eql(expectedOutputScript); | expect(utxos.outputScript).to.eql(expectedOutputScript); | ||||
// We have as many utxos as there were txs sent to this address | // We have as many utxos as there were txs sent to this address | ||||
expect(utxos.utxos.length).to.eql(broadcastTxids.length); | expect(utxos.utxos.length).to.eql(broadcastTxids.length); | ||||
const utxoTxids = []; | const utxoTxids = []; | ||||
for (const utxo of utxos.utxos) { | for (const utxo of utxos.utxos) { | ||||
Show All 9 Lines | it('After some txs have been broadcast', async () => { | ||||
// utxos fetched from history match what the node broadcast | // utxos fetched from history match what the node broadcast | ||||
expect(utxoTxids).to.have.members(broadcastTxids); | expect(utxoTxids).to.have.members(broadcastTxids); | ||||
console.log('\x1b[32m%s\x1b[0m', `✔ ${type}`); | console.log('\x1b[32m%s\x1b[0m', `✔ ${type}`); | ||||
}; | }; | ||||
// p2pkh | // p2pkh | ||||
p2pkhTxids = await get_p2pkh_txids; | p2pkhTxids = await get_p2pkh_txids; | ||||
await checkHistoryAndUtxosInMempool( | await checkScriptMethodsInMempool( | ||||
chronik, | chronik, | ||||
'p2pkh', | 'p2pkh', | ||||
p2pkhAddressHash, | p2pkhAddressHash, | ||||
cashaddr.getOutputScriptFromAddress(p2pkhAddress), | cashaddr.getOutputScriptFromAddress(p2pkhAddress), | ||||
p2pkhTxids, | p2pkhTxids, | ||||
); | ); | ||||
// p2sh | // p2sh | ||||
p2shTxids = await get_p2sh_txids; | p2shTxids = await get_p2sh_txids; | ||||
await checkHistoryAndUtxosInMempool( | await checkScriptMethodsInMempool( | ||||
chronik, | chronik, | ||||
'p2sh', | 'p2sh', | ||||
p2shAddressHash, | p2shAddressHash, | ||||
cashaddr.getOutputScriptFromAddress(p2shAddress), | cashaddr.getOutputScriptFromAddress(p2shAddress), | ||||
p2shTxids, | p2shTxids, | ||||
); | ); | ||||
// p2pk | // p2pk | ||||
p2pkTxids = await get_p2pk_txids; | p2pkTxids = await get_p2pk_txids; | ||||
await checkHistoryAndUtxosInMempool( | await checkScriptMethodsInMempool( | ||||
chronik, | chronik, | ||||
'p2pk', | 'p2pk', | ||||
p2pkScript, | p2pkScript, | ||||
`${p2pkScriptBytecountHex}${p2pkScript}ac`, | `${p2pkScriptBytecountHex}${p2pkScript}ac`, | ||||
p2pkTxids, | p2pkTxids, | ||||
); | ); | ||||
// other | // other | ||||
otherTxids = await get_other_txids; | otherTxids = await get_other_txids; | ||||
await checkHistoryAndUtxosInMempool( | await checkScriptMethodsInMempool( | ||||
chronik, | chronik, | ||||
'other', | 'other', | ||||
otherScript, | otherScript, | ||||
otherScript, | otherScript, | ||||
otherTxids, | otherTxids, | ||||
); | ); | ||||
const checkPagination = async ( | const checkPagination = async ( | ||||
▲ Show 20 Lines • Show All 68 Lines • ▼ Show 20 Lines | it('After some txs have been broadcast', async () => { | ||||
// p2pk pagination | // p2pk pagination | ||||
await checkPagination(chronik, 'p2pk', p2pkScript, txsBroadcast, 20); | await checkPagination(chronik, 'p2pk', p2pkScript, txsBroadcast, 20); | ||||
// other pagination | // other pagination | ||||
await checkPagination(chronik, 'other', otherScript, txsBroadcast, 50); | await checkPagination(chronik, 'other', otherScript, txsBroadcast, 50); | ||||
}); | }); | ||||
it('After these txs are mined', async () => { | it('After these txs are mined', async () => { | ||||
const chronik = new ChronikClientNode(chronikUrl); | const chronik = new ChronikClientNode(chronikUrl); | ||||
const checkHistoryAndUtxosAfterConfirmation = async ( | const checkScriptMethodsAfterConfirmation = async ( | ||||
chronik: ChronikClientNode, | chronik: ChronikClientNode, | ||||
type: ScriptType_InNode, | type: ScriptType_InNode, | ||||
payload: string, | payload: string, | ||||
expectedOutputScript: string, | expectedOutputScript: string, | ||||
broadcastTxids: string[], | broadcastTxids: string[], | ||||
) => { | ) => { | ||||
const chronikScript = chronik.script(type, payload); | const chronikScript = chronik.script(type, payload); | ||||
// Use broadcastTxids.length for page size, so that we can be sure the first page has all the txs | // Use broadcastTxids.length for page size, so that we can be sure the first page has all the txs | ||||
// Test pagination separately | // Test pagination separately | ||||
const history = await chronikScript.history( | const history = await chronikScript.history( | ||||
0, | 0, | ||||
broadcastTxids.length, | broadcastTxids.length, | ||||
); | ); | ||||
// Clone history.txs to test sorting | |||||
const historyClone: Tx_InNode[] = JSON.parse( | |||||
JSON.stringify(history.txs), | |||||
); | |||||
// history txs within blocks sorting | |||||
// The history endpoint returns confirmed txs sorted by timeFirstSeen (high to low) and then by txid (alphabetical last to first) | |||||
historyClone.sort( | |||||
(b, a) => | |||||
a.timeFirstSeen - b.timeFirstSeen || | |||||
a.txid.localeCompare(b.txid), | |||||
); | |||||
expect(history.txs).to.deep.equal(historyClone); | |||||
const confirmedTxs = await chronikScript.confirmedTxs( | |||||
0, | |||||
broadcastTxids.length, | |||||
); | |||||
const unconfirmedTxs = await chronikScript.unconfirmedTxs( | |||||
0, | |||||
broadcastTxids.length, | |||||
); | |||||
const utxos = await chronikScript.utxos(); | const utxos = await chronikScript.utxos(); | ||||
// fetched history tx count is the same as txids broadcast to this address | // fetched history tx count is the same as txids broadcast to this address | ||||
expect(history.numTxs).to.eql(broadcastTxids.length); | expect(history.numTxs).to.eql(broadcastTxids.length); | ||||
const historyTxids = []; | const historyTxids = []; | ||||
for (const fetchedHistoryTx of history.txs) { | for (const fetchedHistoryTx of history.txs) { | ||||
historyTxids.push(fetchedHistoryTx.txid); | historyTxids.push(fetchedHistoryTx.txid); | ||||
// We now have a blockheight | // We now have a blockheight | ||||
expect(fetchedHistoryTx.block?.height).to.eql( | expect(fetchedHistoryTx.block?.height).to.eql( | ||||
REGTEST_CHAIN_INIT_HEIGHT + 1, | REGTEST_CHAIN_INIT_HEIGHT + 1, | ||||
); | ); | ||||
} | } | ||||
// txids fetched from history match what the node broadcast | // txids fetched from history match what the node broadcast | ||||
expect(historyTxids).to.have.members(broadcastTxids); | expect(historyTxids).to.have.members(broadcastTxids); | ||||
// If all txs are mined, unconfirmedTxs is empty | |||||
expect(unconfirmedTxs).to.deep.equal({ | |||||
txs: [], | |||||
numPages: 0, | |||||
numTxs: 0, | |||||
}); | |||||
// If all txs are mined, confirmedTxs matches history | |||||
// Confirmed txs are sorted by block order | |||||
// coinbase txs come first, then alphabetically | |||||
// we have no coinbase txs, so alphabetically | |||||
historyClone.sort((a, b) => a.txid.localeCompare(b.txid)); | |||||
// confirmedTxs txs are sorted in block order, txid alphabetical | |||||
// and to have the same txs as history | |||||
expect(confirmedTxs.txs).to.deep.equal(historyClone); | |||||
// The returned outputScript matches the calling script hash | // The returned outputScript matches the calling script hash | ||||
expect(utxos.outputScript).to.eql(expectedOutputScript); | expect(utxos.outputScript).to.eql(expectedOutputScript); | ||||
// We have as many utxos as there were txs sent to this address | // We have as many utxos as there were txs sent to this address | ||||
expect(utxos.utxos.length).to.eql(broadcastTxids.length); | expect(utxos.utxos.length).to.eql(broadcastTxids.length); | ||||
const utxoTxids = []; | const utxoTxids = []; | ||||
for (const utxo of utxos.utxos) { | for (const utxo of utxos.utxos) { | ||||
// Each utxo is now confirmed at the right blockheight | // Each utxo is now confirmed at the right blockheight | ||||
expect(utxo.blockHeight).to.eql(REGTEST_CHAIN_INIT_HEIGHT + 1); | expect(utxo.blockHeight).to.eql(REGTEST_CHAIN_INIT_HEIGHT + 1); | ||||
// The utxo is not from a finalized tx | // The utxo is not from a finalized tx | ||||
expect(utxo.isFinal).to.eql(false); | expect(utxo.isFinal).to.eql(false); | ||||
utxoTxids.push(utxo.outpoint.txid); | utxoTxids.push(utxo.outpoint.txid); | ||||
} | } | ||||
// utxos fetched from history match what the node broadcast | // utxos fetched from history match what the node broadcast | ||||
expect(utxoTxids).to.have.members(broadcastTxids); | expect(utxoTxids).to.have.members(broadcastTxids); | ||||
console.log('\x1b[32m%s\x1b[0m', `✔ ${type}`); | console.log('\x1b[32m%s\x1b[0m', `✔ ${type}`); | ||||
}; | }; | ||||
// p2pkh | // p2pkh | ||||
await checkHistoryAndUtxosAfterConfirmation( | await checkScriptMethodsAfterConfirmation( | ||||
chronik, | chronik, | ||||
'p2pkh', | 'p2pkh', | ||||
p2pkhAddressHash, | p2pkhAddressHash, | ||||
cashaddr.getOutputScriptFromAddress(p2pkhAddress), | cashaddr.getOutputScriptFromAddress(p2pkhAddress), | ||||
p2pkhTxids, | p2pkhTxids, | ||||
); | ); | ||||
// p2sh | // p2sh | ||||
await checkHistoryAndUtxosAfterConfirmation( | await checkScriptMethodsAfterConfirmation( | ||||
chronik, | chronik, | ||||
'p2sh', | 'p2sh', | ||||
p2shAddressHash, | p2shAddressHash, | ||||
cashaddr.getOutputScriptFromAddress(p2shAddress), | cashaddr.getOutputScriptFromAddress(p2shAddress), | ||||
p2shTxids, | p2shTxids, | ||||
); | ); | ||||
// p2pk | // p2pk | ||||
await checkHistoryAndUtxosAfterConfirmation( | await checkScriptMethodsAfterConfirmation( | ||||
chronik, | chronik, | ||||
'p2pk', | 'p2pk', | ||||
p2pkScript, | p2pkScript, | ||||
`${p2pkScriptBytecountHex}${p2pkScript}ac`, | `${p2pkScriptBytecountHex}${p2pkScript}ac`, | ||||
p2pkTxids, | p2pkTxids, | ||||
); | ); | ||||
// other | // other | ||||
await checkHistoryAndUtxosAfterConfirmation( | await checkScriptMethodsAfterConfirmation( | ||||
chronik, | chronik, | ||||
'other', | 'other', | ||||
otherScript, | otherScript, | ||||
otherScript, | otherScript, | ||||
otherTxids, | otherTxids, | ||||
); | ); | ||||
}); | }); | ||||
it('After these txs are avalanche finalized', async () => { | it('After these txs are avalanche finalized', async () => { | ||||
▲ Show 20 Lines • Show All 78 Lines • ▼ Show 20 Lines | it('After a tx is broadcast with outputs of each type', async () => { | ||||
chronik: ChronikClientNode, | chronik: ChronikClientNode, | ||||
type: ScriptType_InNode, | type: ScriptType_InNode, | ||||
payload: string, | payload: string, | ||||
mixedTxid: string, | mixedTxid: string, | ||||
txsBroadcast: number, | txsBroadcast: number, | ||||
) => { | ) => { | ||||
const chronikScript = chronik.script(type, payload); | const chronikScript = chronik.script(type, payload); | ||||
const history = await chronikScript.history(); | const history = await chronikScript.history(); | ||||
const unconfirmedTxs = await chronikScript.unconfirmedTxs(); | |||||
const utxos = await chronikScript.utxos(); | const utxos = await chronikScript.utxos(); | ||||
// We see a new tx in numTxs count | // We see a new tx in numTxs count | ||||
expect(history.numTxs).to.eql(txsBroadcast + 1); | expect(history.numTxs).to.eql(txsBroadcast + 1); | ||||
// The most recent txid appears at the first element of the tx history array | // The most recent txid appears at the first element of the tx history array | ||||
expect(history.txs[0].txid).to.eql(mixedTxid); | expect(history.txs[0].txid).to.eql(mixedTxid); | ||||
// We can also get this tx from unconfirmedTxs | |||||
expect(unconfirmedTxs.txs[0].txid).to.eql(mixedTxid); | |||||
// The most recent txid appears at the last element of the utxos array | // The most recent txid appears at the last element of the utxos array | ||||
expect(utxos.utxos[utxos.utxos.length - 1].outpoint.txid).to.eql( | expect(utxos.utxos[utxos.utxos.length - 1].outpoint.txid).to.eql( | ||||
mixedTxid, | mixedTxid, | ||||
); | ); | ||||
console.log( | console.log( | ||||
`${type} script endpoints registed tx with mixed outputs`, | `${type} script endpoints registed tx with mixed outputs`, | ||||
); | ); | ||||
}; | }; | ||||
Show All 34 Lines |