diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..5d005e8f --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,19 @@ +module.exports = { + "extends": [ + "react-app", + "prettier/@typescript-eslint", + "plugin:prettier/recommended" + ], + "settings": { + "react": { + "version": "999.999.999" + }, + }, + "rules": { + "prettier/prettier": [ + "error", { + "endOfLine": "auto" + }, + ], + } +} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 535e4b7c..7eae8909 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,5 +1,7 @@ name: CI on: [push] +env: + NODE_OPTIONS: --max_old_space_size=4096 jobs: build: name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }} diff --git a/src/api/fullnode.ts b/src/api/fullnode.ts index 0dbd2172..fdfaebfb 100644 --- a/src/api/fullnode.ts +++ b/src/api/fullnode.ts @@ -18,7 +18,9 @@ import axios from 'axios'; import { globalCache } from '../utils'; import logger from '../logger'; -const DEFAULT_SERVER = process.env.DEFAULT_SERVER || 'https://node1.foxtrot.testnet.hathor.network/v1a/'; +const DEFAULT_SERVER = + process.env.DEFAULT_SERVER || + 'https://node1.foxtrot.testnet.hathor.network/v1a/'; /** * Returns a transaction from the fullnode @@ -61,8 +63,12 @@ export const downloadMempool = async () => { * * @param height - The block's height */ -export const downloadBlockByHeight = async (height: number): Promise => { - const response = await axios.get(`${DEFAULT_SERVER}block_at_height?height=${height}`); +export const downloadBlockByHeight = async ( + height: number +): Promise => { + const response = await axios.get( + `${DEFAULT_SERVER}block_at_height?height=${height}` + ); const data = response.data; @@ -82,9 +88,13 @@ export const downloadBlockByHeight = async (height: number): Promise const typedDecodedScript: DecodedScript = { type: input.decoded.type as string, address: input.decoded.address as string, - timelock: input.decoded.timelock ? input.decoded.timelock as number : null, - value: input.decoded.value ? input.decoded.value as number : null, - tokenData: input.decoded.token_data ? input.decoded.token_data as number : null, + timelock: input.decoded.timelock + ? (input.decoded.timelock as number) + : null, + value: input.decoded.value ? (input.decoded.value as number) : null, + tokenData: input.decoded.token_data + ? (input.decoded.token_data as number) + : null, }; const typedInput: Input = { txId: input.tx_id as string, @@ -98,25 +108,31 @@ export const downloadBlockByHeight = async (height: number): Promise return typedInput; }), - outputs: responseBlock.outputs.map((output: RawOutput): Output => { - const typedDecodedScript: DecodedScript = { - type: output.decoded.type as string, - address: output.decoded.address as string, - timelock: output.decoded.timelock ? output.decoded.timelock as number : null, - value: output.decoded.value ? output.decoded.value as number : null, - tokenData: output.decoded.token_data ? output.decoded.token_data as number : null, - }; - - const typedOutput: Output = { - value: output.value as number, - tokenData: output.token_data as number, - script: output.script as string, - decoded: typedDecodedScript, - token: output.token as string, - }; - - return typedOutput; - }), + outputs: responseBlock.outputs.map( + (output: RawOutput): Output => { + const typedDecodedScript: DecodedScript = { + type: output.decoded.type as string, + address: output.decoded.address as string, + timelock: output.decoded.timelock + ? (output.decoded.timelock as number) + : null, + value: output.decoded.value ? (output.decoded.value as number) : null, + tokenData: output.decoded.token_data + ? (output.decoded.token_data as number) + : null, + }; + + const typedOutput: Output = { + value: output.value as number, + tokenData: output.token_data as number, + script: output.script as string, + decoded: typedDecodedScript, + token: output.token as string, + }; + + return typedOutput; + } + ), parents: responseBlock.parents, height: responseBlock.height as number, }; @@ -130,7 +146,10 @@ export const downloadBlockByHeight = async (height: number): Promise * @param txId - The block txId * @param noCache - Prevents downloading the block from cache as a reorg may have ocurred */ -export const getBlockByTxId = async (txId: string, noCache: boolean = false) => { +export const getBlockByTxId = async ( + txId: string, + noCache: boolean = false +) => { return downloadTx(txId, noCache); }; @@ -140,7 +159,9 @@ export const getBlockByTxId = async (txId: string, noCache: boolean = false) => * a specialized API from the full_node to query its best block. */ export const getFullNodeBestBlock = async (): Promise => { - const response = await axios.get(`${DEFAULT_SERVER}transaction?type=block&count=1`); + const response = await axios.get( + `${DEFAULT_SERVER}transaction?type=block&count=1` + ); const { transactions } = response.data; const bestBlock: Block = { diff --git a/src/api/lambda.ts b/src/api/lambda.ts index a3fc977a..13484ce9 100644 --- a/src/api/lambda.ts +++ b/src/api/lambda.ts @@ -7,11 +7,7 @@ import AWS from 'aws-sdk'; import logger from '../logger'; -import { - PreparedTx, - ApiResponse, - Block, -} from '../types'; +import { PreparedTx, ApiResponse, Block } from '../types'; AWS.config.update({ region: process.env.AWS_REGION, @@ -23,57 +19,67 @@ AWS.config.update({ * @param fnName - The lambda function name * @param payload - The payload to be sent */ -export const lambdaCall = (fnName: string, payload: any): Promise => new Promise((resolve, reject) => { - const lambda = new AWS.Lambda({ - apiVersion: '2015-03-31', - endpoint: process.env.WALLET_SERVICE_STAGE === 'local' - ? process.env.WALLET_SERVICE_LOCAL_URL || 'http://localhost:3002' - : `https://lambda.${process.env.AWS_REGION}.amazonaws.com`, - }); +export const lambdaCall = (fnName: string, payload: any): Promise => + new Promise((resolve, reject) => { + const lambda = new AWS.Lambda({ + apiVersion: '2015-03-31', + endpoint: + process.env.WALLET_SERVICE_STAGE === 'local' + ? process.env.WALLET_SERVICE_LOCAL_URL || 'http://localhost:3002' + : `https://lambda.${process.env.AWS_REGION}.amazonaws.com`, + }); - const params = { - FunctionName: `${process.env.WALLET_SERVICE_NAME}-${process.env.WALLET_SERVICE_STAGE}-${fnName}`, - Payload: JSON.stringify({ - body: payload, - }), - }; + const params = { + FunctionName: `${process.env.WALLET_SERVICE_NAME}-${process.env.WALLET_SERVICE_STAGE}-${fnName}`, + Payload: JSON.stringify({ + body: payload, + }), + }; - lambda.invoke(params, (err, data) => { - if (err) { - logger.error(`Erroed on ${fnName} method call with payload: ${payload}`); - logger.error(err); - reject(err); - } else { - if (data.StatusCode !== 200) { - reject(new Error('Request failed.')); - } + lambda.invoke(params, (err, data) => { + if (err) { + logger.error( + `Erroed on ${fnName} method call with payload: ${payload}` + ); + logger.error(err); + reject(err); + } else { + if (data.StatusCode !== 200) { + reject(new Error('Request failed.')); + } - try { - const responsePayload = JSON.parse(data.Payload as string); - const body = JSON.parse(responsePayload.body); + try { + const responsePayload = JSON.parse(data.Payload as string); + const body = JSON.parse(responsePayload.body); - resolve(body); - } catch(e) { - logger.error(`Erroed on lambda call to ${fnName} with payload: ${JSON.stringify(payload)}`); - logger.error(e); + resolve(body); + } catch (e) { + logger.error( + `Erroed on lambda call to ${fnName} with payload: ${JSON.stringify( + payload + )}` + ); + logger.error(e); - return reject(e.message); - } + return reject(e.message); } - }); -}); + } + }); + }); /** * Calls the onHandleReorgRequest lambda function */ -export const invokeReorg = async (): Promise => lambdaCall('onHandleReorgRequest', {}); +export const invokeReorg = async (): Promise => + lambdaCall('onHandleReorgRequest', {}); /** * Calls the onNewTxRequest lambda function with a PreparedTx * * @param tx - The prepared transaction to be sent */ -export const sendTx = async (tx: PreparedTx): Promise => lambdaCall('onNewTxRequest', tx); +export const sendTx = async (tx: PreparedTx): Promise => + lambdaCall('onNewTxRequest', tx); /** * Calls the getLatestBlock lambda function from the wallet-service returning diff --git a/src/index.ts b/src/index.ts index 077ade67..40ab3288 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,7 +20,7 @@ const machine = interpret(SyncMachine).onTransition(state => { }); const handleMessage = (message: any) => { - switch(message.type) { + switch (message.type) { case 'dashboard:metrics': break; @@ -49,7 +49,9 @@ const handleMessage = (message: any) => { machine.send({ type: 'NEW_BLOCK' }); } if (message.state === Connection.CONNECTING) { - logger.info(`Websocket is attempting to connect to ${process.env.DEFAULT_SERVER}`); + logger.info( + `Websocket is attempting to connect to ${process.env.DEFAULT_SERVER}` + ); } if (message.state === Connection.CLOSED) { logger.error('Websocket connection was closed.'); @@ -65,14 +67,16 @@ const conn = new Connection({ }); // @ts-ignore -conn.websocket.on('network', (message) => handleMessage(message)); +conn.websocket.on('network', message => handleMessage(message)); // @ts-ignore -conn.on('state', (state) => handleMessage({ - type: 'state_update', - state, -})); +conn.on('state', state => + handleMessage({ + type: 'state_update', + state, + }) +); // @ts-ignore -conn.websocket.on('connection_error', (evt) => { +conn.websocket.on('connection_error', evt => { logger.error(`Websocket connection error: ${evt.message}`); }); diff --git a/src/logger.ts b/src/logger.ts index b23045e5..cbd265b7 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -10,34 +10,36 @@ import * as winston from 'winston'; const CONSOLE_LEVEL = process.env.CONSOLE_LEVEL || 'info'; -const myFormat = winston.format.printf(({ level, message, service, timestamp, ...args }) => { - let argsStr = ''; - - if (Object.keys(args).length > 0) { - // Adapted from https://github.com/winstonjs/logform/blob/master/pretty-print.js - const stripped = Object.assign({}, args); - - const levelSymbol = Symbol.for('level'); - const messageSymbol = Symbol.for('message'); - const splatSymbol = Symbol.for('splat'); - - // Typing Symbol as any is a workaround for https://github.com/microsoft/TypeScript/issues/1863 - delete stripped[levelSymbol as any]; - delete stripped[messageSymbol as any]; - delete stripped[splatSymbol as any]; - - argsStr = util.inspect(stripped, {compact: true, breakLength: Infinity}); +const myFormat = winston.format.printf( + ({ level, message, service, timestamp, ...args }) => { + let argsStr = ''; + + if (Object.keys(args).length > 0) { + // Adapted from https://github.com/winstonjs/logform/blob/master/pretty-print.js + const stripped = Object.assign({}, args); + + const levelSymbol = Symbol.for('level'); + const messageSymbol = Symbol.for('message'); + const splatSymbol = Symbol.for('splat'); + + // Typing Symbol as any is a workaround for https://github.com/microsoft/TypeScript/issues/1863 + delete stripped[levelSymbol as any]; + delete stripped[messageSymbol as any]; + delete stripped[splatSymbol as any]; + + argsStr = util.inspect(stripped, { + compact: true, + breakLength: Infinity, + }); + } + + return `${timestamp} [${service}] ${level}: ${message} ${argsStr}`; } - - return `${timestamp} [${service}] ${level}: ${message} ${argsStr}`; -}); +); const transports = [ new winston.transports.Console({ - format: winston.format.combine( - winston.format.colorize(), - myFormat, - ), + format: winston.format.combine(winston.format.colorize(), myFormat), level: CONSOLE_LEVEL, }), ]; @@ -45,7 +47,7 @@ const transports = [ const logger = winston.createLogger({ format: winston.format.combine( winston.format.timestamp(), - winston.format.errors({ stack: true }), + winston.format.errors({ stack: true }) ), defaultMeta: { service: 'wallet-service-daemon' }, transports: transports, diff --git a/src/machine.ts b/src/machine.ts index 40db28fb..3dd9f427 100644 --- a/src/machine.ts +++ b/src/machine.ts @@ -5,11 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import { - Machine, - assign, - send, -} from 'xstate'; +import { Machine, assign, send } from 'xstate'; import { syncToLatestBlock, syncLatestMempool } from './utils'; import { GeneratorYieldResult, @@ -33,7 +29,7 @@ export const syncHandler = () => (callback, onReceive) => { if (done) { // The generator reached its end, we should end this handler - logger.debug('Done.', value) + logger.debug('Done.', value); break; } @@ -53,9 +49,13 @@ export const syncHandler = () => (callback, onReceive) => { logger.info('Sync generator finished.'); callback('DONE'); } else if (value.type === 'block_success') { - logger.info(`Block id: ${value.blockId} sent successfully, transactions sent: ${value.transactions.length}`); + logger.info( + `Block id: ${value.blockId} sent successfully, transactions sent: ${value.transactions.length}` + ); } else { - logger.warn(`Unhandled type received from sync generator: ${value.type}`); + logger.warn( + `Unhandled type received from sync generator: ${value.type}` + ); } } @@ -93,7 +93,7 @@ export const mempoolHandler = () => (callback, onReceive) => { if (done) { // The generator reached its end, we should end this handler - logger.debug('Done.', value) + logger.debug('Done.', value); break; } @@ -110,7 +110,9 @@ export const mempoolHandler = () => (callback, onReceive) => { } else if (value.type === 'tx_success') { logger.info('Mempool tx synced!'); } else { - logger.warn(`Unhandled type received from sync generator: ${value.type}`); + logger.warn( + `Unhandled type received from sync generator: ${value.type}` + ); } } @@ -132,126 +134,129 @@ export const mempoolHandler = () => (callback, onReceive) => { /* See README for an explanation on how the machine works. * TODO: We need to type the Event */ -export const SyncMachine = Machine({ - id: 'sync', - initial: 'idle', - context: { - hasMoreBlocks: false, - hasMempoolUpdate: false, - }, - states: { - idle: { - always: [ - // Conditions are tested in order, the first valid one is taken, if any are valid - // https://xstate.js.org/docs/guides/guards.html#multiple-guards - { target: 'syncing', cond: 'hasMoreBlocks' }, - { target: 'mempoolsync', cond: 'hasMempoolUpdate' }, - ], - on: { - NEW_BLOCK: 'syncing', - MEMPOOL_UPDATE: 'mempoolsync', - }, +export const SyncMachine = Machine( + { + id: 'sync', + initial: 'idle', + context: { + hasMoreBlocks: false, + hasMempoolUpdate: false, }, - mempoolsync: { - invoke: { - id: 'syncLatestMempool', - src: 'mempoolHandler', + states: { + idle: { + always: [ + // Conditions are tested in order, the first valid one is taken, if any are valid + // https://xstate.js.org/docs/guides/guards.html#multiple-guards + { target: 'syncing', cond: 'hasMoreBlocks' }, + { target: 'mempoolsync', cond: 'hasMempoolUpdate' }, + ], + on: { + NEW_BLOCK: 'syncing', + MEMPOOL_UPDATE: 'mempoolsync', + }, }, - on: { - MEMPOOL_UPDATE: { - actions: ['setMempoolUpdate'], + mempoolsync: { + invoke: { + id: 'syncLatestMempool', + src: 'mempoolHandler', }, - // Stop mempool sync when a block arrives - // this means that the mempool may not be fully synced when it leaves this state - // giving priority to blocks means the mempool may change between syncs - NEW_BLOCK: { - target: 'syncing', - // When block sync finishes, go back to mempool sync - actions: ['setMempoolUpdate'], + on: { + MEMPOOL_UPDATE: { + actions: ['setMempoolUpdate'], + }, + // Stop mempool sync when a block arrives + // this means that the mempool may not be fully synced when it leaves this state + // giving priority to blocks means the mempool may change between syncs + NEW_BLOCK: { + target: 'syncing', + // When block sync finishes, go back to mempool sync + actions: ['setMempoolUpdate'], + }, + STOP: 'idle', + DONE: 'idle', + // Errors on mempool sync are "ignored" since next sync (either block or mempool) should fix it + ERROR: 'idle', }, - STOP: 'idle', - DONE: 'idle', - // Errors on mempool sync are "ignored" since next sync (either block or mempool) should fix it - ERROR: 'idle', - }, - entry: [ - 'resetMempoolUpdate', - send('START', { - to: 'syncLatestMempool', - }), - ], - }, - syncing: { - invoke: { - id: 'syncToLatestBlock', - src: 'syncHandler', + entry: [ + 'resetMempoolUpdate', + send('START', { + to: 'syncLatestMempool', + }), + ], }, - on: { - NEW_BLOCK: { - actions: ['setMoreBlocks'], + syncing: { + invoke: { + id: 'syncToLatestBlock', + src: 'syncHandler', + }, + on: { + NEW_BLOCK: { + actions: ['setMoreBlocks'], + }, + STOP: 'idle', + DONE: 'idle', + ERROR: 'failure', + REORG: 'reorg', }, - STOP: 'idle', - DONE: 'idle', - ERROR: 'failure', - REORG: 'reorg', + entry: [ + 'resetMoreBlocks', + send('START', { + to: 'syncToLatestBlock', + }), + ], }, - entry: [ - 'resetMoreBlocks', - send('START', { - to: 'syncToLatestBlock', - }), - ], - }, - reorg: { - invoke: { - id: 'invokeReorg', - src: (_context, _event) => async () => { - const response = await invokeReorg(); + reorg: { + invoke: { + id: 'invokeReorg', + src: (_context, _event) => async () => { + const response = await invokeReorg(); - if (!response.success) { - logger.error(response); - throw new Error('Reorg failed'); - } + if (!response.success) { + logger.error(response); + throw new Error('Reorg failed'); + } - return; - }, - onDone: { - target: 'idle', + return; + }, + onDone: { + target: 'idle', + }, + onError: { + target: 'failure', + }, }, - onError: { - target: 'failure', - }, - } + }, + failure: { + type: 'final', + }, + }, + }, + { + guards: { + hasMoreBlocks: ctx => ctx.hasMoreBlocks, + hasMempoolUpdate: ctx => ctx.hasMempoolUpdate, + }, + actions: { + // @ts-ignore + resetMoreBlocks: assign({ + hasMoreBlocks: () => false, + }), + // @ts-ignore + setMoreBlocks: assign({ + hasMoreBlocks: () => true, + }), + // @ts-ignore + resetMempoolUpdate: assign({ + hasMempoolUpdate: () => false, + }), + // @ts-ignore + setMempoolUpdate: assign({ + hasMempoolUpdate: () => true, + }), }, - failure: { - type: 'final', + services: { + syncHandler, + mempoolHandler, }, } -}, { - guards: { - hasMoreBlocks: (ctx) => ctx.hasMoreBlocks, - hasMempoolUpdate: (ctx) => ctx.hasMempoolUpdate, - }, - actions: { - // @ts-ignore - resetMoreBlocks: assign({ - hasMoreBlocks: () => false, - }), - // @ts-ignore - setMoreBlocks: assign({ - hasMoreBlocks: () => true, - }), - // @ts-ignore - resetMempoolUpdate: assign({ - hasMempoolUpdate: () => false, - }), - // @ts-ignore - setMempoolUpdate: assign({ - hasMempoolUpdate: () => true, - }), - }, - services: { - syncHandler, - mempoolHandler, - }, -}); +); diff --git a/src/types.ts b/src/types.ts index 57d670ae..df994183 100644 --- a/src/types.ts +++ b/src/types.ts @@ -91,7 +91,7 @@ export interface SyncSchema { syncing: {}; failure: {}; reorg: {}; - } + }; } export interface SyncContext { @@ -111,51 +111,60 @@ export interface HandlerEvent { type: string; } -export type StatusEvent = { - type: 'finished'; - success: boolean; - message?: string; -} | { - type: 'block_success'; - success: boolean; - height?: number; - blockId: string; - message?: string; - transactions: string[]; -} | { - type: 'transaction_failure'; - success: boolean; - message?: string; -} | { - type: 'reorg'; - success: boolean; - message?: string; -} | { - type: 'error'; - success: boolean; - message?: string; - error?: string; -} +export type StatusEvent = + | { + type: 'finished'; + success: boolean; + message?: string; + } + | { + type: 'block_success'; + success: boolean; + height?: number; + blockId: string; + message?: string; + transactions: string[]; + } + | { + type: 'transaction_failure'; + success: boolean; + message?: string; + } + | { + type: 'reorg'; + success: boolean; + message?: string; + } + | { + type: 'error'; + success: boolean; + message?: string; + error?: string; + }; -export type MempoolEvent = { - type: 'finished'; - success: boolean; - message?: string; -} | { - type: 'tx_success'; - success: boolean; - txId: string; - message?: string; -} | { - type: 'wait'; - success: boolean; - message?: string; -} | { - type: 'error'; - success: boolean; - message?: string; - error?: string; -} +export type MempoolEvent = + | { + type: 'finished'; + success: boolean; + message?: string; + } + | { + type: 'tx_success'; + success: boolean; + txId: string; + message?: string; + } + | { + type: 'wait'; + success: boolean; + message?: string; + } + | { + type: 'error'; + success: boolean; + message?: string; + error?: string; + }; /* export interface StatusEvent { type: string; diff --git a/src/utils.ts b/src/utils.ts index a52af0de..7888f6dd 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -31,10 +31,7 @@ import { getFullNodeBestBlock, downloadBlockByHeight, } from './api/fullnode'; -import { - getWalletServiceBestBlock, - sendTx, -} from './api/lambda'; +import { getWalletServiceBestBlock, sendTx } from './api/lambda'; import dotenv from 'dotenv'; // @ts-ignore import { wallet } from '@hathor/wallet-lib'; @@ -44,28 +41,35 @@ import { isNumber } from 'lodash'; dotenv.config(); export const IGNORE_TXS: Map = new Map([ - ['mainnet', [ - '000006cb93385b8b87a545a1cbb6197e6caff600c12cc12fc54250d39c8088fc', - '0002d4d2a15def7604688e1878ab681142a7b155cbe52a6b4e031250ae96db0a', - '0002ad8d1519daaddc8e1a37b14aac0b045129c01832281fb1c02d873c7abbf9', - ]], - ['testnet', [ - '0000033139d08176d1051fb3a272c3610457f0c7f686afbe0afe3d37f966db85', - '00e161a6b0bee1781ea9300680913fb76fd0fac4acab527cd9626cc1514abdc9', - '00975897028ceb037307327c953f5e7ad4d3f42402d71bd3d11ecb63ac39f01a', - ]], + [ + 'mainnet', + [ + '000006cb93385b8b87a545a1cbb6197e6caff600c12cc12fc54250d39c8088fc', + '0002d4d2a15def7604688e1878ab681142a7b155cbe52a6b4e031250ae96db0a', + '0002ad8d1519daaddc8e1a37b14aac0b045129c01832281fb1c02d873c7abbf9', + ], + ], + [ + 'testnet', + [ + '0000033139d08176d1051fb3a272c3610457f0c7f686afbe0afe3d37f966db85', + '00e161a6b0bee1781ea9300680913fb76fd0fac4acab527cd9626cc1514abdc9', + '00975897028ceb037307327c953f5e7ad4d3f42402d71bd3d11ecb63ac39f01a', + ], + ], ]); - -const TX_CACHE_SIZE: number = parseInt(process.env.TX_CACHE_SIZE as string) || 200; +const TX_CACHE_SIZE: number = + parseInt(process.env.TX_CACHE_SIZE as string) || 200; /** * Download and parse a tx by it's id * * @param txId - the id of the tx to be downloaded */ -export const downloadTxFromId = async (txId: string): Promise => { - +export const downloadTxFromId = async ( + txId: string +): Promise => { const network: string = process.env.NETWORK || 'mainnet'; // Do not download genesis transactions @@ -79,7 +83,7 @@ export const downloadTxFromId = async (txId: string): Promise => } const txData: RawTxResponse = await downloadTx(txId); - const { tx, meta } = txData; + const { tx } = txData; return parseTx(tx); }; @@ -91,7 +95,11 @@ export const downloadTxFromId = async (txId: string): Promise => * @param txIds - List of transactions to download * @param data - Downloaded transactions, used while being called recursively */ -export const recursivelyDownloadTx = async (blockId: string, txIds: string[] = [], data = new Map()): Promise> => { +export const recursivelyDownloadTx = async ( + blockId: string, + txIds: string[] = [], + data = new Map() +): Promise> => { if (txIds.length === 0) { return data; } @@ -128,14 +136,16 @@ export const recursivelyDownloadTx = async (blockId: string, txIds: string[] = [ } // check if we have already downloaded the parents - const newParents = parsedTx.parents.filter((parent) => { - return txIds.indexOf(parent) < 0 && + const newParents = parsedTx.parents.filter(parent => { + return ( + txIds.indexOf(parent) < 0 && /* Removing the current tx from the list of transactions to download: */ parent !== txId && /* Data works as our return list on the recursion and also as a "seen" list on the BFS. * We don't want to download a transaction that is already on our seen list. */ !data.has(parent) + ); }); const newData = data.set(parsedTx.txId, parsedTx); @@ -159,7 +169,7 @@ export const prepareTx = (tx: FullTx | FullBlock): PreparedTx => { token_name: tx.tokenName, token_symbol: tx.tokenSymbol, height: tx.height, - inputs: tx.inputs.map((input) => { + inputs: tx.inputs.map(input => { const baseInput: PreparedInput = { tx_id: input.txId, value: input.value, @@ -178,17 +188,21 @@ export const prepareTx = (tx: FullTx | FullBlock): PreparedTx => { } if (!tx.tokens || tx.tokens.length <= 0) { - throw new Error('Input is a token but there are no tokens in the tokens list.'); + throw new Error( + 'Input is a token but there are no tokens in the tokens list.' + ); } - const { uid } = tx.tokens[wallet.getTokenIndex(input.decoded.tokenData) - 1]; + const { uid } = tx.tokens[ + wallet.getTokenIndex(input.decoded.tokenData) - 1 + ]; return { ...baseInput, token: uid, }; }), - outputs: tx.outputs.map((output) => { + outputs: tx.outputs.map(output => { const baseOutput: PreparedOutput = { value: output.value, token_data: output.tokenData, @@ -205,10 +219,14 @@ export const prepareTx = (tx: FullTx | FullBlock): PreparedTx => { } if (!tx.tokens || tx.tokens.length <= 0) { - throw new Error('Output is a token but there are no tokens in the tokens list.'); + throw new Error( + 'Output is a token but there are no tokens in the tokens list.' + ); } - const { uid } = tx.tokens[wallet.getTokenIndex(output.decoded.tokenData) - 1]; + const { uid } = tx.tokens[ + wallet.getTokenIndex(output.decoded.tokenData) - 1 + ]; return { ...baseOutput, @@ -234,15 +252,21 @@ export const parseTx = (tx: RawTx): FullTx => { version: tx.version as number, weight: tx.weight as number, timestamp: tx.timestamp as number, - tokenName: tx.token_name ? tx.token_name as string : null, - tokenSymbol: tx.token_symbol ? tx.token_symbol as string : null, + tokenName: tx.token_name ? (tx.token_name as string) : null, + tokenSymbol: tx.token_symbol ? (tx.token_symbol as string) : null, inputs: tx.inputs.map((input: RawInput) => { const typedDecodedScript: DecodedScript = { type: input.decoded.type as string, address: input.decoded.address as string, - timelock: isNumber(input.decoded.timelock) ? input.decoded.timelock as number : null, - value: isNumber(input.decoded.value) ? input.decoded.value as number : null, - tokenData: isNumber(input.decoded.token_data) ? input.decoded.token_data as number : null, + timelock: isNumber(input.decoded.timelock) + ? (input.decoded.timelock as number) + : null, + value: isNumber(input.decoded.value) + ? (input.decoded.value as number) + : null, + tokenData: isNumber(input.decoded.token_data) + ? (input.decoded.token_data as number) + : null, }; const typedInput: Input = { txId: input.tx_id as string, @@ -255,24 +279,32 @@ export const parseTx = (tx: RawTx): FullTx => { return typedInput; }), - outputs: tx.outputs.map((output: RawOutput): Output => { - const typedDecodedScript: DecodedScript = { - type: output.decoded.type as string, - address: output.decoded.address as string, - timelock: isNumber(output.decoded.timelock) ? output.decoded.timelock as number : null, - value: isNumber(output.decoded.value) ? output.decoded.value as number : null, - tokenData: isNumber(output.decoded.token_data) ? output.decoded.token_data as number : null, - }; + outputs: tx.outputs.map( + (output: RawOutput): Output => { + const typedDecodedScript: DecodedScript = { + type: output.decoded.type as string, + address: output.decoded.address as string, + timelock: isNumber(output.decoded.timelock) + ? (output.decoded.timelock as number) + : null, + value: isNumber(output.decoded.value) + ? (output.decoded.value as number) + : null, + tokenData: isNumber(output.decoded.token_data) + ? (output.decoded.token_data as number) + : null, + }; - const typedOutput: Output = { - value: output.value as number, - tokenData: output.token_data as number, - script: output.script as string, - decoded: typedDecodedScript, - }; + const typedOutput: Output = { + value: output.value as number, + tokenData: output.token_data as number, + script: output.script as string, + decoded: typedDecodedScript, + }; - return typedOutput; - }), + return typedOutput; + } + ), parents: tx.parents, tokens: tx.tokens, raw: tx.raw as string, @@ -288,7 +320,6 @@ export const parseTx = (tx: RawTx): FullTx => { * @yields {MempoolEvent} */ export async function* syncLatestMempool(): AsyncGenerator { - logger.info(`Downloading mempool...`); let mempoolResp; try { @@ -372,9 +403,7 @@ export async function* syncToLatestBlock(): AsyncGenerator { const { meta } = ourBestBlockInFullNode; - if ((meta.voided_by && - meta.voided_by.length && - meta.voided_by.length > 0)) { + if (meta.voided_by && meta.voided_by.length && meta.voided_by.length > 0) { yield { type: 'reorg', success: false, @@ -388,44 +417,54 @@ export async function* syncToLatestBlock(): AsyncGenerator { yield { type: 'reorg', success: false, - message: 'Our height is higher than the wallet-service\'s height, we should reorg.', + message: + "Our height is higher than the wallet-service's height, we should reorg.", }; return; } - logger.info(`Downloading ${fullNodeBestBlock.height - ourBestBlock.height} blocks...`); + logger.info( + `Downloading ${fullNodeBestBlock.height - ourBestBlock.height} blocks...` + ); let success = true; - blockLoop: - for (let i = ourBestBlock.height + 1; i <= fullNodeBestBlock.height; i++) { + blockLoop: for ( + let i = ourBestBlock.height + 1; + i <= fullNodeBestBlock.height; + i++ + ) { const block: FullBlock = await downloadBlockByHeight(i); const preparedBlock: PreparedTx = prepareTx(block); // Ignore parents[0] because it is a block - const blockTxs = [ - block.parents[1], - block.parents[2], - ]; + const blockTxs = [block.parents[1], block.parents[2]]; // Download block transactions - const txList: Map = await recursivelyDownloadTx(block.txId, blockTxs); - const txs: FullTx[] = Array.from(txList.values()).sort((x, y) => x.timestamp - y.timestamp); + const txList: Map = await recursivelyDownloadTx( + block.txId, + blockTxs + ); + const txs: FullTx[] = Array.from(txList.values()).sort( + (x, y) => x.timestamp - y.timestamp + ); // Exclude duplicates: - const uniqueTxs: Record = txs.reduce((acc: Record, tx: FullTx) => { - if (tx.txId in acc) { - return acc; - } + const uniqueTxs: Record = txs.reduce( + (acc: Record, tx: FullTx) => { + if (tx.txId in acc) { + return acc; + } - return { - ...acc, - [tx.txId]: tx - }; - }, {}); + return { + ...acc, + [tx.txId]: tx, + }; + }, + {} + ); - txLoop: - for (const key of Object.keys(uniqueTxs)) { + for (const key of Object.keys(uniqueTxs)) { const preparedTx: PreparedTx = prepareTx({ ...uniqueTxs[key], height: block.height, // this tx is confirmed by the current block on the loop, we must send its height @@ -489,12 +528,12 @@ export class LRU { max: number; cache: Map; - constructor (max: number = 10) { + constructor(max: number = 10) { this.max = max; this.cache = new Map(); } - get (txId: string): any { + get(txId: string): any { const transaction = this.cache.get(txId); if (transaction) { @@ -506,7 +545,7 @@ export class LRU { return transaction; } - set (txId: string, transaction: any): void { + set(txId: string, transaction: any): void { if (this.cache.has(txId)) { // Refresh it in the map this.cache.delete(txId); @@ -520,7 +559,7 @@ export class LRU { this.cache.set(txId, transaction); } - first (): string { + first(): string { return this.cache.keys().next().value; } } diff --git a/test/machine.test.ts b/test/machine.test.ts index 9a770e15..cd69ce88 100644 --- a/test/machine.test.ts +++ b/test/machine.test.ts @@ -23,13 +23,13 @@ test('SyncMachine should start as idle', async () => { expect(syncMachine.state.value).toStrictEqual('idle'); }, 100); -test('An idle SyncMachine should transition to \'syncing\' when a NEW_BLOCK action is received', async () => { +test("An idle SyncMachine should transition to 'syncing' when a NEW_BLOCK action is received", async () => { const TestSyncMachine = SyncMachine.withConfig({ services: { syncHandler: () => () => { return () => {}; }, - } + }, }); // @ts-ignore @@ -40,13 +40,13 @@ test('An idle SyncMachine should transition to \'syncing\' when a NEW_BLOCK acti expect(syncMachine.state.value).toStrictEqual('syncing'); }, 500); -test('A SyncMachine in the syncing state should transition to \'failure\' when an ERROR event is received', async () => { +test("A SyncMachine in the syncing state should transition to 'failure' when an ERROR event is received", async () => { const TestSyncMachine = SyncMachine.withConfig({ services: { syncHandler: () => () => { return () => {}; }, - } + }, }); // @ts-ignore @@ -67,7 +67,7 @@ test('A SyncMachine in the syncing state should store hasMoreBlocks on context i syncHandler: () => () => { return () => {}; }, - } + }, }); // @ts-ignore @@ -84,13 +84,13 @@ test('A SyncMachine in the syncing state should store hasMoreBlocks on context i expect(syncMachine.state.context.hasMoreBlocks).toStrictEqual(true); }, 500); -test('A SyncMachine should transition to \'idle\' when it is on \'syncing\' state and received \'DONE\'', async () => { +test("A SyncMachine should transition to 'idle' when it is on 'syncing' state and received 'DONE'", async () => { const TestSyncMachine = SyncMachine.withConfig({ services: { syncHandler: () => () => { return () => {}; }, - } + }, }); // @ts-ignore @@ -107,13 +107,13 @@ test('A SyncMachine should transition to \'idle\' when it is on \'syncing\' stat expect(syncMachine.state.value).toStrictEqual('idle'); }, 500); -test('A SyncMachine should transition to \'syncing\' if hasMoreBlocks context is true on IDLE state entry', async () => { +test("A SyncMachine should transition to 'syncing' if hasMoreBlocks context is true on IDLE state entry", async () => { const TestSyncMachine = SyncMachine.withConfig({ services: { syncHandler: () => () => { return () => {}; }, - } + }, }); // @ts-ignore @@ -132,16 +132,15 @@ test('A SyncMachine should transition to \'syncing\' if hasMoreBlocks context is syncMachine.send({ type: 'DONE' }); expect(syncMachine.state.value).toStrictEqual('syncing'); - }, 500); -test('A SyncMachine should clear hasMoreBlocks from context when transitioning to \'syncing\'', async () => { +test("A SyncMachine should clear hasMoreBlocks from context when transitioning to 'syncing'", async () => { const TestSyncMachine = SyncMachine.withConfig({ services: { syncHandler: () => () => { return () => {}; }, - } + }, }); // @ts-ignore @@ -164,7 +163,6 @@ test('A SyncMachine should clear hasMoreBlocks from context when transitioning t expect(syncMachine.state.value).toStrictEqual('syncing'); expect(syncMachine.state.context.hasMoreBlocks).toStrictEqual(false); - }, 500); test('A SyncMachine should call the cleanupFn on the syncHandler service when state is transitioned out of syncing', async () => { @@ -175,7 +173,7 @@ test('A SyncMachine should call the cleanupFn on the syncHandler service when st syncHandler: () => () => { return mockCleanupFunction; }, - } + }, }); // @ts-ignore @@ -216,7 +214,7 @@ test('A SyncMachine should call the cleanupFn on the syncHandler service when st expect(mockCleanupFunction).toHaveBeenCalledTimes(3); }, 500); -test('The SyncMachine should transition to \'reorg\' state when a reorg is detected', async () => { +test("The SyncMachine should transition to 'reorg' state when a reorg is detected", async () => { const mockCleanupFunction = jest.fn(); const TestSyncMachine = SyncMachine.withConfig({ @@ -224,7 +222,7 @@ test('The SyncMachine should transition to \'reorg\' state when a reorg is detec syncHandler: () => () => { return mockCleanupFunction; }, - } + }, }); // @ts-ignore @@ -243,7 +241,7 @@ test('The SyncMachine should transition to \'reorg\' state when a reorg is detec expect(syncMachine.state.value).toStrictEqual('reorg'); }, 500); -test('Mempool: transition to \'idle\' on ERROR event', async () => { +test("Mempool: transition to 'idle' on ERROR event", async () => { const mockCleanupFunction = jest.fn(); const TestSyncMachine = SyncMachine.withConfig({ @@ -251,7 +249,7 @@ test('Mempool: transition to \'idle\' on ERROR event', async () => { syncHandler: () => () => { return mockCleanupFunction; }, - } + }, }); // @ts-ignore @@ -266,7 +264,7 @@ test('Mempool: transition to \'idle\' on ERROR event', async () => { expect(syncMachine.state.value).toStrictEqual('idle'); }, 500); -test('Mempool: transition to \'idle\' on DONE event', async () => { +test("Mempool: transition to 'idle' on DONE event", async () => { const mockCleanupFunction = jest.fn(); const TestSyncMachine = SyncMachine.withConfig({ @@ -274,7 +272,7 @@ test('Mempool: transition to \'idle\' on DONE event', async () => { syncHandler: () => () => { return mockCleanupFunction; }, - } + }, }); // @ts-ignore @@ -289,7 +287,7 @@ test('Mempool: transition to \'idle\' on DONE event', async () => { expect(syncMachine.state.value).toStrictEqual('idle'); }, 500); -test('Mempool: transition to \'idle\' on STOP event', async () => { +test("Mempool: transition to 'idle' on STOP event", async () => { const mockCleanupFunction = jest.fn(); const TestSyncMachine = SyncMachine.withConfig({ @@ -297,7 +295,7 @@ test('Mempool: transition to \'idle\' on STOP event', async () => { syncHandler: () => () => { return mockCleanupFunction; }, - } + }, }); // @ts-ignore @@ -312,7 +310,7 @@ test('Mempool: transition to \'idle\' on STOP event', async () => { expect(syncMachine.state.value).toStrictEqual('idle'); }, 500); -test('Mempool: transition to \'syncing\' on NEW_BLOCK event and back to \'mempoolsync\' when DONE with block sync', async () => { +test("Mempool: transition to 'syncing' on NEW_BLOCK event and back to 'mempoolsync' when DONE with block sync", async () => { const mockCleanupFunction = jest.fn(); const TestSyncMachine = SyncMachine.withConfig({ @@ -320,7 +318,7 @@ test('Mempool: transition to \'syncing\' on NEW_BLOCK event and back to \'mempoo syncHandler: () => () => { return mockCleanupFunction; }, - } + }, }); // @ts-ignore @@ -341,4 +339,4 @@ test('Mempool: transition to \'syncing\' on NEW_BLOCK event and back to \'mempoo // Will go to idle and straight back to mempoolsync, setting hasMempoolUpdate to false expect(syncMachine.state.context.hasMempoolUpdate).toStrictEqual(false); expect(syncMachine.state.value).toStrictEqual('mempoolsync'); -}, 500); \ No newline at end of file +}, 500); diff --git a/test/utils.test.ts b/test/utils.test.ts index d5b9f0dc..2c9875d1 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -14,23 +14,17 @@ import { BLOCK_BY_HEIGHT, MOCK_TXS, MOCK_FULL_TXS, - MOCK_NFT_TX, MOCK_CREATE_TOKEN_TX, generateBlock, } from './utils'; -import { - FullTx, -} from '../src/types'; -import { - prepareTx, - parseTx, -} from '../src/utils'; +import { FullTx } from '../src/types'; +import { prepareTx, parseTx } from '../src/utils'; import * as Utils from '../src/utils'; import * as FullNode from '../src/api/fullnode'; import * as Lambda from '../src/api/lambda'; import axios from 'axios'; - // @ts-ignore +// @ts-ignore import hathorLib from '@hathor/wallet-lib'; const { globalCache, syncToLatestBlock, LRU } = Utils; const { downloadTx } = FullNode; @@ -43,21 +37,35 @@ test('syncToLatestBlock should send transaction height for every block tx', asyn expect.hasAssertions(); const getFullNodeBestBlockSpy = jest.spyOn(FullNode, 'getFullNodeBestBlock'); - const getWalletServiceBestBlockSpy = jest.spyOn(Lambda, 'getWalletServiceBestBlock'); + const getWalletServiceBestBlockSpy = jest.spyOn( + Lambda, + 'getWalletServiceBestBlock' + ); const getBlockByTxIdSpy = jest.spyOn(FullNode, 'getBlockByTxId'); - const downloadBlockByHeightSpy = jest.spyOn(FullNode, 'downloadBlockByHeight'); + const downloadBlockByHeightSpy = jest.spyOn( + FullNode, + 'downloadBlockByHeight' + ); const recursivelyDownloadTxSpy = jest.spyOn(Utils, 'recursivelyDownloadTx'); const sendTxSpy = jest.spyOn(Lambda, 'sendTx'); - getFullNodeBestBlockSpy.mockReturnValue(Promise.resolve(generateBlock(MOCK_TXS[0], 1))); - getWalletServiceBestBlockSpy.mockReturnValue(Promise.resolve(generateBlock(MOCK_TXS[1], 0))); - getBlockByTxIdSpy.mockReturnValue(Promise.resolve(OUR_BEST_BLOCK_API_RESPONSE)); + getFullNodeBestBlockSpy.mockReturnValue( + Promise.resolve(generateBlock(MOCK_TXS[0], 1)) + ); + getWalletServiceBestBlockSpy.mockReturnValue( + Promise.resolve(generateBlock(MOCK_TXS[1], 0)) + ); + getBlockByTxIdSpy.mockReturnValue( + Promise.resolve(OUR_BEST_BLOCK_API_RESPONSE) + ); downloadBlockByHeightSpy.mockReturnValue(Promise.resolve(BLOCK_BY_HEIGHT)); - recursivelyDownloadTxSpy.mockReturnValue(Promise.resolve(new Map([ - [MOCK_FULL_TXS[0].txId, MOCK_FULL_TXS[0]], - ]))); + recursivelyDownloadTxSpy.mockReturnValue( + Promise.resolve( + new Map([[MOCK_FULL_TXS[0].txId, MOCK_FULL_TXS[0]]]) + ) + ); - const mockSendTxImplementation = jest.fn((tx) => { + const mockSendTxImplementation = jest.fn(tx => { return Promise.resolve({ success: true, }); @@ -68,50 +76,80 @@ test('syncToLatestBlock should send transaction height for every block tx', asyn await iterator.next(); - expect(mockFn).toHaveBeenCalledWith(prepareTx({ - ...MOCK_FULL_TXS[0], - height: BLOCK_BY_HEIGHT.height, - })); + expect(mockFn).toHaveBeenCalledWith( + prepareTx({ + ...MOCK_FULL_TXS[0], + height: BLOCK_BY_HEIGHT.height, + }) + ); }); test('syncToLatestBlockGen should yield an error when the latest block from the wallet-service is_voided', async () => { expect.hasAssertions(); const getFullNodeBestBlockSpy = jest.spyOn(FullNode, 'getFullNodeBestBlock'); - const getWalletServiceBestBlockSpy = jest.spyOn(Lambda, 'getWalletServiceBestBlock'); + const getWalletServiceBestBlockSpy = jest.spyOn( + Lambda, + 'getWalletServiceBestBlock' + ); const getBlockByTxIdSpy = jest.spyOn(FullNode, 'getBlockByTxId'); - const downloadBlockByHeightSpy = jest.spyOn(FullNode, 'downloadBlockByHeight'); - - getFullNodeBestBlockSpy.mockReturnValue(Promise.resolve(generateBlock(MOCK_TXS[0], 1))); - getWalletServiceBestBlockSpy.mockReturnValue(Promise.resolve(generateBlock(MOCK_TXS[1], 0))); - getBlockByTxIdSpy.mockReturnValue(Promise.resolve(OUR_BEST_BLOCK_API_RESPONSE_VOIDED)); + const downloadBlockByHeightSpy = jest.spyOn( + FullNode, + 'downloadBlockByHeight' + ); + + getFullNodeBestBlockSpy.mockReturnValue( + Promise.resolve(generateBlock(MOCK_TXS[0], 1)) + ); + getWalletServiceBestBlockSpy.mockReturnValue( + Promise.resolve(generateBlock(MOCK_TXS[1], 0)) + ); + getBlockByTxIdSpy.mockReturnValue( + Promise.resolve(OUR_BEST_BLOCK_API_RESPONSE_VOIDED) + ); downloadBlockByHeightSpy.mockReturnValue(Promise.resolve(BLOCK_BY_HEIGHT)); const iterator = syncToLatestBlock(); - const { value: { type, success, message } } = await iterator.next(); + const { + value: { type, success, message }, + } = await iterator.next(); expect(type).toStrictEqual('reorg'); expect(success).toStrictEqual(false); expect(message).toStrictEqual('Our best block was voided, we should reorg.'); }, 500); -test('syncToLatestBlockGen should yield an error when our best block height is higher than the fullnode\'s', async () => { +test("syncToLatestBlockGen should yield an error when our best block height is higher than the fullnode's", async () => { expect.hasAssertions(); const getFullNodeBestBlockSpy = jest.spyOn(FullNode, 'getFullNodeBestBlock'); - const getWalletServiceBestBlockSpy = jest.spyOn(Lambda, 'getWalletServiceBestBlock'); + const getWalletServiceBestBlockSpy = jest.spyOn( + Lambda, + 'getWalletServiceBestBlock' + ); const getBlockByTxIdSpy = jest.spyOn(FullNode, 'getBlockByTxId'); - const downloadBlockByHeightSpy = jest.spyOn(FullNode, 'downloadBlockByHeight'); - - getWalletServiceBestBlockSpy.mockReturnValue(Promise.resolve(generateBlock(MOCK_TXS[1], 6))); - getFullNodeBestBlockSpy.mockReturnValue(Promise.resolve(generateBlock(MOCK_TXS[0], 3))); - getBlockByTxIdSpy.mockReturnValue(Promise.resolve(OUR_BEST_BLOCK_API_RESPONSE_VOIDED)); + const downloadBlockByHeightSpy = jest.spyOn( + FullNode, + 'downloadBlockByHeight' + ); + + getWalletServiceBestBlockSpy.mockReturnValue( + Promise.resolve(generateBlock(MOCK_TXS[1], 6)) + ); + getFullNodeBestBlockSpy.mockReturnValue( + Promise.resolve(generateBlock(MOCK_TXS[0], 3)) + ); + getBlockByTxIdSpy.mockReturnValue( + Promise.resolve(OUR_BEST_BLOCK_API_RESPONSE_VOIDED) + ); downloadBlockByHeightSpy.mockReturnValue(Promise.resolve(BLOCK_BY_HEIGHT)); const iterator = syncToLatestBlock(); - const { value: { type, success, message } } = await iterator.next(); + const { + value: { type, success, message }, + } = await iterator.next(); expect(type).toStrictEqual('reorg'); expect(success).toStrictEqual(false); @@ -122,49 +160,84 @@ test('syncToLatestBlockGen should yield an error when it fails to send a block', expect.hasAssertions(); const getFullNodeBestBlockSpy = jest.spyOn(FullNode, 'getFullNodeBestBlock'); - const getWalletServiceBestBlockSpy = jest.spyOn(Lambda, 'getWalletServiceBestBlock'); + const getWalletServiceBestBlockSpy = jest.spyOn( + Lambda, + 'getWalletServiceBestBlock' + ); const getBlockByTxIdSpy = jest.spyOn(FullNode, 'getBlockByTxId'); const sendTxSpy = jest.spyOn(Lambda, 'sendTx'); - const downloadBlockByHeightSpy = jest.spyOn(FullNode, 'downloadBlockByHeight'); + const downloadBlockByHeightSpy = jest.spyOn( + FullNode, + 'downloadBlockByHeight' + ); const recursivelyDownloadTxSpy = jest.spyOn(Utils, 'recursivelyDownloadTx'); - getWalletServiceBestBlockSpy.mockReturnValue(Promise.resolve(generateBlock(MOCK_TXS[1], 3))); - getFullNodeBestBlockSpy.mockReturnValue(Promise.resolve(generateBlock(MOCK_TXS[0], 6))); - getBlockByTxIdSpy.mockReturnValue(Promise.resolve(OUR_BEST_BLOCK_API_RESPONSE)); - sendTxSpy.mockReturnValue(Promise.resolve({ success: false, message: 'generic error message' })); + getWalletServiceBestBlockSpy.mockReturnValue( + Promise.resolve(generateBlock(MOCK_TXS[1], 3)) + ); + getFullNodeBestBlockSpy.mockReturnValue( + Promise.resolve(generateBlock(MOCK_TXS[0], 6)) + ); + getBlockByTxIdSpy.mockReturnValue( + Promise.resolve(OUR_BEST_BLOCK_API_RESPONSE) + ); + sendTxSpy.mockReturnValue( + Promise.resolve({ success: false, message: 'generic error message' }) + ); downloadBlockByHeightSpy.mockReturnValue(Promise.resolve(BLOCK_BY_HEIGHT)); - recursivelyDownloadTxSpy.mockReturnValue(Promise.resolve(new Map())); + recursivelyDownloadTxSpy.mockReturnValue( + Promise.resolve(new Map()) + ); const iterator = syncToLatestBlock(); - const { value: { type, success, message } } = await iterator.next(); + const { + value: { type, success, message }, + } = await iterator.next(); expect(type).toStrictEqual('error'); expect(success).toStrictEqual(false); - expect(message).toStrictEqual('Failure on block 0000000f1fbb4bd8a8e71735af832be210ac9a6c1e2081b21faeea3c0f5797f7'); + expect(message).toStrictEqual( + 'Failure on block 0000000f1fbb4bd8a8e71735af832be210ac9a6c1e2081b21faeea3c0f5797f7' + ); }, 500); test('syncToLatestBlockGen should yield an error when it fails to send a transaction', async () => { expect.hasAssertions(); const getFullNodeBestBlockSpy = jest.spyOn(FullNode, 'getFullNodeBestBlock'); - const getWalletServiceBestBlockSpy = jest.spyOn(Lambda, 'getWalletServiceBestBlock'); + const getWalletServiceBestBlockSpy = jest.spyOn( + Lambda, + 'getWalletServiceBestBlock' + ); const getBlockByTxIdSpy = jest.spyOn(FullNode, 'getBlockByTxId'); const sendTxSpy = jest.spyOn(Lambda, 'sendTx'); - const downloadBlockByHeightSpy = jest.spyOn(FullNode, 'downloadBlockByHeight'); + const downloadBlockByHeightSpy = jest.spyOn( + FullNode, + 'downloadBlockByHeight' + ); const recursivelyDownloadTxSpy = jest.spyOn(Utils, 'recursivelyDownloadTx'); - getWalletServiceBestBlockSpy.mockReturnValue(Promise.resolve(generateBlock(MOCK_TXS[1], 3))); - getFullNodeBestBlockSpy.mockReturnValue(Promise.resolve(generateBlock(MOCK_TXS[0], 6))); - getBlockByTxIdSpy.mockReturnValue(Promise.resolve(OUR_BEST_BLOCK_API_RESPONSE)); + getWalletServiceBestBlockSpy.mockReturnValue( + Promise.resolve(generateBlock(MOCK_TXS[1], 3)) + ); + getFullNodeBestBlockSpy.mockReturnValue( + Promise.resolve(generateBlock(MOCK_TXS[0], 6)) + ); + getBlockByTxIdSpy.mockReturnValue( + Promise.resolve(OUR_BEST_BLOCK_API_RESPONSE) + ); // sendTxSpy.mockReturnValue(Promise.resolve({ success: false, message: 'generic error message' })); downloadBlockByHeightSpy.mockReturnValue(Promise.resolve(BLOCK_BY_HEIGHT)); - recursivelyDownloadTxSpy.mockReturnValue(Promise.resolve(new Map([[ - MOCK_FULL_TXS[0].txId as string, - MOCK_FULL_TXS[0] as FullTx, - ]]))); - - const mockSendTxImplementation = jest.fn((tx) => { + recursivelyDownloadTxSpy.mockReturnValue( + Promise.resolve( + new Map([ + [MOCK_FULL_TXS[0].txId as string, MOCK_FULL_TXS[0] as FullTx], + ]) + ) + ); + + const mockSendTxImplementation = jest.fn(tx => { if (hathorLib.helpers.isBlock(tx)) { // is block return Promise.resolve({ @@ -183,28 +256,46 @@ test('syncToLatestBlockGen should yield an error when it fails to send a transac const iterator = syncToLatestBlock(); - const { value: { type, success, message } } = await iterator.next(); + const { + value: { type, success, message }, + } = await iterator.next(); expect(type).toStrictEqual('transaction_failure'); expect(success).toStrictEqual(false); - expect(message).toStrictEqual('Failure on transaction 0000000033a3bb347e0401d85a70b38f0aa7b5e37ea4c70d7dacf8e493946e64 from block: 0000000f1fbb4bd8a8e71735af832be210ac9a6c1e2081b21faeea3c0f5797f7'); + expect(message).toStrictEqual( + 'Failure on transaction 0000000033a3bb347e0401d85a70b38f0aa7b5e37ea4c70d7dacf8e493946e64 from block: 0000000f1fbb4bd8a8e71735af832be210ac9a6c1e2081b21faeea3c0f5797f7' + ); }, 500); test('syncToLatestBlockGen should sync from our current height until the best block height', async () => { expect.hasAssertions(); const getFullNodeBestBlockSpy = jest.spyOn(FullNode, 'getFullNodeBestBlock'); - const getWalletServiceBestBlockSpy = jest.spyOn(Lambda, 'getWalletServiceBestBlock'); + const getWalletServiceBestBlockSpy = jest.spyOn( + Lambda, + 'getWalletServiceBestBlock' + ); const getBlockByTxIdSpy = jest.spyOn(FullNode, 'getBlockByTxId'); const sendTxSpy = jest.spyOn(Lambda, 'sendTx'); - const downloadBlockByHeightSpy = jest.spyOn(FullNode, 'downloadBlockByHeight'); + const downloadBlockByHeightSpy = jest.spyOn( + FullNode, + 'downloadBlockByHeight' + ); const recursivelyDownloadTxSpy = jest.spyOn(Utils, 'recursivelyDownloadTx'); - getWalletServiceBestBlockSpy.mockReturnValue(Promise.resolve(generateBlock(MOCK_TXS[1], 1))); - getFullNodeBestBlockSpy.mockReturnValue(Promise.resolve(generateBlock(MOCK_TXS[0], 3))); - getBlockByTxIdSpy.mockReturnValue(Promise.resolve(OUR_BEST_BLOCK_API_RESPONSE)); + getWalletServiceBestBlockSpy.mockReturnValue( + Promise.resolve(generateBlock(MOCK_TXS[1], 1)) + ); + getFullNodeBestBlockSpy.mockReturnValue( + Promise.resolve(generateBlock(MOCK_TXS[0], 3)) + ); + getBlockByTxIdSpy.mockReturnValue( + Promise.resolve(OUR_BEST_BLOCK_API_RESPONSE) + ); sendTxSpy.mockReturnValue(Promise.resolve({ success: true, message: 'ok' })); - recursivelyDownloadTxSpy.mockReturnValue(Promise.resolve(new Map())); + recursivelyDownloadTxSpy.mockReturnValue( + Promise.resolve(new Map()) + ); const mockBlockHeightImplementation = jest.fn((height: number) => { return Promise.resolve({ @@ -213,7 +304,9 @@ test('syncToLatestBlockGen should sync from our current height until the best bl }); }); - downloadBlockByHeightSpy.mockImplementationOnce(mockBlockHeightImplementation); + downloadBlockByHeightSpy.mockImplementationOnce( + mockBlockHeightImplementation + ); const iterator = syncToLatestBlock(); @@ -237,14 +330,14 @@ test('Dowload tx should cache transactions', async () => { const axiosGetSpy = jest.spyOn(axios, 'get'); - const mockAxiosGetImplementation = jest.fn((url) => { + const mockAxiosGetImplementation = jest.fn(url => { const [_, txId] = url.split('='); // is tx return Promise.resolve({ success: true, data: { - tx_id: txId - } + tx_id: txId, + }, }); }); @@ -255,7 +348,6 @@ test('Dowload tx should cache transactions', async () => { const cachedTx = globalCache.get('tx1'); expect(cachedTx).toStrictEqual({ tx_id: 'tx1' }); - }, 500); test('LRU cache', async () => { diff --git a/test/utils.ts b/test/utils.ts index cc712c9c..a6d849b5 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -1,9 +1,4 @@ -import { - FullBlock, - FullTx, - Block, - RawTxResponse, -} from '../src/types'; +import { FullBlock, FullTx, Block, RawTxResponse } from '../src/types'; export const MOCK_TXS = [ '0000018b4b08ad8668a42af30185e4ff228b5d2afc41ce7ee5cb7a085342ffda', @@ -16,64 +11,74 @@ export const MOCK_TXS = [ ]; export interface DecodedScript { - type: string, - address: string, - timelock?: number, + type: string; + address: string; + timelock?: number; } -export const MOCK_FULL_TXS: FullTx[] = [{ - txId: '0000000033a3bb347e0401d85a70b38f0aa7b5e37ea4c70d7dacf8e493946e64', - nonce: '2553516830', - timestamp: 1615397872, - version: 1, - weight: 17.52710175798647, - parents: [ - '00000000d016d0d677b533b37efd958ecfa1feefa123721240e55d1dac499f1a', - '00000000911bc85c571d8d671f202ae6ac4d50043800e72672ccb65e925853a3' - ], - inputs: [{ - value: 500, - tokenData: 1, - script: 'dqkU57viZuQ/P/Az3VqVQ9pxVi58uAmIrA==', - decoded: { - type: 'P2PKH', - address: 'HTeRZ6LksptwhxT1xxBuC8DxHWmpycHMEW', - timelock: null, - value: 500, - tokenData: 1 - }, - txId: '000000005b7069bf187363f79df0b14763b60a9ead153a9eab51cdaf5b6283ec', - index: 1 - }], - outputs: [{ - value: 475, - tokenData: 1, - script: 'dqkUi2Jrejdrx0C6QW/osvQNIqltHFmIrA==', - decoded: { - type: 'P2PKH', - address: 'HKE8DLbXXbMAjvMAfkZRLAC16CaoCY38we', - timelock: null, - value: 475, - tokenData: 1 - } - }, { - value: 25, - tokenData: 1, - script: 'dqkUCXfQI6LZVe5cqOy274Aoolf6Q7SIrA==', - decoded: { - type: 'P2PKH', - address: 'H7PBzpvKSBjAhoWVwiAKJVgJr9ZKy2QhpS', - timelock: null, - value: 25, - tokenData: 1 - } - }], - tokens: [{ - uid: '00000000f76262bb1cca969d952ac2f0e85f88ec34c31f26a13eb3c31e29d4ed', - name: 'Cathor', - symbol: 'CTHOR' - }], -}]; +export const MOCK_FULL_TXS: FullTx[] = [ + { + txId: '0000000033a3bb347e0401d85a70b38f0aa7b5e37ea4c70d7dacf8e493946e64', + nonce: '2553516830', + timestamp: 1615397872, + version: 1, + weight: 17.52710175798647, + parents: [ + '00000000d016d0d677b533b37efd958ecfa1feefa123721240e55d1dac499f1a', + '00000000911bc85c571d8d671f202ae6ac4d50043800e72672ccb65e925853a3', + ], + inputs: [ + { + value: 500, + tokenData: 1, + script: 'dqkU57viZuQ/P/Az3VqVQ9pxVi58uAmIrA==', + decoded: { + type: 'P2PKH', + address: 'HTeRZ6LksptwhxT1xxBuC8DxHWmpycHMEW', + timelock: null, + value: 500, + tokenData: 1, + }, + txId: + '000000005b7069bf187363f79df0b14763b60a9ead153a9eab51cdaf5b6283ec', + index: 1, + }, + ], + outputs: [ + { + value: 475, + tokenData: 1, + script: 'dqkUi2Jrejdrx0C6QW/osvQNIqltHFmIrA==', + decoded: { + type: 'P2PKH', + address: 'HKE8DLbXXbMAjvMAfkZRLAC16CaoCY38we', + timelock: null, + value: 475, + tokenData: 1, + }, + }, + { + value: 25, + tokenData: 1, + script: 'dqkUCXfQI6LZVe5cqOy274Aoolf6Q7SIrA==', + decoded: { + type: 'P2PKH', + address: 'H7PBzpvKSBjAhoWVwiAKJVgJr9ZKy2QhpS', + timelock: null, + value: 25, + tokenData: 1, + }, + }, + ], + tokens: [ + { + uid: '00000000f76262bb1cca969d952ac2f0e85f88ec34c31f26a13eb3c31e29d4ed', + name: 'Cathor', + symbol: 'CTHOR', + }, + ], + }, +]; export const generateBlock = (txId: string, height: number): Block => { return { @@ -93,7 +98,7 @@ export const OUR_BEST_BLOCK_API_RESPONSE = { parents: [ '00000154ac4fac94eaeafbecdca8d7e10e23953dd8250b0b154e5d2a31abc641', '006358e9e1e2b22c0658f3f14a315cd8c10ef2fd5c12b6cf3be64557a90f5bd3', - '0034557890ad299a2d683459132c0b09aba219e9aac67fbd31028432594022d7' + '0034557890ad299a2d683459132c0b09aba219e9aac67fbd31028432594022d7', ], inputs: [], outputs: [], @@ -106,7 +111,7 @@ export const OUR_BEST_BLOCK_API_RESPONSE = { spent_outputs: [], received_by: [], children: [ - '000000f4016a7402c71b772ad0bf91505d4083cb48723995bb3917d7cd0dd7cd' + '000000f4016a7402c71b772ad0bf91505d4083cb48723995bb3917d7cd0dd7cd', ], conflict_with: [], voided_by: [], @@ -115,8 +120,9 @@ export const OUR_BEST_BLOCK_API_RESPONSE = { score: 44.90233102099014, height: 646026, first_block: null, - validation: 'full' }, - spent_outputs: {} + validation: 'full', + }, + spent_outputs: {}, }; export const OUR_BEST_BLOCK_API_RESPONSE_VOIDED = { @@ -130,7 +136,7 @@ export const OUR_BEST_BLOCK_API_RESPONSE_VOIDED = { parents: [ '00000154ac4fac94eaeafbecdca8d7e10e23953dd8250b0b154e5d2a31abc641', '006358e9e1e2b22c0658f3f14a315cd8c10ef2fd5c12b6cf3be64557a90f5bd3', - '0034557890ad299a2d683459132c0b09aba219e9aac67fbd31028432594022d7' + '0034557890ad299a2d683459132c0b09aba219e9aac67fbd31028432594022d7', ], inputs: [], outputs: [], @@ -143,17 +149,20 @@ export const OUR_BEST_BLOCK_API_RESPONSE_VOIDED = { spent_outputs: [], received_by: [], children: [ - '000000f4016a7402c71b772ad0bf91505d4083cb48723995bb3917d7cd0dd7cd' + '000000f4016a7402c71b772ad0bf91505d4083cb48723995bb3917d7cd0dd7cd', ], conflict_with: [], - voided_by: ['000000f4016a7402c71b772ad0bf91505d4083cb48723995bb3917d7cd0dd7cd'], + voided_by: [ + '000000f4016a7402c71b772ad0bf91505d4083cb48723995bb3917d7cd0dd7cd', + ], twins: [], accumulated_weight: 23.09092323788272, score: 44.90233102099014, height: 646026, first_block: null, - validation: 'full' }, - spent_outputs: {} + validation: 'full', + }, + spent_outputs: {}, }; export const BLOCK_BY_HEIGHT: FullBlock = { @@ -170,95 +179,109 @@ export const BLOCK_BY_HEIGHT: FullBlock = { decoded: { type: 'P2PKH', address: 'WfJqB5SNHnkwXCCGLMBVPcwuVr94hq1oKH', - timelock: null + timelock: null, }, token: '00', - } + }, ], parents: [ '000005cbcb8b29f74446a260cd7d36fab3cba1295ac9fe904795d7b064e0e53c', '00975897028ceb037307327c953f5e7ad4d3f42402d71bd3d11ecb63ac39f01a', - '00e161a6b0bee1781ea9300680913fb76fd0fac4acab527cd9626cc1514abdc9' + '00e161a6b0bee1781ea9300680913fb76fd0fac4acab527cd9626cc1514abdc9', ], - height: 3 + height: 3, }; export const MOCK_CREATE_TOKEN_TX: RawTxResponse = { success: true, tx: { - hash: "0035db82f5993097515d5bcc9e869700d538332e017c7ff599c47f659ab63d42", - nonce: "180", + hash: '0035db82f5993097515d5bcc9e869700d538332e017c7ff599c47f659ab63d42', + nonce: '180', timestamp: 1620266110, version: 2, weight: 8.000001, parents: [ - "00504c97802cc199e2e418aefdafd1a627fdc4cf6fc9e4198b916c2456bbb203", - "0063b3ec31f8ffe0ebcb465e6c1111e1e9700926ac4d504c74b74b1af9cc6aad" + '00504c97802cc199e2e418aefdafd1a627fdc4cf6fc9e4198b916c2456bbb203', + '0063b3ec31f8ffe0ebcb465e6c1111e1e9700926ac4d504c74b74b1af9cc6aad', ], - inputs: [{ - value: 1, - token_data: 0, - script: "dqkURCVU2U54vCcmN8UVMeIBKQ+ldayIrA==", - decoded: { - type: "P2PKH", - address: "WUtMYoi96nNVgf6i3Rq3GuvJkYsbkx3KDi", - timelock: null, + inputs: [ + { value: 1, - token_data: 2 + token_data: 0, + script: 'dqkURCVU2U54vCcmN8UVMeIBKQ+ldayIrA==', + decoded: { + type: 'P2PKH', + address: 'WUtMYoi96nNVgf6i3Rq3GuvJkYsbkx3KDi', + timelock: null, + value: 1, + token_data: 2, + }, + tx_id: + '00504c97802cc199e2e418aefdafd1a627fdc4cf6fc9e4198b916c2456bbb203', + index: 1, }, - tx_id: "00504c97802cc199e2e418aefdafd1a627fdc4cf6fc9e4198b916c2456bbb203", - index: 1 - }], - outputs: [{ - value: 100, - token_data: 1, - script: "dqkU1vXqQItRBKC9TwophPs9I5reNnOIrA==", - decoded: { - type: "P2PKH", - address: "WiGe5TRjhAsrYP2dxp1zsgvYZqcBjXdWmy", - timelock: null, + ], + outputs: [ + { value: 100, - token_data: 1 - } - }, { - value: 1, - token_data: 129, - script: "dqkUvKVTGtZCXV/Wmwxsdc47FUnf8f6IrA==", - decoded: { - type: "P2PKH", - address: "WfsVxwxZhrfKHSYCeqPubQkWaeBcWZJ1ox", - timelock: null, + token_data: 1, + script: 'dqkU1vXqQItRBKC9TwophPs9I5reNnOIrA==', + decoded: { + type: 'P2PKH', + address: 'WiGe5TRjhAsrYP2dxp1zsgvYZqcBjXdWmy', + timelock: null, + value: 100, + token_data: 1, + }, + }, + { value: 1, - token_data: 129 - } - }, { - value: 2, - token_data: 129, - script: "dqkU6v6yo/94Z55pVSHPv+gTJWLln22IrA==", - decoded: { - type: "P2PKH", - address: "Wk6a7Xif6qYsprSzFmFhVXYrgQdqg7h1K6", - timelock: null, + token_data: 129, + script: 'dqkUvKVTGtZCXV/Wmwxsdc47FUnf8f6IrA==', + decoded: { + type: 'P2PKH', + address: 'WfsVxwxZhrfKHSYCeqPubQkWaeBcWZJ1ox', + timelock: null, + value: 1, + token_data: 129, + }, + }, + { value: 2, - token_data: 129 - } - }], - tokens: [{ - uid: "0035db82f5993097515d5bcc9e869700d538332e017c7ff599c47f659ab63d42", - name: "XCoin", - symbol: "XCN" - }, { - uid: "00", - name: null, - symbol: null - }], - token_name: "XCoin", - token_symbol: "XCN", - raw: "" + token_data: 129, + script: 'dqkU6v6yo/94Z55pVSHPv+gTJWLln22IrA==', + decoded: { + type: 'P2PKH', + address: 'Wk6a7Xif6qYsprSzFmFhVXYrgQdqg7h1K6', + timelock: null, + value: 2, + token_data: 129, + }, + }, + ], + tokens: [ + { + uid: '0035db82f5993097515d5bcc9e869700d538332e017c7ff599c47f659ab63d42', + name: 'XCoin', + symbol: 'XCN', + }, + { + uid: '00', + name: null, + symbol: null, + }, + ], + token_name: 'XCoin', + token_symbol: 'XCN', + raw: '', }, meta: { - hash: "0035db82f5993097515d5bcc9e869700d538332e017c7ff599c47f659ab63d42", - spent_outputs: [ [ 0, [] ], [ 1, [] ], [ 2, [] ] ], + hash: '0035db82f5993097515d5bcc9e869700d538332e017c7ff599c47f659ab63d42', + spent_outputs: [ + [0, []], + [1, []], + [2, []], + ], received_by: [], children: [], conflict_with: [], @@ -267,103 +290,104 @@ export const MOCK_CREATE_TOKEN_TX: RawTxResponse = { accumulated_weight: 25.78875940418488, score: 0.0, height: 0, - first_block: "000000bd45ecc5119963cc3fa03e894f574e69811eef266ed7c6a0d4c1e1806c" + first_block: + '000000bd45ecc5119963cc3fa03e894f574e69811eef266ed7c6a0d4c1e1806c', }, - spent_outputs: {} + spent_outputs: {}, }; export const MOCK_NFT_TX: RawTxResponse = { - "success": true, - "tx": { - "hash": "0055c424b9038b0a8888b574ccdb1933a007fdfc15b91a4b38a48cc883b540bf", - "nonce": "389", - "timestamp": 1626187098, - "version": 2, - "weight": 8.0, - "parents": [ - "0055b20066e8168ad8f05e82d66a34d19970cfb1861281735215cdd84744d842", - "00bb42880bd1183ce34df2185d1431f531a0a95af3556e368fa72e462edf7a9f" + success: true, + tx: { + hash: '0055c424b9038b0a8888b574ccdb1933a007fdfc15b91a4b38a48cc883b540bf', + nonce: '389', + timestamp: 1626187098, + version: 2, + weight: 8.0, + parents: [ + '0055b20066e8168ad8f05e82d66a34d19970cfb1861281735215cdd84744d842', + '00bb42880bd1183ce34df2185d1431f531a0a95af3556e368fa72e462edf7a9f', + ], + inputs: [ + { + value: 2, + token_data: 0, + script: 'dqkU8uf1ieRE8taN5bCNug5z5UHMO6eIrA==', + decoded: { + type: 'P2PKH', + address: 'WkpQH9t4ue4LbTQKAEWssiXnYHC8CyMp7J', + timelock: null, + value: 2, + token_data: 0, + }, + tx_id: + '0055b20066e8168ad8f05e82d66a34d19970cfb1861281735215cdd84744d842', + index: 1, + }, ], - "inputs": [{ - "value": 2, - "token_data": 0, - "script": "dqkU8uf1ieRE8taN5bCNug5z5UHMO6eIrA==", - "decoded": { - "type": "P2PKH", - "address": "WkpQH9t4ue4LbTQKAEWssiXnYHC8CyMp7J", - "timelock": null, - "value": 2, - "token_data": 0 + outputs: [ + { + value: 1, + token_data: 0, + script: + 'TFFodHRwczovL2lwZnMuaW8vaXBmcy9RbWJIdEZrWWlGSG5XdEV6bm01RFFHTVNOSmdwTExXeDdRNlBxdHAxb0NiQlpwL21ldGFkYXRhLmpzb26s', + decoded: {}, }, - "tx_id": "0055b20066e8168ad8f05e82d66a34d19970cfb1861281735215cdd84744d842", - "index": 1 - }], - "outputs": [{ - "value": 1, - "token_data": 0, - "script": "TFFodHRwczovL2lwZnMuaW8vaXBmcy9RbWJIdEZrWWlGSG5XdEV6bm01RFFHTVNOSmdwTExXeDdRNlBxdHAxb0NiQlpwL21ldGFkYXRhLmpzb26s", - "decoded": {} + { + value: 2, + token_data: 129, + script: 'dqkUYpULlr3iJ6sZbP3YIfgL52fasneIrA==', + decoded: { + type: 'P2PKH', + address: 'WXfHeaEtr3fS9ex42V5chr2jY7wb5tdcWD', + timelock: null, + value: 2, + token_data: 129, + }, }, { - "value": 2, - "token_data": 129, - "script": "dqkUYpULlr3iJ6sZbP3YIfgL52fasneIrA==", - "decoded": { - "type": "P2PKH", - "address": "WXfHeaEtr3fS9ex42V5chr2jY7wb5tdcWD", - "timelock": null, - "value": 2, - "token_data": 129 - } + value: 1, + token_data: 1, + script: 'dqkUYpULlr3iJ6sZbP3YIfgL52fasneIrA==', + decoded: { + type: 'P2PKH', + address: 'WXfHeaEtr3fS9ex42V5chr2jY7wb5tdcWD', + timelock: null, + value: 1, + token_data: 1, + }, }, + ], + tokens: [ { - "value": 1, - "token_data": 1, - "script": "dqkUYpULlr3iJ6sZbP3YIfgL52fasneIrA==", - "decoded": { - "type": "P2PKH", - "address": "WXfHeaEtr3fS9ex42V5chr2jY7wb5tdcWD", - "timelock": null, - "value": 1, - "token_data": 1 - } - } + uid: '0055c424b9038b0a8888b574ccdb1933a007fdfc15b91a4b38a48cc883b540bf', + name: 'Furia Special Edition', + symbol: 'DPL9', + }, ], - "tokens": [{ - "uid": "0055c424b9038b0a8888b574ccdb1933a007fdfc15b91a4b38a48cc883b540bf", - "name": "Furia Special Edition", - "symbol": "DPL9" - }], - "token_name": "Furia Special Edition", - "token_symbol": "DPL9", - "raw": "000201030055b20066e8168ad8f05e82d66a34d19970cfb1861281735215cdd84744d8420100694630440220692c2a95bbb335729520bc1717d9b6da7361ebfc5fb500e6ac45ed4243b4912202201980930659406e06a0f0117a9eb01a29f06faaebf9e4ec58cc96941681043a2e21020377708f22ac1e829c9cfbfd891bb99a47f460bf45d71f4841db404cbefdcb93000000010000544c5168747470733a2f2f697066732e696f2f697066732f516d624874466b596946486e5774457a6e6d354451474d534e4a67704c4c577837513650717470316f4362425a702f6d657461646174612e6a736f6eac0000000281001976a91462950b96bde227ab196cfdd821f80be767dab27788ac0000000101001976a91462950b96bde227ab196cfdd821f80be767dab27788ac01154675726961205370656369616c2045646974696f6e0444504c39402000000000000060eda55a020055b20066e8168ad8f05e82d66a34d19970cfb1861281735215cdd84744d84200bb42880bd1183ce34df2185d1431f531a0a95af3556e368fa72e462edf7a9f00000185" + token_name: 'Furia Special Edition', + token_symbol: 'DPL9', + raw: + '000201030055b20066e8168ad8f05e82d66a34d19970cfb1861281735215cdd84744d8420100694630440220692c2a95bbb335729520bc1717d9b6da7361ebfc5fb500e6ac45ed4243b4912202201980930659406e06a0f0117a9eb01a29f06faaebf9e4ec58cc96941681043a2e21020377708f22ac1e829c9cfbfd891bb99a47f460bf45d71f4841db404cbefdcb93000000010000544c5168747470733a2f2f697066732e696f2f697066732f516d624874466b596946486e5774457a6e6d354451474d534e4a67704c4c577837513650717470316f4362425a702f6d657461646174612e6a736f6eac0000000281001976a91462950b96bde227ab196cfdd821f80be767dab27788ac0000000101001976a91462950b96bde227ab196cfdd821f80be767dab27788ac01154675726961205370656369616c2045646974696f6e0444504c39402000000000000060eda55a020055b20066e8168ad8f05e82d66a34d19970cfb1861281735215cdd84744d84200bb42880bd1183ce34df2185d1431f531a0a95af3556e368fa72e462edf7a9f00000185', }, - "meta": { - "hash": "0055c424b9038b0a8888b574ccdb1933a007fdfc15b91a4b38a48cc883b540bf", - "spent_outputs": [ - [ - 0, - [] - ], - [ - 1, - [] - ], - [ - 2, - [] - ] + meta: { + hash: '0055c424b9038b0a8888b574ccdb1933a007fdfc15b91a4b38a48cc883b540bf', + spent_outputs: [ + [0, []], + [1, []], + [2, []], ], - "received_by": [], - "children": [], - "conflict_with": [], - "voided_by": [], - "twins": [], - "accumulated_weight": 8.0, - "score": 0, - "height": 0, - "first_block": "000000b17b22dd27fb1205a1f810a2c4d40de1e20af140e001529642c4b173a1", - "validation": "full" + received_by: [], + children: [], + conflict_with: [], + voided_by: [], + twins: [], + accumulated_weight: 8.0, + score: 0, + height: 0, + first_block: + '000000b17b22dd27fb1205a1f810a2c4d40de1e20af140e001529642c4b173a1', + validation: 'full', }, - "spent_outputs": {} + spent_outputs: {}, };