mirror of
https://github.com/alexgo-io/stacks-blockchain-api.git
synced 2026-05-13 20:16:45 +08:00
feat: return all the contracts implement a given trait
* feat: implement an api to get all contracts which implement a given trait * test: add test cases for contract/trait endpoint * docs: added docs for contract/trait endpoint * fix: add return statment after response * perf: add index for jsonb abi column * refactor: use createIndex method * fix: use get request instead of post for contracts trait * docs: update docs for trait/contract api * fix: fixed rebase issue * test: add test for large query data * refactor: change path of contract/by_trait api
This commit is contained in:
14
docs/api/contract/smart-contract-list-response.example.json
Normal file
14
docs/api/contract/smart-contract-list-response.example.json
Normal file
File diff suppressed because one or more lines are too long
29
docs/api/contract/smart-contract-list-response.schema.json
Normal file
29
docs/api/contract/smart-contract-list-response.schema.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "GET list of contracts",
|
||||
"title": "ContractListResponse",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"results",
|
||||
"limit",
|
||||
"offset"
|
||||
],
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "The number of contracts to return"
|
||||
},
|
||||
"offset": {
|
||||
"type": "integer",
|
||||
"description": "The number to contracts to skip (starting at `0`)",
|
||||
"default": 0
|
||||
},
|
||||
"results": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "../../entities/contracts/smart-contract.schema.json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
8
docs/entities/contracts/smart-contract.example.json
Normal file
8
docs/entities/contracts/smart-contract.example.json
Normal file
File diff suppressed because one or more lines are too long
31
docs/entities/contracts/smart-contract.schema.json
Normal file
31
docs/entities/contracts/smart-contract.schema.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "SmartContract",
|
||||
"description": "A Smart Contract Detail",
|
||||
"required": [
|
||||
"tx_id",
|
||||
"canonical",
|
||||
"block_height",
|
||||
"source_code",
|
||||
"abi"
|
||||
],
|
||||
"properties": {
|
||||
"tx_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"canonical": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"block_height": {
|
||||
"type": "integer"
|
||||
},
|
||||
"source_code": {
|
||||
"type": "string"
|
||||
},
|
||||
"abi": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
26
docs/generated.d.ts
vendored
26
docs/generated.d.ts
vendored
@@ -29,6 +29,7 @@ export type SchemaMergeRootStub =
|
||||
| BurnchainRewardSlotHolderListResponse
|
||||
| BurnchainRewardListResponse
|
||||
| ReadOnlyFunctionSuccessResponse
|
||||
| ContractListResponse
|
||||
| AccountDataResponse
|
||||
| MapEntryResponse
|
||||
| ContractInterfaceResponse
|
||||
@@ -143,6 +144,7 @@ export type SchemaMergeRootStub =
|
||||
| BurnchainReward
|
||||
| BurnchainRewardsTotal
|
||||
| ReadOnlyFunctionArgs
|
||||
| SmartContract
|
||||
| {
|
||||
target_block_time: number;
|
||||
}
|
||||
@@ -1451,6 +1453,30 @@ export interface ReadOnlyFunctionSuccessResponse {
|
||||
result?: string;
|
||||
cause?: string;
|
||||
}
|
||||
/**
|
||||
* GET list of contracts
|
||||
*/
|
||||
export interface ContractListResponse {
|
||||
/**
|
||||
* The number of contracts to return
|
||||
*/
|
||||
limit: number;
|
||||
/**
|
||||
* The number to contracts to skip (starting at `0`)
|
||||
*/
|
||||
offset: number;
|
||||
results: SmartContract[];
|
||||
}
|
||||
/**
|
||||
* A Smart Contract Detail
|
||||
*/
|
||||
export interface SmartContract {
|
||||
tx_id: string;
|
||||
canonical: boolean;
|
||||
block_height: number;
|
||||
source_code: string;
|
||||
abi: string;
|
||||
}
|
||||
/**
|
||||
* GET request for account data
|
||||
*/
|
||||
|
||||
@@ -774,6 +774,42 @@ paths:
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
/extended/v1/contract/by_trait:
|
||||
get:
|
||||
summary: Get contracts by trait
|
||||
description: Get all contracts implement a given trait
|
||||
tags:
|
||||
- Smart Contracts
|
||||
operationId: get_contracts_by_trait
|
||||
responses:
|
||||
200:
|
||||
description: List of contracts implement given trait
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ./api/contract/smart-contract-list-response.schema.json
|
||||
example:
|
||||
$ref: ./api/contract/smart-contract-list-response.example.json
|
||||
parameters:
|
||||
- name: trait_abi
|
||||
in: query
|
||||
description: JSON abi of the trait
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: limit
|
||||
in: query
|
||||
description: max number of contracts fetch
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
- name: offset
|
||||
in: query
|
||||
description: index of first contract event to fetch
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
|
||||
/extended/v1/contract/{contract_id}/events:
|
||||
get:
|
||||
summary: Get contract events
|
||||
@@ -782,6 +818,12 @@ paths:
|
||||
- Smart Contracts
|
||||
operationId: get_contract_events_by_id
|
||||
parameters:
|
||||
- name: contract_id
|
||||
in: path
|
||||
description: Contract identifier formatted as `<contract_address>.<contract_name>`
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: limit
|
||||
in: query
|
||||
description: max number of contract events to fetch
|
||||
@@ -794,12 +836,6 @@ paths:
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
- name: contract_id
|
||||
in: path
|
||||
description: Contract identifier formatted as `<contract_address>.<contract_name>`
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: unanchored
|
||||
in: query
|
||||
description: Include transaction data from unanchored (i.e. unconfirmed) microblocks
|
||||
|
||||
@@ -1011,7 +1011,7 @@ function parseContractCallMetadata(
|
||||
if (!contract.result.abi) {
|
||||
return parsedTx;
|
||||
}
|
||||
const contractAbi: ClarityAbi = JSON.parse(contract.result.abi);
|
||||
const contractAbi: ClarityAbi = JSON.parse(JSON.stringify(contract.result.abi));
|
||||
const functionAbi = contractAbi.functions.find(
|
||||
fn => fn.name === parsedTx.contract_call.function_name
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ClarityAbi } from '@stacks/transactions';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { has0xPrefix } from './../helpers';
|
||||
|
||||
@@ -176,3 +177,18 @@ export function parseUntilBlockQuery(
|
||||
}
|
||||
handleBadRequest(res, next, 'until_block must be either `string` or `number`');
|
||||
}
|
||||
|
||||
export function parseTraitAbi(req: Request, res: Response, next: NextFunction): ClarityAbi | never {
|
||||
if (!('trait_abi' in req.query)) {
|
||||
handleBadRequest(res, next, `Can't find query param 'trait_abi'`);
|
||||
}
|
||||
const trait = req.query.trait_abi;
|
||||
if (typeof trait === 'string') {
|
||||
const trait_abi: ClarityAbi = JSON.parse(trait);
|
||||
if (!('functions' in trait_abi)) {
|
||||
handleBadRequest(res, next, `Invalid 'trait_abi'`);
|
||||
}
|
||||
return trait_abi;
|
||||
}
|
||||
handleBadRequest(res, next, `Invalid 'trait_abi'`);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import { addAsync, RouterWithAsync } from '@awaitjs/express';
|
||||
import { DataStore } from '../../datastore/common';
|
||||
import { parseLimitQuery, parsePagingQueryInput } from '../pagination';
|
||||
import { parseDbEvent } from '../controllers/db-controller';
|
||||
import { ClarityAbi, ClarityAbiTypeId } from '@stacks/transactions';
|
||||
import { parseTraitAbi } from '../query-helpers';
|
||||
|
||||
const MAX_EVENTS_PER_REQUEST = 50;
|
||||
const parseContractEventsQueryLimit = parseLimitQuery({
|
||||
@@ -12,6 +14,23 @@ const parseContractEventsQueryLimit = parseLimitQuery({
|
||||
|
||||
export function createContractRouter(db: DataStore): RouterWithAsync {
|
||||
const router = addAsync(express.Router());
|
||||
|
||||
router.getAsync('/by_trait', async (req, res, next) => {
|
||||
const trait_abi = parseTraitAbi(req, res, next);
|
||||
const limit = parseContractEventsQueryLimit(req.query.limit ?? 20);
|
||||
const offset = parsePagingQueryInput(req.query.offset ?? 0);
|
||||
const smartContracts = await db.getSmartContractByTrait({
|
||||
trait: trait_abi,
|
||||
limit,
|
||||
offset,
|
||||
});
|
||||
if (!smartContracts.found) {
|
||||
res.status(404).json({ error: `cannot find contract for this trait` });
|
||||
return;
|
||||
}
|
||||
res.json({ limit, offset, results: smartContracts.result });
|
||||
});
|
||||
|
||||
router.getAsync('/:contract_id', async (req, res) => {
|
||||
const { contract_id } = req.params;
|
||||
const contractQuery = await db.getSmartContract(contract_id);
|
||||
|
||||
@@ -652,6 +652,12 @@ export interface DataStore extends DataStoreEventEmitter {
|
||||
offset: number;
|
||||
}): Promise<FoundOrNot<DbSmartContractEvent[]>>;
|
||||
|
||||
getSmartContractByTrait(args: {
|
||||
trait: ClarityAbi;
|
||||
limit: number;
|
||||
offset: number;
|
||||
}): Promise<FoundOrNot<DbSmartContract[]>>;
|
||||
|
||||
update(data: DataStoreBlockUpdateData): Promise<void>;
|
||||
|
||||
updateMicroblocks(data: DataStoreMicroblockUpdateData): Promise<void>;
|
||||
|
||||
@@ -43,6 +43,7 @@ import { logger, FoundOrNot } from '../helpers';
|
||||
import { AddressTokenOfferingLocked, TransactionType } from '@stacks/stacks-blockchain-api-types';
|
||||
import { getTxTypeId } from '../api/controllers/db-controller';
|
||||
import { RawTxQueryResult } from './postgres-store';
|
||||
import { ClarityAbi } from '@stacks/transactions';
|
||||
|
||||
export class MemoryDataStore
|
||||
extends (EventEmitter as { new (): DataStoreEventEmitter })
|
||||
@@ -760,4 +761,12 @@ export class MemoryDataStore
|
||||
): Promise<DbTokenMetadataQueueEntry[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
getSmartContractByTrait(args: {
|
||||
trait: ClarityAbi;
|
||||
limit: number;
|
||||
offset: number;
|
||||
}): Promise<FoundOrNot<DbSmartContract[]>> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4677,6 +4677,59 @@ export class PgDataStore
|
||||
}
|
||||
await client.query(`REFRESH MATERIALIZED VIEW ${viewName}`);
|
||||
}
|
||||
async getSmartContractByTrait(args: {
|
||||
trait: ClarityAbi;
|
||||
limit: number;
|
||||
offset: number;
|
||||
}): Promise<FoundOrNot<DbSmartContract[]>> {
|
||||
const traitFunctionList = args.trait.functions.map(traitFunction => {
|
||||
return {
|
||||
name: traitFunction.name,
|
||||
access: traitFunction.access,
|
||||
args: traitFunction.args.map(arg => {
|
||||
return {
|
||||
type: arg.type,
|
||||
};
|
||||
}),
|
||||
outputs: traitFunction.outputs,
|
||||
};
|
||||
});
|
||||
|
||||
return this.query(async client => {
|
||||
const result = await client.query<{
|
||||
tx_id: Buffer;
|
||||
canonical: boolean;
|
||||
contract_id: string;
|
||||
block_height: number;
|
||||
source_code: string;
|
||||
abi: string;
|
||||
}>(
|
||||
`
|
||||
SELECT tx_id, canonical, contract_id, block_height, source_code, abi
|
||||
FROM smart_contracts
|
||||
WHERE abi->'functions' @> $1::jsonb AND canonical = true AND microblock_canonical = true
|
||||
ORDER BY block_height DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
`,
|
||||
[JSON.stringify(traitFunctionList), args.limit, args.offset]
|
||||
);
|
||||
if (result.rowCount === 0) {
|
||||
return { found: false } as const;
|
||||
}
|
||||
const smartContracts = result.rows.map(row => {
|
||||
const smartContract: DbSmartContract = {
|
||||
tx_id: bufferToHexPrefixString(row.tx_id),
|
||||
canonical: row.canonical,
|
||||
contract_id: row.contract_id,
|
||||
block_height: row.block_height,
|
||||
source_code: row.source_code,
|
||||
abi: row.abi,
|
||||
};
|
||||
return smartContract;
|
||||
});
|
||||
return { found: true, result: smartContracts };
|
||||
});
|
||||
}
|
||||
|
||||
async getStxBalance({
|
||||
stxAddress,
|
||||
|
||||
@@ -792,7 +792,7 @@ export class TokensProcessorQueue {
|
||||
);
|
||||
this.queuedEntries.set(queueEntry.queueId, queueEntry);
|
||||
|
||||
const contractAbi: ClarityAbi = JSON.parse(contractQuery.result.abi);
|
||||
const contractAbi: ClarityAbi = JSON.parse(JSON.stringify(contractQuery.result.abi));
|
||||
const tokenContractHandler = new TokensContractHandler({
|
||||
contractId: queueEntry.contractId,
|
||||
smartContractAbi: contractAbi,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MigrationBuilder, ColumnDefinitions } from 'node-pg-migrate';
|
||||
import { MigrationBuilder } from 'node-pg-migrate';
|
||||
|
||||
export async function up(pgm: MigrationBuilder): Promise<void> {
|
||||
pgm.createTable('smart_contracts', {
|
||||
@@ -47,7 +47,7 @@ export async function up(pgm: MigrationBuilder): Promise<void> {
|
||||
notNull: true,
|
||||
},
|
||||
abi: {
|
||||
type: 'string',
|
||||
type: 'jsonb',
|
||||
notNull: true,
|
||||
},
|
||||
});
|
||||
@@ -61,12 +61,14 @@ export async function up(pgm: MigrationBuilder): Promise<void> {
|
||||
pgm.createIndex('smart_contracts', 'microblock_canonical');
|
||||
pgm.createIndex('smart_contracts', 'canonical');
|
||||
pgm.createIndex('smart_contracts', 'contract_id');
|
||||
|
||||
pgm.createIndex('smart_contracts', 'abi', { method: 'gin' });
|
||||
|
||||
pgm.createIndex('smart_contracts', [
|
||||
{ name: 'contract_id', sort: 'DESC' },
|
||||
{ name: 'canonical', sort: 'DESC' },
|
||||
{ name: 'microblock_canonical', sort: 'DESC' },
|
||||
{ name: 'block_height', sort: 'DESC' },
|
||||
{ name: 'abi', sort: 'DESC' }
|
||||
{ name: 'block_height', sort: 'DESC' }
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user