diff --git a/modules/chronik-client/.mocharc.js b/modules/chronik-client/.mocharc.js --- a/modules/chronik-client/.mocharc.js +++ b/modules/chronik-client/.mocharc.js @@ -1,4 +1,4 @@ 'use strict'; module.exports = { - timeout: '20s', + timeout: '60s', }; diff --git a/modules/chronik-client/README.md b/modules/chronik-client/README.md --- a/modules/chronik-client/README.md +++ b/modules/chronik-client/README.md @@ -96,3 +96,5 @@ 0.18.0 - Add support for websocket connections to `ChronikClientNode` 0.19.0 - Add support for token data in tx inputs and outputs to `ChronikClientNode` 0.20.0 - Add support for calling script endpoints by address to `ChronikClientNode` +0.21.0 - Skipped as accidentally published 0.22.0 before diff approval at 0.21.1-rc.1 +0.22.0 - Add support for `tokenId` endpoints and token data in utxos to `ChronikClientNode` diff --git a/modules/chronik-client/package-lock.json b/modules/chronik-client/package-lock.json --- a/modules/chronik-client/package-lock.json +++ b/modules/chronik-client/package-lock.json @@ -1,12 +1,12 @@ { "name": "chronik-client", - "version": "0.20.0", + "version": "0.22.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "chronik-client", - "version": "0.20.0", + "version": "0.22.0", "license": "MIT", "dependencies": { "@types/ws": "^8.2.1", 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 @@ -1,6 +1,6 @@ { "name": "chronik-client", - "version": "0.20.0", + "version": "0.22.0", "description": "A client for accessing the Chronik Indexer API", "main": "dist/index.js", "types": "dist/index.d.ts", 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 @@ -143,6 +143,11 @@ return convertToRawTx(rawTx); } + /** Create object that allows fetching info about a given token */ + public tokenId(tokenId: string): TokenIdEndpoint { + return new TokenIdEndpoint(this._proxyInterface, tokenId); + } + /** Create object that allows fetching script history or UTXOs. */ public script( scriptType: ScriptType_InNode, @@ -228,6 +233,91 @@ } } +/** Allows fetching tokenId confirmedTxs, unconfirmedTxs, history, and UTXOs. */ +export class TokenIdEndpoint { + private _proxyInterface: FailoverProxy; + private _tokenId: string; + + constructor(proxyInterface: FailoverProxy, tokenId: string) { + this._proxyInterface = proxyInterface; + this._tokenId = tokenId; + } + + /** + * Fetches the 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 history( + 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( + `/token-id/${this._tokenId}/history?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 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( + `/token-id/${this._tokenId}/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( + `/token-id/${this._tokenId}/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 tokenId. + */ + public async utxos(): Promise { + const data = await this._proxyInterface.get( + `/token-id/${this._tokenId}/utxos`, + ); + const utxos = proto.Utxos.decode(data); + return { + tokenId: this._tokenId, + utxos: utxos.utxos.map(convertToUtxo), + }; + } +} + /** Config for a WebSocket connection to Chronik. */ export interface WsConfig_InNode { /** Fired when a message is sent from the WebSocket. */ @@ -587,7 +677,7 @@ if (utxo.outpoint === undefined) { throw new Error('UTXO outpoint is undefined'); } - return { + const utxoInNode: Utxo_InNode = { outpoint: { txid: toHexRev(utxo.outpoint.txid), outIdx: utxo.outpoint.outIdx, @@ -597,6 +687,11 @@ value: parseInt(utxo.value), isFinal: utxo.isFinal, }; + if (typeof utxo.token !== 'undefined') { + // We only return a token key if we have token data for this input + utxoInNode.token = convertToTokenInNode(utxo.token); + } + return utxoInNode; } function convertToTokenEntry(tokenEntry: proto.TokenEntry): TokenEntry { @@ -711,13 +806,19 @@ ); } - return { + const tokenInNode: Token_InNode = { tokenId: token.tokenId, tokenType: convertToTokenType(token.tokenType), - entryIdx: token.entryIdx, amount: token.amount, isMintBaton: token.isMintBaton, }; + + // We do not bother including entryIdx for utxos, where it is always -1 + if (token.entryIdx !== -1) { + tokenInNode.entryIdx = token.entryIdx; + } + + return tokenInNode; } function convertToBlockMsgType(msgType: proto.BlockMsgType): BlockMsgType { @@ -1063,6 +1164,8 @@ value: number; /** Is this utxo avalanche finalized */ isFinal: boolean; + /** Token value attached to this utxo */ + token?: Token_InNode; } /** Token coloring an input or output */ @@ -1071,8 +1174,12 @@ tokenId: string; /** Token type of the token */ tokenType: TokenType; - /** Index into `token_entries` for `Tx`. -1 for UTXOs */ - entryIdx: number; + /** + * Index into `token_entries` for `Tx` + * chronik returns -1 for UTXOs, chronik-client + * passes no entryIdx key for UTXOS + */ + entryIdx?: number; /** Base token amount of the input/output */ amount: string; /** Whether the token is a mint baton */ @@ -1163,3 +1270,11 @@ type: 'Error'; msg: string; } + +/** List of UTXOs */ +export interface TokenIdUtxos { + /** TokenId used to fetch these utxos */ + tokenId: string; + /** UTXOs */ + utxos: Utxo_InNode[]; +} diff --git a/modules/chronik-client/test/integration/token_alp.ts b/modules/chronik-client/test/integration/token_alp.ts --- a/modules/chronik-client/test/integration/token_alp.ts +++ b/modules/chronik-client/test/integration/token_alp.ts @@ -6,7 +6,12 @@ import chaiAsPromised from 'chai-as-promised'; import { ChildProcess } from 'node:child_process'; import { EventEmitter, once } from 'node:events'; -import { ChronikClientNode, Tx_InNode } from '../../index'; +import { + ChronikClientNode, + Token_InNode, + TxHistoryPage_InNode, + Tx_InNode, +} from '../../index'; import initializeTestRunner from '../setup/testRunner'; const expect = chai.expect; @@ -21,6 +26,8 @@ let get_alp_genesis2_txid: Promise; let get_alp_multi_txid: Promise; let get_alp_mega_txid: Promise; + let get_alp_mint_two_txid: Promise; + let get_alp_send_two_txid: Promise; const statusEvent = new EventEmitter(); before(async () => { @@ -69,6 +76,18 @@ }); } + if (message && message.alp_mint_two_txid) { + get_alp_mint_two_txid = new Promise(resolve => { + resolve(message.alp_mint_two_txid); + }); + } + + if (message && message.alp_send_two_txid) { + get_alp_send_two_txid = new Promise(resolve => { + resolve(message.alp_send_two_txid); + }); + } + if (message && message.status) { statusEvent.emit(message.status); } @@ -126,6 +145,8 @@ let alpNextGenesisTxid = ''; let alpMultiTxid = ''; let alpMegaTxid = ''; + let alpMintTwoTxid = ''; + let alpSendTwoTxid = ''; let alpGenesis: Tx_InNode; let alpMint: Tx_InNode; @@ -133,6 +154,10 @@ let alpNextGenesis: Tx_InNode; let alpMulti: Tx_InNode; let alpMega: Tx_InNode; + let alpMintTwo: Tx_InNode; + let alpSendTwo: Tx_InNode; + + let confirmedTxsForAlpGenesisTxid: TxHistoryPage_InNode; it('Gets an ALP genesis tx from the mempool', async () => { const chronikUrl = await chronik_url; @@ -157,7 +182,7 @@ ]); // We get expected outputs including expected Token data - expect(alpGenesis.outputs).to.deep.equal([ + const expectedOutputs = [ { ...BASE_TX_OUTPUT, value: 0, @@ -210,7 +235,8 @@ isMintBaton: true, }, }, - ]); + ]; + expect(alpGenesis.outputs).to.deep.equal(expectedOutputs); // We get a Entries of expected shape, with tokenId the txid for a genesis tx expect(alpGenesis.tokenEntries).to.deep.equal([ @@ -231,6 +257,61 @@ // Normal status expect(alpGenesis.tokenStatus).to.eql('TOKEN_STATUS_NORMAL'); + + // We can get the same token info from calling chronik.tokenId.utxos() on this genesis txid + const utxosByTokenId = await chronik.tokenId(alpGenesisTxid).utxos(); + + // We get the calling tokenId returned + expect(utxosByTokenId.tokenId).to.eql(alpGenesisTxid); + + // Utxos returned by token id include Token object matching the outputs, except they have no entryIdx + + // Get only the outputs that are token utxos for alpGenesisTxid + const outputsWithTokenKey = expectedOutputs.filter( + output => 'token' in output, + ); + + const utxoTokenKeysFromOutputs: Token_InNode[] = []; + for (const output of outputsWithTokenKey) { + if ('token' in output) { + const { token } = output; + // Remove the entryIdx key from these outputs, as we do not expect to see it in tokenId.utxos() output + delete (token as Token_InNode).entryIdx; + utxoTokenKeysFromOutputs.push(output.token as Token_InNode); + } + } + + // We have as many utxosByTokenId as we do outputs with token key + expect(utxosByTokenId.utxos.length).to.eql( + utxoTokenKeysFromOutputs.length, + ); + + // They match and are in the same order + for (let i = 0; i < utxosByTokenId.utxos.length; i += 1) { + expect(utxosByTokenId.utxos[i].token).to.deep.equal( + utxoTokenKeysFromOutputs[i], + ); + } + + // We get the same tx info for this tx from calling chronik.tokenId().unconfirmedTxs() + const unconfirmedTxsForThisTokenId = await chronik + .tokenId(alpGenesisTxid) + .unconfirmedTxs(); + expect(unconfirmedTxsForThisTokenId.txs.length).to.eql(1); + expect(unconfirmedTxsForThisTokenId.txs[0]).to.deep.equal(alpGenesis); + + // We get nothing from confirmedTxs() as none are confirmed + const confirmedTxsForThisTokenId = await chronik + .tokenId(alpGenesisTxid) + .confirmedTxs(); + expect(confirmedTxsForThisTokenId.txs.length).to.eql(0); + + // History returns the output of confirmed + unconfirmed (in this case, just unconfirmed) + const historyForThisTokenId = await chronik + .tokenId(alpGenesisTxid) + .history(); + expect(historyForThisTokenId.txs.length).to.eql(1); + expect(historyForThisTokenId.txs[0]).to.deep.equal(alpGenesis); }); it('Gets an ALP mint tx from the mempool', async () => { const chronikUrl = await chronik_url; @@ -607,6 +688,45 @@ // Normal status expect(alpMulti.tokenStatus).to.eql('TOKEN_STATUS_NORMAL'); + + // Test order of tokenId.history() + const unconfirmedTxs = await chronik + .tokenId(alpGenesisTxid) + .unconfirmedTxs(); + + const alphabeticalBroadcastAlpGenesisMempoolTxs = [ + alpGenesisTxid, + alpMintTxid, + alpSendTxid, + alpMultiTxid, + ].sort(); + + // Unconfirmed txs come in alphabetical order by txid + // Note that the genesis tx was broadcast first but is alphabetically 2nd + for (let i = 0; i < unconfirmedTxs.txs.length; i += 1) { + expect(unconfirmedTxs.txs[i].txid).to.eql( + alphabeticalBroadcastAlpGenesisMempoolTxs[i], + ); + } + + // Test order of tokenId.history() + const historyTxs = await chronik.tokenId(alpGenesisTxid).history(); + + // History txs are sorted by blockheight, then timeFirstSeen, then reverse alphabetical by txid + // These txs all have the same blockheight and timeFirstSeen, so we see them in reverse alphabetical order + const reverseAlphabeticalBroadcastAlpGenesisMempoolTxs = [ + alpGenesisTxid, + alpMintTxid, + alpSendTxid, + alpMultiTxid, + ] + .sort() + .reverse(); + for (let i = 0; i < historyTxs.txs.length; i += 1) { + expect(historyTxs.txs[i].txid).to.eql( + reverseAlphabeticalBroadcastAlpGenesisMempoolTxs[i], + ); + } }); it('Can get all of the above txs, and a wild mega-tx, from the blockTxs endpoint after they are mined in a block', async () => { const chronikUrl = await chronik_url; @@ -904,7 +1024,7 @@ alpMega, ].sort((a, b) => a.txid.localeCompare(b.txid)); - // The token fields of Tx_InNode(s) from blockTxs match the Tx_InNode(s) from tx] + // The token fields of Tx_InNode(s) from blockTxs match the Tx_InNode(s) from tx // Note the txs are not expected to fully match bc now we have block key and spentBy, // expected after confirmation // This type of functionality is tested in blocktxs_and_tx_and_rawtx.ts @@ -939,5 +1059,260 @@ // Same tx count as blockTxs expect(history.numTxs).to.eql(7); + + // Now we have no unconfirmed txs for the alpGenesisTxid + const unconfirmedTxsForThisTokenId = await chronik + .tokenId(alpGenesisTxid) + .unconfirmedTxs(); + expect(unconfirmedTxsForThisTokenId.txs.length).to.eql(0); + + // We can get all the confirmedTxs for alpGenesisTxid + // Note: they are in alphabetical order by txid (at least, in this block) + const broadcastAlpTxsOfAlpGenesisTokenId = [ + alpGenesis, + alpMint, + alpSend, + alpMulti, + alpMega, + ].sort((a, b) => a.txid.localeCompare(b.txid)); + + confirmedTxsForAlpGenesisTxid = await chronik + .tokenId(alpGenesisTxid) + .confirmedTxs(); + + expect(confirmedTxsForAlpGenesisTxid.txs.length).to.eql( + broadcastAlpTxsOfAlpGenesisTokenId.length, + ); + + // They are sorted by blockheight, then alphabetical by txid + // In this case, they all have the same blockheight -- so we see alphabetical by txid + // Note that timeFirstSeen is not considered from this endpoint, as alpMega has timeFirstSeen of 0 + // But it appears second here + for (let i = 0; i < confirmedTxsForAlpGenesisTxid.txs.length; i += 1) { + // in practice, everything matches except for the 'block' and 'output.spentBy' keys + // these are expected to have changed since we stored the txs when they were in the mempool + // now we are comparing result to confirmed txs + expect(confirmedTxsForAlpGenesisTxid.txs[i].txid).to.eql( + broadcastAlpTxsOfAlpGenesisTokenId[i].txid, + ); + expect(confirmedTxsForAlpGenesisTxid.txs[i].inputs).to.deep.equal( + broadcastAlpTxsOfAlpGenesisTokenId[i].inputs, + ); + expect( + confirmedTxsForAlpGenesisTxid.txs[i].tokenEntries, + ).to.deep.equal(broadcastAlpTxsOfAlpGenesisTokenId[i].tokenEntries); + } + }); + it('Can get confirmed and unconfirmed txs from tokenId.history()', async () => { + const chronikUrl = await chronik_url; + const chronik = new ChronikClientNode(chronikUrl); + + alpMintTwoTxid = await get_alp_mint_two_txid; + alpSendTwoTxid = await get_alp_send_two_txid; + + // Can get the tx object from the tx endpoint + alpMintTwo = await chronik.tx(alpMintTwoTxid); + alpSendTwo = await chronik.tx(alpSendTwoTxid); + + // alpSendTwoTxid eb227bec6fd4d270262e14efa128a970f0ea4e529c599b1eb28af5fc8eaf3452 (broadcast 1st, alphabetically 2nd) + // alpMintTwoTxid 665c70b6c01b8e9b4865136ee650bae344fb8b45def49eb7ec303b04d2520b4b (broadcast 2nd, alphabetically 1st) + const alphabeticalUnconfirmedAlpGenesisTxs = [ + alpMintTwo, + alpSendTwo, + ].sort((a, b) => a.txid.localeCompare(b.txid)); + + // Can get these from unconfirmed txs + const unconfirmedTxs = await chronik + .tokenId(alpGenesisTxid) + .unconfirmedTxs(); + + // unconfirmedTxs returns them in alphabetical order + for (let i = 0; i < unconfirmedTxs.txs.length; i += 1) { + expect(unconfirmedTxs.txs[i]).to.deep.equal( + alphabeticalUnconfirmedAlpGenesisTxs[i], + ); + } + + // They are the only unconfirmed txs + expect(unconfirmedTxs.txs.length).to.eql(2); + + // Calling chronik.tokenId.history() returns all confirmed and unconfirmed txs + + const allTokenTxsForAlpGenesisTxid = await chronik + .tokenId(alpGenesisTxid) + .history(); + + // We get all expected txs, confirmed and unconfirmed + expect(allTokenTxsForAlpGenesisTxid.txs.length).to.eql( + confirmedTxsForAlpGenesisTxid.txs.length + + unconfirmedTxs.txs.length, + ); + + // Txs from history are sorted by blockheight, then timeFirstSeen, then txid + // In this case, timeFirstSeen is a mock time, so it is the same for each + // So we expect reverse alphabetical order by txid + const unconfirmedInHistory = allTokenTxsForAlpGenesisTxid.txs.splice( + 0, + 2, + ); + + const reverseAlphabeticalUnconfirmedAlpGenesisTxs = [ + alpMintTwo, + alpSendTwo, + ].sort((b, a) => a.txid.localeCompare(b.txid)); + expect(unconfirmedInHistory).to.deep.equal( + reverseAlphabeticalUnconfirmedAlpGenesisTxs, + ); + + // We get the rest of the txs in expected order + // Sorted by blockheight, then timeFirstSeen, then txid + // In this case, all txs have the same blockheight + // alpMegaTxid has timeFirstSeen of 0 since it was manually mined + // All other txs have the same timeFirstSeen so are sorted in reverse alphabetical order + + // Confirm the order of txs from .history() + const confirmedAlpTxs = [ + alpMegaTxid, // timeFirstSeen 0 | 72101f535470e0a6de7db9ba0ba115845566f738cc5124255b472347b5927565 + alpSendTxid, // timeFirstSeen 1300000000 | e623ab8971c93fa1a831a4310da65554c8dfd811c16cd5d41c6612268cb5dd5f + alpMultiTxid, // timeFirstSeen 1300000000 | e3c47f14d7ba3ab9a6f32a0fa8fcac41d06d3af595ebb5bab77ad03633a52eba + alpGenesisTxid, // timeFirstSeen 1300000000 | bb4d71aa6c0a92144f854402f2677975ad86d3a72cb7b0fb48d02473a88fc6e2 + alpMintTxid, // timeFirstSeen 1300000000 | 0dab1008db30343a4f771983e9fd96cbc15f0c6efc73f5249c9bae311ef1e92f + ]; + + for (let i = 0; i < allTokenTxsForAlpGenesisTxid.txs.length; i += 1) { + expect(confirmedAlpTxs[i]).to.eql( + allTokenTxsForAlpGenesisTxid.txs[i].txid, + ); + } + + // Make sure other parts of returned tx objects are as expected + // Since one of the utxos from confirmedTxsForAlpGenesisTxid was spent to create + // the second mint tx, this spentBy key has changed + + // We spent the mint baton + const newConfirmedMintTxIndex = + allTokenTxsForAlpGenesisTxid.txs.findIndex( + tx => tx.txid === alpMintTxid, + ); + + const newMintTx = allTokenTxsForAlpGenesisTxid.txs.splice( + newConfirmedMintTxIndex, + 1, + )[0]; + const confirmedMintTxIndex = + confirmedTxsForAlpGenesisTxid.txs.findIndex( + tx => tx.txid === alpMintTxid, + ); + const oldMintTx = confirmedTxsForAlpGenesisTxid.txs.splice( + confirmedMintTxIndex, + 1, + )[0]; + + // We have removed this tx from both histories + expect(allTokenTxsForAlpGenesisTxid.txs.length).to.eql( + confirmedTxsForAlpGenesisTxid.txs.length, + ); + + // They are the same except for outputs, as expected + expect(oldMintTx.inputs).to.deep.equal(newMintTx.inputs); + expect(oldMintTx.tokenEntries).to.deep.equal(newMintTx.tokenEntries); + + // Since one of the utxos from confirmedTxsForAlpGenesisTxid was spent to create + // the second send tx, this spentBy key has changed + const newConfirmedSendTxIndex = + allTokenTxsForAlpGenesisTxid.txs.findIndex( + tx => tx.txid === alpSendTxid, + ); + + const newSendTx = allTokenTxsForAlpGenesisTxid.txs.splice( + newConfirmedSendTxIndex, + 1, + )[0]; + const confirmedSendTxIndex = + confirmedTxsForAlpGenesisTxid.txs.findIndex( + tx => tx.txid === alpSendTxid, + ); + const oldSendTx = confirmedTxsForAlpGenesisTxid.txs.splice( + confirmedSendTxIndex, + 1, + )[0]; + + // We have removed this tx from both histories + expect(allTokenTxsForAlpGenesisTxid.txs.length).to.eql( + confirmedTxsForAlpGenesisTxid.txs.length, + ); + + // They are the same except for outputs, as expected + expect(oldSendTx.inputs).to.deep.equal(newSendTx.inputs); + expect(oldSendTx.tokenEntries).to.deep.equal(newSendTx.tokenEntries); + + // The other txs are the same, though the order is not the same + // allTokenTxsForAlpGenesisTxid is sorted by blockheight, then timeFirstSeen, then reverse alphabetical by txid + // confirmedTxsForAlpGenesisTxid is sorted by blockheight, then alphabetical by txid + + // These respective orderings are already tested above, here we test they have the same content + expect( + allTokenTxsForAlpGenesisTxid.txs.sort((a, b) => + a.txid.localeCompare(b.txid), + ), + ).to.deep.equal( + confirmedTxsForAlpGenesisTxid.txs.sort((a, b) => + a.txid.localeCompare(b.txid), + ), + ); + }); + it('We get tx history in expected order from both tokenId().history() and tokenId.confirmedTxs()', async () => { + const chronikUrl = await chronik_url; + const chronik = new ChronikClientNode(chronikUrl); + + // Can get all confirmed token txs for alpGenesisTxid + const confirmedTxs = await chronik + .tokenId(alpGenesisTxid) + .confirmedTxs(); + + // Confirmed txs are sorted by blockheight, then alphabetically by txid + // timeFirstSeen is not considered, demonstrated by alpMegaTxid not being first + const confirmedAlpTxids = [ + alpMintTxid, // blockheight 102, timeFirstSeen 1300000000 | 0dab1008db30343a4f771983e9fd96cbc15f0c6efc73f5249c9bae311ef1e92f + alpMegaTxid, // blockheight 102, timeFirstSeen 0 | 72101f535470e0a6de7db9ba0ba115845566f738cc5124255b472347b5927565 + alpGenesisTxid, // blockheight 102, timeFirstSeen 1300000000 | bb4d71aa6c0a92144f854402f2677975ad86d3a72cb7b0fb48d02473a88fc6e2 + alpMultiTxid, // blockheight 102, timeFirstSeen 1300000000 | e3c47f14d7ba3ab9a6f32a0fa8fcac41d06d3af595ebb5bab77ad03633a52eba + alpSendTxid, //blockheight 102, timeFirstSeen 1300000000 | e623ab8971c93fa1a831a4310da65554c8dfd811c16cd5d41c6612268cb5dd5f + alpMintTwoTxid, // blockheight 103, timeFirstSeen 1300000000 | 665c70b6c01b8e9b4865136ee650bae344fb8b45def49eb7ec303b04d2520b4b + alpSendTwoTxid, // blockheight 103, timeFirstSeen 1300000000 | eb227bec6fd4d270262e14efa128a970f0ea4e529c599b1eb28af5fc8eaf3452 + ]; + + // Same amount of txs in each + expect(confirmedTxs.txs.length).to.eql(confirmedAlpTxids.length); + + // Txs are in expected order + for (let i = 0; i < confirmedTxs.txs.length; i += 1) { + expect(confirmedTxs.txs[i].txid).to.eql(confirmedAlpTxids[i]); + } + + // Can get all confirmed token txs for alpGenesisTxid + const history = await chronik.tokenId(alpGenesisTxid).history(); + + // Txs from history are ordered by blockheight, then timeFirstSeen, then reverse alphabetical by txid + // In this case, alpSendTwoTxid and alpMintTwoTxid are from the highest blockheight. + // alpMegaTxid has timeFirstSeen of 0 because it had to be manually mined in block. So, it comes first in the next block + // The other txids in the alpMegaTxid block all have the same timeFirstSeen, so they are sorted reverse alphabetically + const historyAlpTxids = [ + alpSendTwoTxid, // timeFirstSeen 1300000000, blockheight 103 | eb227bec6fd4d270262e14efa128a970f0ea4e529c599b1eb28af5fc8eaf3452 + alpMintTwoTxid, // timeFirstSeen 1300000000, blockheight 103 | 665c70b6c01b8e9b4865136ee650bae344fb8b45def49eb7ec303b04d2520b4b + alpMegaTxid, // timeFirstSeen 0, blockheight 102 | 72101f535470e0a6de7db9ba0ba115845566f738cc5124255b472347b5927565 + alpSendTxid, // timeFirstSeen 1300000000, blockheight 102 | e623ab8971c93fa1a831a4310da65554c8dfd811c16cd5d41c6612268cb5dd5f + alpMultiTxid, // timeFirstSeen 1300000000, blockheight 102 | e3c47f14d7ba3ab9a6f32a0fa8fcac41d06d3af595ebb5bab77ad03633a52eba + alpGenesisTxid, // timeFirstSeen 1300000000, blockheight 102 | bb4d71aa6c0a92144f854402f2677975ad86d3a72cb7b0fb48d02473a88fc6e2 + alpMintTxid, // timeFirstSeen 1300000000, blockheight 102 | 0dab1008db30343a4f771983e9fd96cbc15f0c6efc73f5249c9bae311ef1e92f + ]; + + // Same amount of txs in each + expect(history.txs.length).to.eql(historyAlpTxids.length); + + // Txs in expected order + for (let i = 0; i < confirmedTxs.txs.length; i += 1) { + expect(history.txs[i].txid).to.eql(historyAlpTxids[i]); + } }); }); diff --git a/test/functional/setup_scripts/chronik-client_token_alp.py b/test/functional/setup_scripts/chronik-client_token_alp.py --- a/test/functional/setup_scripts/chronik-client_token_alp.py +++ b/test/functional/setup_scripts/chronik-client_token_alp.py @@ -308,8 +308,69 @@ assert_equal(node.getblockcount(), 102) yield True - self.log.info("Step 7: Send wild oversized ALP tx") - assert_equal(node.getblockcount(), 102) + self.log.info("Step 7: Send another ALP tx in the mempool") + # Another ALP Send tx + alp_send_two_tx = CTransaction() + alp_send_two_tx.vin = [ + CTxIn( + COutPoint(int(another_alp_genesis_tx_txid, 16), 4), + SCRIPTSIG_OP_TRUE, + ), + CTxIn( + COutPoint(int(alp_send_tx_txid, 16), 2), + SCRIPTSIG_OP_TRUE, + ), + ] + alp_send_two_tx.vout = [ + alp_opreturn( + alp_send( + token_id=alp_genesis_tx_txid, + output_amounts=[10, 1], + ), + ), + CTxOut(coinvalue - 300000, P2SH_OP_TRUE), + CTxOut(546, P2SH_OP_TRUE), + ] + alp_send_two_tx_txid = node.sendrawtransaction( + alp_send_two_tx.serialize().hex() + ) + self.log.info(f"alp_send_two_tx_txid: {alp_send_two_tx_txid}") + send_ipc_message({"alp_send_two_txid": alp_send_two_tx_txid}) + + # Another ALP Mint tx + alp_mint_two_tx = CTransaction() + alp_mint_two_tx.vin = [ + CTxIn( + COutPoint(int(alp_send_two_tx_txid, 16), 1), + SCRIPTSIG_OP_TRUE, + ), + CTxIn( + COutPoint(int(alp_mint_tx_txid, 16), 3), + SCRIPTSIG_OP_TRUE, + ), + ] + alp_mint_two_tx.vout = [ + alp_opreturn( + alp_mint( + token_id=alp_genesis_tx_txid, + mint_amounts=[5, 0], + num_batons=1, + ), + ), + CTxOut(546, P2SH_OP_TRUE), + CTxOut(546, P2SH_OP_TRUE), + CTxOut(546, P2SH_OP_TRUE), + CTxOut(coinvalue - 400000, P2SH_OP_TRUE), + ] + alp_mint_two_tx_txid = node.sendrawtransaction( + alp_mint_two_tx.serialize().hex() + ) + send_ipc_message({"alp_mint_two_txid": alp_mint_two_tx_txid}) + yield True + + self.log.info("Step 8: Confirm your second ALP tx") + self.generate(node, 1) + assert_equal(node.getblockcount(), 103) yield True