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:
Asim Mehmood
2021-12-06 09:43:04 +05:00
committed by GitHub
parent f9b4e72517
commit f18068c300
15 changed files with 1566 additions and 13 deletions

File diff suppressed because one or more lines are too long

View 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
}

File diff suppressed because one or more lines are too long

View 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
View File

@@ -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
*/

View File

@@ -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

View File

@@ -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
);

View File

@@ -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'`);
}

View File

@@ -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);

View File

@@ -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>;

View File

@@ -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.');
}
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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