mirror of
https://github.com/alexgo-io/stacks-blockchain-api.git
synced 2026-01-12 16:53:19 +08:00
208 lines
7.4 KiB
TypeScript
208 lines
7.4 KiB
TypeScript
import * as express from 'express';
|
|
import { addAsync, RouterWithAsync } from '@awaitjs/express';
|
|
import { DataStore, DbBlock, DbTx, DbMempoolTx } from '../../datastore/common';
|
|
import { isValidPrincipal, has0xPrefix } from '../../helpers';
|
|
import {
|
|
Transaction,
|
|
Block,
|
|
SearchResult,
|
|
BlockSearchResult,
|
|
TxSearchResult,
|
|
MempoolTxSearchResult,
|
|
ContractSearchResult,
|
|
AddressSearchResult,
|
|
SearchErrorResult,
|
|
} from '@stacks/stacks-blockchain-api-types';
|
|
import { getTxTypeString } from '../controllers/db-controller';
|
|
import { address } from 'bitcoinjs-lib';
|
|
|
|
const enum SearchResultType {
|
|
TxId = 'tx_id',
|
|
MempoolTxId = 'mempool_tx_id',
|
|
BlockHash = 'block_hash',
|
|
StandardAddress = 'standard_address',
|
|
ContractAddress = 'contract_address',
|
|
UnknownHash = 'unknown_hash',
|
|
InvalidTerm = 'invalid_term',
|
|
}
|
|
|
|
export function createSearchRouter(db: DataStore): RouterWithAsync {
|
|
const router = addAsync(express.Router());
|
|
|
|
const performSearch = async (term: string): Promise<SearchResult> => {
|
|
// Check if term is a 32-byte hash, e.g.:
|
|
// `0x4ac9b89ec7f2a0ca3b4399888904f171d7bdf3460b1c63ea86c28a83c2feaad8`
|
|
// `4ac9b89ec7f2a0ca3b4399888904f171d7bdf3460b1c63ea86c28a83c2feaad8`
|
|
let hashBuffer: Buffer | undefined;
|
|
if (term.length === 66 && has0xPrefix(term)) {
|
|
hashBuffer = Buffer.from(term.slice(2), 'hex');
|
|
} else if (term.length === 64) {
|
|
hashBuffer = Buffer.from(term, 'hex');
|
|
}
|
|
if (hashBuffer !== undefined && hashBuffer.length === 32) {
|
|
const hash = '0x' + hashBuffer.toString('hex');
|
|
const queryResult = await db.searchHash({ hash });
|
|
if (queryResult.found) {
|
|
if (queryResult.result.entity_type === 'block_hash') {
|
|
const blockData = queryResult.result.entity_data as DbBlock;
|
|
const blockResult: BlockSearchResult = {
|
|
found: true,
|
|
result: {
|
|
entity_id: queryResult.result.entity_id,
|
|
entity_type: SearchResultType.BlockHash,
|
|
block_data: {
|
|
canonical: blockData.canonical,
|
|
hash: blockData.block_hash,
|
|
parent_block_hash: blockData.parent_block_hash,
|
|
burn_block_time: blockData.burn_block_time,
|
|
height: blockData.block_height,
|
|
},
|
|
},
|
|
};
|
|
return blockResult;
|
|
} else if (queryResult.result.entity_type === 'tx_id') {
|
|
const txData = queryResult.result.entity_data as DbTx;
|
|
const txResult: TxSearchResult = {
|
|
found: true,
|
|
result: {
|
|
entity_id: queryResult.result.entity_id,
|
|
entity_type: SearchResultType.TxId,
|
|
tx_data: {
|
|
canonical: txData.canonical,
|
|
block_hash: txData.block_hash,
|
|
burn_block_time: txData.burn_block_time,
|
|
block_height: txData.block_height,
|
|
tx_type: getTxTypeString(txData.type_id),
|
|
},
|
|
},
|
|
};
|
|
return txResult;
|
|
} else if (queryResult.result.entity_type === 'mempool_tx_id') {
|
|
const txData = queryResult.result.entity_data as DbMempoolTx;
|
|
const txResult: MempoolTxSearchResult = {
|
|
found: true,
|
|
result: {
|
|
entity_id: queryResult.result.entity_id,
|
|
entity_type: SearchResultType.MempoolTxId,
|
|
tx_data: {
|
|
tx_type: getTxTypeString(txData.type_id),
|
|
},
|
|
},
|
|
};
|
|
return txResult;
|
|
} else {
|
|
throw new Error(
|
|
`Unexpected entity_type from db search result: ${queryResult.result.entity_type}`
|
|
);
|
|
}
|
|
} else {
|
|
const unknownResult: SearchErrorResult = {
|
|
found: false,
|
|
result: {
|
|
entity_type: SearchResultType.UnknownHash,
|
|
},
|
|
error: `No block or transaction found with hash "${hash}"`,
|
|
};
|
|
return unknownResult;
|
|
}
|
|
}
|
|
|
|
// Check if term is an account or contract principal address, e.g.:
|
|
// `ST3DQ94YDRH07GRKTCNN5FTW962ACVADVJZD7GSK3`
|
|
// `ST2TJRHDHMYBQ417HFB0BDX430TQA5PXRX6495G1V.contract-name`
|
|
const principalCheck = isValidPrincipal(term);
|
|
if (principalCheck) {
|
|
const principalResult = await db.searchPrincipal({ principal: term });
|
|
const entityType =
|
|
principalCheck.type === 'contractAddress'
|
|
? SearchResultType.ContractAddress
|
|
: SearchResultType.StandardAddress;
|
|
|
|
if (principalResult.found) {
|
|
// Check if the contract has an associated tx
|
|
if (entityType === SearchResultType.ContractAddress && principalResult.result.entity_data) {
|
|
// Check if associated tx is mined (non-mempool)
|
|
if ((principalResult.result.entity_data as DbTx).block_hash) {
|
|
const txData = principalResult.result.entity_data as DbTx;
|
|
const contractResult: ContractSearchResult = {
|
|
found: true,
|
|
result: {
|
|
entity_id: principalResult.result.entity_id,
|
|
entity_type: entityType,
|
|
tx_data: {
|
|
canonical: txData.canonical,
|
|
block_hash: txData.block_hash,
|
|
burn_block_time: txData.burn_block_time,
|
|
block_height: txData.block_height,
|
|
tx_type: getTxTypeString(txData.type_id),
|
|
tx_id: txData.tx_id,
|
|
},
|
|
},
|
|
};
|
|
return contractResult;
|
|
} else {
|
|
// Associated tx is a mempool tx
|
|
const txData = principalResult.result.entity_data as DbMempoolTx;
|
|
const contractResult: ContractSearchResult = {
|
|
found: true,
|
|
result: {
|
|
entity_id: principalResult.result.entity_id,
|
|
entity_type: entityType,
|
|
tx_data: {
|
|
tx_type: getTxTypeString(txData.type_id),
|
|
tx_id: txData.tx_id,
|
|
},
|
|
},
|
|
};
|
|
return contractResult;
|
|
}
|
|
} else if (entityType === SearchResultType.ContractAddress) {
|
|
// Contract has no associated tx.
|
|
// TODO: Can a non-materialized contract principal be an asset transfer recipient?
|
|
const addrResult: ContractSearchResult = {
|
|
found: true,
|
|
result: {
|
|
entity_id: principalResult.result.entity_id,
|
|
entity_type: entityType,
|
|
},
|
|
};
|
|
return addrResult;
|
|
}
|
|
const addrResult: AddressSearchResult = {
|
|
found: true,
|
|
result: {
|
|
entity_id: principalResult.result.entity_id,
|
|
entity_type: entityType,
|
|
},
|
|
};
|
|
return addrResult;
|
|
} else {
|
|
return {
|
|
found: false,
|
|
result: { entity_type: entityType },
|
|
error: `No principal found with address "${term}"`,
|
|
};
|
|
}
|
|
}
|
|
|
|
return {
|
|
found: false,
|
|
result: { entity_type: SearchResultType.InvalidTerm },
|
|
error: `The term "${term}" is not a valid block hash, transaction ID, contract principal, or account address principal`,
|
|
};
|
|
};
|
|
|
|
router.getAsync('/:term', async (req, res) => {
|
|
const { term: rawTerm } = req.params;
|
|
const term = rawTerm.trim();
|
|
|
|
const searchResult = await performSearch(term);
|
|
if (!searchResult.found) {
|
|
res.status(404);
|
|
}
|
|
res.json(searchResult);
|
|
});
|
|
|
|
return router;
|
|
}
|