diff --git a/modules/chronik-client/package.json b/modules/chronik-client/package.json --- a/modules/chronik-client/package.json +++ b/modules/chronik-client/package.json @@ -11,7 +11,7 @@ "build": "tsc", "build-docs": "typedoc --out docs index.ts", "test": "mocha -r ts-node/register test/test.ts", - "integration-tests": "mocha -j1 -r ts-node/register test/integration/*.ts", + "integration-tests": "mocha -j1 -r ts-node/register test/integration/script_endpoints.ts", "test-long": "mocha -r ts-node/register test/test.ts --timeout 1000000000", "format": "prettier --write .", "lint": "eslint . --ext .js,.jsx,.ts,.tsx && prettier --check .", diff --git a/modules/chronik-client/src/ChronikClientNode.ts b/modules/chronik-client/src/ChronikClientNode.ts --- a/modules/chronik-client/src/ChronikClientNode.ts +++ b/modules/chronik-client/src/ChronikClientNode.ts @@ -223,6 +223,46 @@ }; } + /** + * Fetches the confirmed tx history of this tokenId, in anti-chronological order. + * @param page Page index of the tx history. + * @param pageSize Number of txs per page. + */ + public async confirmedTxs( + page = 0, // Get the first page if unspecified + pageSize = 25, // Must be less than 200, let server handle error as server setting could change + ): Promise { + const data = await this._proxyInterface.get( + `/script/${this._scriptType}/${this._scriptPayload}/confirmed-txs?page=${page}&page_size=${pageSize}`, + ); + const historyPage = proto.TxHistoryPage.decode(data); + return { + txs: historyPage.txs.map(convertToTx), + numPages: historyPage.numPages, + numTxs: historyPage.numTxs, + }; + } + + /** + * Fetches the unconfirmed tx history of this tokenId, in anti-chronological order. + * @param page Page index of the tx history. + * @param pageSize Number of txs per page. + */ + public async unconfirmedTxs( + page = 0, // Get the first page if unspecified + pageSize = 25, // Must be less than 200, let server handle error as server setting could change + ): Promise { + const data = await this._proxyInterface.get( + `/script/${this._scriptType}/${this._scriptPayload}/unconfirmed-txs?page=${page}&page_size=${pageSize}`, + ); + const historyPage = proto.TxHistoryPage.decode(data); + return { + txs: historyPage.txs.map(convertToTx), + numPages: historyPage.numPages, + numTxs: historyPage.numTxs, + }; + } + /** * Fetches the current UTXO set for this script. * It is grouped by output script, in case a script type can match multiple diff --git a/modules/chronik-client/test/integration/script_endpoints.ts b/modules/chronik-client/test/integration/script_endpoints.ts --- a/modules/chronik-client/test/integration/script_endpoints.ts +++ b/modules/chronik-client/test/integration/script_endpoints.ts @@ -204,7 +204,7 @@ 'Invalid address: notAnAddress.', ); - const checkEmptyHistoryAndUtxos = async ( + const checkEmptyScriptMethods = async ( chronik: ChronikClientNode, type: ScriptType_InNode, payload: string, @@ -212,11 +212,27 @@ ) => { const chronikScript = chronik.script(type, payload); const history = await chronikScript.history(); + const confirmedTxs = await chronikScript.confirmedTxs(); + const unconfirmedTxs = await chronikScript.unconfirmedTxs(); const utxos = await chronikScript.utxos(); // Expect empty history 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 expect(utxos).to.deep.equal({ outputScript: expectedOutputScript, @@ -227,7 +243,7 @@ }; // p2pkh - await checkEmptyHistoryAndUtxos( + await checkEmptyScriptMethods( chronik, 'p2pkh', p2pkhAddressHash, @@ -235,7 +251,7 @@ ); // p2sh - await checkEmptyHistoryAndUtxos( + await checkEmptyScriptMethods( chronik, 'p2sh', p2shAddressHash, @@ -244,7 +260,7 @@ // p2pk p2pkScriptBytecountHex = (p2pkScript.length / 2).toString(16); - await checkEmptyHistoryAndUtxos( + await checkEmptyScriptMethods( chronik, 'p2pk', p2pkScript, @@ -252,7 +268,7 @@ ); // other - await checkEmptyHistoryAndUtxos( + await checkEmptyScriptMethods( chronik, 'other', otherScript, @@ -275,6 +291,18 @@ Error, `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( Error, `Failed getting /script/${type}/${nonHexPayload}/utxos (): 400: Invalid hex: Invalid character '${nonHexPayload[0]}' at position 0`, @@ -290,6 +318,18 @@ 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`, ); + await expect( + chronikScriptHexPayload.confirmedTxs(), + ).to.be.rejectedWith( + Error, + `Failed getting /script/${type}/${hexPayload}/confirmed-txs?page=0&page_size=25 (): 400: Invalid hex: Invalid character '${nonHexPayload[0]}' at position 0`, + ); + await expect( + chronikScriptHexPayload.unconfirmedTxs(), + ).to.be.rejectedWith( + Error, + `Failed getting /script/${type}/${hexPayload}/unconfirmed-txs?page=0&page_size=25 (): 400: Invalid hex: Invalid character '${nonHexPayload[0]}' at position 0`, + ); await expect( chronikScriptHexPayload.utxos(), ).to.be.rejectedWith( @@ -304,6 +344,18 @@ 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`, ); + await expect( + chronikScriptHexPayload.confirmedTxs(), + ).to.be.rejectedWith( + Error, + `Failed getting /script/${type}/${hexPayload}/confirmed-txs?page=0&page_size=25 (): 400: Invalid hex: Invalid character '${nonHexPayload[0]}' at position 0`, + ); + await expect( + chronikScriptHexPayload.unconfirmedTxs(), + ).to.be.rejectedWith( + Error, + `Failed getting /script/${type}/${hexPayload}/unconfirmed-txs?page=0&page_size=25 (): 400: Invalid hex: Invalid character '${nonHexPayload[0]}' at position 0`, + ); await expect( chronikScriptHexPayload.utxos(), ).to.be.rejectedWith( @@ -326,7 +378,7 @@ // 440 bytes const outTherePayload = 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef'; - await checkEmptyHistoryAndUtxos( + await checkEmptyScriptMethods( chronik, 'other', outTherePayload, @@ -339,7 +391,7 @@ const chronik = new ChronikClientNode(chronikUrl); - const checkHistoryAndUtxosInMempool = async ( + const checkScriptMethodsInMempool = async ( chronik: ChronikClientNode, type: ScriptType_InNode, payload: string, @@ -353,6 +405,14 @@ 0, broadcastTxids.length, ); + const confirmedTxs = await chronikScript.confirmedTxs( + 0, + broadcastTxids.length, + ); + const unconfirmedTxs = await chronikScript.unconfirmedTxs( + 0, + broadcastTxids.length, + ); const utxos = await chronikScript.utxos(); // fetched history tx count is the same as txids broadcast to this address @@ -368,6 +428,16 @@ // txids fetched from history match what the node broadcast expect(historyTxids).to.have.members(broadcastTxids); + // If all txs are in the mempool, unconfirmedTxs matches what we get for history + expect(unconfirmedTxs).to.deep.equal(history); + + // 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 expect(utxos.outputScript).to.eql(expectedOutputScript); @@ -393,7 +463,7 @@ // p2pkh p2pkhTxids = await get_p2pkh_txids; - await checkHistoryAndUtxosInMempool( + await checkScriptMethodsInMempool( chronik, 'p2pkh', p2pkhAddressHash, @@ -403,7 +473,7 @@ // p2sh p2shTxids = await get_p2sh_txids; - await checkHistoryAndUtxosInMempool( + await checkScriptMethodsInMempool( chronik, 'p2sh', p2shAddressHash, @@ -413,7 +483,7 @@ // p2pk p2pkTxids = await get_p2pk_txids; - await checkHistoryAndUtxosInMempool( + await checkScriptMethodsInMempool( chronik, 'p2pk', p2pkScript, @@ -423,7 +493,7 @@ // other otherTxids = await get_other_txids; - await checkHistoryAndUtxosInMempool( + await checkScriptMethodsInMempool( chronik, 'other', otherScript, @@ -508,7 +578,7 @@ it('After these txs are mined', async () => { const chronik = new ChronikClientNode(chronikUrl); - const checkHistoryAndUtxosAfterConfirmation = async ( + const checkScriptMethodsAfterConfirmation = async ( chronik: ChronikClientNode, type: ScriptType_InNode, payload: string, @@ -522,6 +592,14 @@ 0, broadcastTxids.length, ); + const confirmedTxs = await chronikScript.confirmedTxs( + 0, + broadcastTxids.length, + ); + const unconfirmedTxs = await chronikScript.unconfirmedTxs( + 0, + broadcastTxids.length, + ); const utxos = await chronikScript.utxos(); // fetched history tx count is the same as txids broadcast to this address @@ -539,6 +617,16 @@ // txids fetched from history match what the node broadcast 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 + expect(confirmedTxs).to.deep.equal(history); + // The returned outputScript matches the calling script hash expect(utxos.outputScript).to.eql(expectedOutputScript); @@ -560,7 +648,7 @@ console.log('\x1b[32m%s\x1b[0m', `✔ ${type}`); }; // p2pkh - await checkHistoryAndUtxosAfterConfirmation( + await checkScriptMethodsAfterConfirmation( chronik, 'p2pkh', p2pkhAddressHash, @@ -569,7 +657,7 @@ ); // p2sh - await checkHistoryAndUtxosAfterConfirmation( + await checkScriptMethodsAfterConfirmation( chronik, 'p2sh', p2shAddressHash, @@ -578,7 +666,7 @@ ); // p2pk - await checkHistoryAndUtxosAfterConfirmation( + await checkScriptMethodsAfterConfirmation( chronik, 'p2pk', p2pkScript, @@ -587,7 +675,7 @@ ); // other - await checkHistoryAndUtxosAfterConfirmation( + await checkScriptMethodsAfterConfirmation( chronik, 'other', otherScript, @@ -682,11 +770,14 @@ ) => { const chronikScript = chronik.script(type, payload); const history = await chronikScript.history(); + const unconfirmedTxs = await chronikScript.unconfirmedTxs(); const utxos = await chronikScript.utxos(); // We see a new tx in numTxs count expect(history.numTxs).to.eql(txsBroadcast + 1); // The most recent txid appears at the first element of the tx history array 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 expect(utxos.utxos[utxos.utxos.length - 1].outpoint.txid).to.eql( mixedTxid,