diff --git a/cashtab/src/chronik/__tests__/index.test.js b/cashtab/src/chronik/__tests__/index.test.js --- a/cashtab/src/chronik/__tests__/index.test.js +++ b/cashtab/src/chronik/__tests__/index.test.js @@ -11,6 +11,7 @@ sortAndTrimChronikTxHistory, parseChronikTx, getMintAddress, + getTokenGenesisInfo, } from 'chronik'; import vectors from '../fixtures/vectors'; import { @@ -41,6 +42,7 @@ } from '../fixtures/chronikMintTxs'; import { ChronikClient } from 'chronik-client'; import { when } from 'jest-when'; +import { MockChronikClient } from '../../../../modules/mock-chronik-client'; describe('Cashtab chronik.js functions', () => { it(`getTokenStats successfully returns a token stats object`, async () => { @@ -451,4 +453,56 @@ }); }); }); + describe.only('We get info we want to cache about a token from its genesis tx and chronik token info endpoint', () => { + const { expectedReturns, expectedErrors } = vectors.getTokenGenesisInfo; + + // TODO populate mocks for expectedReturn vectors + expectedReturns.forEach(expectedReturn => { + const { description, tokenId, tokenInfo, genesisTx, returned } = + expectedReturn; + const mockedChronik = new MockChronikClient(); + + // Set mock for chronik.token(tokenId) + mockedChronik.setMock('token', { + input: tokenId, + output: tokenInfo, + }); + + // Set mock for chronik.tx(tokenId) + mockedChronik.setMock('tx', { + input: tokenId, + output: genesisTx, + }); + + it(`getTokenGenesisInfo: ${description}`, async () => { + expect( + await getTokenGenesisInfo(mockedChronik, tokenId), + ).toStrictEqual(returned); + }); + }); + + expectedErrors.forEach(expectedReturn => { + const { description, tokenId, tokenInfo, genesisTx, msg } = + expectedReturn; + const mockedChronik = new MockChronikClient(); + + // Set mock for chronik.token(tokenId) + mockedChronik.setMock('token', { + input: tokenId, + output: tokenInfo, + }); + + // Set mock for chronik.tx(tokenId) + mockedChronik.setMock('tx', { + input: tokenId, + output: genesisTx, + }); + + it(`getTokenGenesisInfo: ${description}`, async () => { + await expect( + getTokenGenesisInfo(mockedChronik, tokenId), + ).rejects.toEqual(msg); + }); + }); + }); }); diff --git a/cashtab/src/chronik/fixtures/vectors.js b/cashtab/src/chronik/fixtures/vectors.js --- a/cashtab/src/chronik/fixtures/vectors.js +++ b/cashtab/src/chronik/fixtures/vectors.js @@ -279,4 +279,66 @@ }, ], }, + getTokenGenesisInfo: { + expectedReturns: [ + { + description: 'slpv1 token with no minting batons', + tokenId: '', + tokenInfo: {}, + genesisTx: {}, + returned: {}, + }, + { + description: 'slpv1 token with minting baton', + tokenId: '', + tokenInfo: {}, + genesisTx: {}, + returned: {}, + }, + { + description: 'ALP token with no minting batons', + tokenId: '', + tokenInfo: {}, + genesisTx: {}, + returned: {}, + }, + { + description: 'ALP token with minting baton', + tokenId: '', + tokenInfo: {}, + genesisTx: {}, + returned: {}, + }, + { + description: + 'ALP token with minting batons and different mint quantities at different addresses', + tokenId: '', + tokenInfo: {}, + genesisTx: {}, + returned: {}, + }, + ], + expectedErrors: [ + { + description: + 'Error is thrown if 1st chronik API call not completed successfully', + tokenId: + '1111111111111111111111111111111111111111111111111111111111111111', + tokenInfo: new Error( + 'Bad response from chronik.token(tokenId)', + ), + genesisTx: {}, // non-error response + msg: new Error('Bad response from chronik.token(tokenId)'), + }, + { + description: + 'Error is thrown if 2nd chronik API call not completed successfully', + tokenId: + '1111111111111111111111111111111111111111111111111111111111111111', + tokenInfo: {}, // non-error response + genesisTx: new Error('Bad response from chronik.tx(tokenId)'), + msg: new Error('Bad response from chronik.tx(tokenId)'), + }, + ], + }, }; diff --git a/cashtab/src/chronik/index.js b/cashtab/src/chronik/index.js --- a/cashtab/src/chronik/index.js +++ b/cashtab/src/chronik/index.js @@ -923,3 +923,78 @@ return err; } }; + +/** + * Get all info about a token used in Cashtab's token cache + * @param {ChronikClientNode} chronik + * @param {string} tokenId + * @returns {object} + */ +export const getTokenGenesisInfo = async (chronik, tokenId) => { + // We can get timeFirstSeen, block, tokenType, and genesisInfo from the token() endpoint + + const tokenInfo = await chronik.token(tokenId); + const genesisTxInfo = await chronik.tx(tokenId); + + const { block, timeFirstSeen, genesisInfo, tokenType } = tokenInfo; + const decimals = genesisInfo.decimals; + + // Initialize variables for determined quantities we want to cache + + /** + * genesisSupply {string} + * Quantity of token created at mint + * Note: we may have genesisSupply at different genesisAddresses + * We do not track this information, only total genesisSupply + * Cached as a decimalized string + */ + let genesisSupply = new BN(0); + + /** + * genesisMintBatons {number} + * Number of mint batons created in the genesis tx for this token + */ + let genesisMintBatons = 0; + + /** + * genesisOutputScripts {Set()} + * Address(es) where initial token supply was minted + */ + // TODO will need ne getter/setter methods to store a set in a map in cache + let genesisOutputScripts = new Set(); + + // Iterate over outputs + for (const output of genesisTxInfo.outputs) { + if ('token' in output && output.token.tokenId === tokenId) { + // If this output of this genesis tx is associated with this tokenId + + const { token, outputScript } = output; + + // Add its outputScript to genesisOutputScripts + genesisOutputScripts.add(outputScript); + + const { isMintBaton, amount } = token; + if (isMintBaton) { + // If it is a mintBaton, increment genesisMintBatons + genesisMintBatons += 1; + } + + // Increment genesisSupply + genesisSupply = genesisSupply.plus(new BN(amount)); + } + } + + genesisSupply = genesisSupply.shiftedBy(-1 * decimals).toString(); + + const tokenCache = { + tokenType, + genesisInfo, + timeFirstSeen, + block, + genesisSupply, + genesisOutputScripts, + genesisMintBatons, + }; + + return tokenCache; +};