Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api-http): add /contracts endpoint with common contracts #781

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/api-database/source/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
ApiNode,
Block,
Configuration,
Contract,
Peer,
Plugin,
Receipt,
Expand Down Expand Up @@ -37,6 +38,9 @@ export type BlockRepository = ExtendedRepository<Block> & BlockRepositoryExtensi
export type ConfigurationRepositoryExtension = {};
export type ConfigurationRepository = ExtendedRepository<Configuration> & ConfigurationRepositoryExtension;

export type ContractRepositoryExtension = {};
export type ContractRepository = ExtendedRepository<Contract> & ContractRepositoryExtension;

export type ApiNodeRepositoryExtension = {
findManyByCriteria(
apiNodeCriteria: Criteria.OrApiNodeCriteria,
Expand Down Expand Up @@ -120,6 +124,7 @@ export type WalletRepository = ExtendedRepository<Wallet> & WalletRepositoryExte
export type ApiNodeRepositoryFactory = (customDataSource?: RepositoryDataSource) => ApiNodeRepository;
export type BlockRepositoryFactory = (customDataSource?: RepositoryDataSource) => BlockRepository;
export type ConfigurationRepositoryFactory = (customDataSource?: RepositoryDataSource) => ConfigurationRepository;
export type ContractRepositoryFactory = (customDataSource?: RepositoryDataSource) => ContractRepository;
export type PeerRepositoryFactory = (customDataSource?: RepositoryDataSource) => PeerRepository;
export type ReceiptRepositoryFactory = (customDataSource?: RepositoryDataSource) => ReceiptRepository;
export type TransactionRepositoryFactory = (customDataSource?: RepositoryDataSource) => TransactionRepository;
Expand Down
1 change: 1 addition & 0 deletions packages/api-database/source/identifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export const Identifiers = {
ApiNodeRepositoryFactory: Symbol.for("API<Factory.ApiNodeRepository>"),
BlockRepositoryFactory: Symbol.for("API<Factory.BlockRepository>"),
ConfigurationRepositoryFactory: Symbol.for("API<Factory.ConfigurationRepository>"),
ContractRepositoryFactory: Symbol.for("API<Factory.ContractRepository>"),
DataSource: Symbol.for("API<DataSource>"),
Migrations: Symbol.for("API<Migrations>"),
PeerRepositoryFactory: Symbol.for("API<Factory.PeerRepositoryFactory>"),
Expand Down
37 changes: 37 additions & 0 deletions packages/api-database/source/models/contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Column, Entity } from "typeorm";

@Entity({
name: "contracts",
})
export class Contract {
@Column({
primary: true,
type: "varchar",
})
public readonly name!: string;

@Column({
nullable: false,
type: "varchar",
})
public readonly address!: string;

@Column({
default: undefined,
nullable: true,
type: "varchar",
})
public readonly proxy!: string | undefined;

@Column({
nullable: false,
type: "varchar",
})
public readonly activeImplementation!: string;

@Column({
nullable: false,
type: "jsonb",
})
public readonly implementations!: { address: string; abi: Record<string, any> }[];
}
1 change: 1 addition & 0 deletions packages/api-database/source/models/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from "./api-node.js";
export * from "./block.js";
export * from "./configuration.js";
export * from "./contract.js";
export * from "./peer.js";
export * from "./plugin.js";
export * from "./receipt.js";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ContractRepository, ContractRepositoryExtension, RepositoryDataSource } from "../contracts.js";
import { Contract } from "../models/index.js";
import { makeExtendedRepository } from "./repository-extension.js";

export const makeContractRepository = (dataSource: RepositoryDataSource): ContractRepository =>
makeExtendedRepository<Contract, ContractRepositoryExtension>(Contract, dataSource, {
// Add any extensions here
});
1 change: 1 addition & 0 deletions packages/api-database/source/repositories/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { makeApiNodeRepository } from "./api-node-repository.js";
export { makeBlockRepository } from "./block-repository.js";
export { makeConfigurationRepository } from "./configuration-repository.js";
export { makeContractRepository } from "./contract-repository.js";
export { makePeerRepository } from "./peer-repository.js";
export { makePluginRepository } from "./plugin-repository.js";
export { makeReceiptRepository } from "./receipt-repository.js";
Expand Down
10 changes: 10 additions & 0 deletions packages/api-database/source/service-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
ApiNode,
Block,
Configuration,
Contract,
Peer,
Plugin,
Receipt,
Expand All @@ -22,6 +23,7 @@ import {
makeApiNodeRepository,
makeBlockRepository,
makeConfigurationRepository,
makeContractRepository,
makePeerRepository,
makePluginRepository,
makeReceiptRepository,
Expand Down Expand Up @@ -62,6 +64,7 @@ export class ServiceProvider extends Providers.ServiceProvider {
ApiNode,
Block,
Configuration,
Contract,
Peer,
Plugin,
Receipt,
Expand Down Expand Up @@ -106,6 +109,13 @@ export class ServiceProvider extends Providers.ServiceProvider {
makeConfigurationRepository(customDataSource ?? dataSource),
);

this.app
.bind(Identifiers.ContractRepositoryFactory)
.toFactory(
() => (customDataSource?: RepositoryDataSource) =>
makeContractRepository(customDataSource ?? dataSource),
);

this.app
.bind(Identifiers.PeerRepositoryFactory)
.toFactory(
Expand Down
54 changes: 54 additions & 0 deletions packages/api-http/integration/routes/contracts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { describe, Sandbox } from "../../../test-framework/source";
import contracts from "../../test/fixtures/contracts.json";
import contractsResponse from "../../test/fixtures/contracts-response.json";
import { ApiContext, prepareSandbox } from "../../test/helpers/prepare-sandbox";
import { request } from "../../test/helpers/request";

describe<{
sandbox: Sandbox;
}>("ApiNodes", ({ it, afterAll, assert, afterEach, beforeAll, beforeEach, nock }) => {
let apiContext: ApiContext;

const options = {};

beforeAll(async (context) => {
nock.enableNetConnect();
apiContext = await prepareSandbox(context);
});

afterAll((context) => {
nock.disableNetConnect();
apiContext.dispose();
});

beforeEach(async (context) => {
await apiContext.reset();
});

afterEach(async (context) => {
await apiContext.reset();
});

it("/contracts", async () => {
let { statusCode, data } = await request("/contracts", options);
assert.equal(statusCode, 200);
assert.empty(data.data);

await apiContext.contractRepository.save(contracts);

({ statusCode, data } = await request("/contracts", options));
assert.equal(data.data, contractsResponse);
});

it("/contracts/{name}/{implementation}/abi", async () => {
await apiContext.contractRepository.save(contracts);

let contract = contracts[contracts.length - 1];
let { data } = await request(`/contracts/${contract.name}/${contract.activeImplementation}/abi`, options);
assert.equal(data.data, { abi: contract.implementations[0].abi });

contract = contracts[0];
({ data } = await request(`/contracts/${contract.name}/${contract.implementations[1].address}/abi`, options));
assert.equal(data.data, { abi: contract.implementations[1].abi });
});
});
55 changes: 55 additions & 0 deletions packages/api-http/source/controllers/contracts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import Boom from "@hapi/boom";
import Hapi from "@hapi/hapi";
import { Contracts as ApiDatabaseContracts, Identifiers as ApiDatabaseIdentifiers } from "@mainsail/api-database";
import { inject, injectable } from "@mainsail/container";

import { Controller } from "./controller.js";

@injectable()
export class ContractsController extends Controller {
@inject(ApiDatabaseIdentifiers.ContractRepositoryFactory)
private readonly contractRepositoryFactory!: ApiDatabaseContracts.ContractRepositoryFactory;

public async index(request: Hapi.Request) {
const contractRepository = this.contractRepositoryFactory();

const contracts = await contractRepository.createQueryBuilder().orderBy("name").addOrderBy("address").getMany();

return {
data: contracts.reduce((accumulator, previous) => {
accumulator[previous.name] = {
activeImplementation: previous.activeImplementation,
address: previous.address,
implementations: previous.implementations.map((impl) => impl.address),
proxy: previous.proxy,
};
return accumulator;
}, {}),
};
}

public async abi(request: Hapi.Request) {
const contractRepository = this.contractRepositoryFactory();

const contract = await contractRepository
.createQueryBuilder()
.select()
.where("name = :name", { name: request.params.name })
.getOne();

if (!contract) {
return Boom.notFound("contract not found");
}

const implementation = contract.implementations.find((impl) => impl.address === request.params.implementation);
if (!implementation) {
return Boom.notFound("abi not found");
}

return {
data: {
abi: implementation.abi,
},
};
}
}
2 changes: 2 additions & 0 deletions packages/api-http/source/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as ApiNodes from "./routes/api-nodes.js";
import * as Blockchain from "./routes/blockchain.js";
import * as Blocks from "./routes/blocks.js";
import * as Commits from "./routes/commits.js";
import * as DeployedContracts from "./routes/contracts.js";
import * as Delegates from "./routes/delegates.js";
import * as Node from "./routes/node.js";
import * as Peers from "./routes/peers.js";
Expand All @@ -21,6 +22,7 @@ const config = {
Blocks,
Blockchain,
Commits,
DeployedContracts,
Delegates,
Peers,
Receipts,
Expand Down
31 changes: 31 additions & 0 deletions packages/api-http/source/routes/contracts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Hapi from "@hapi/hapi";
import { Contracts } from "@mainsail/contracts";
import Joi from "joi";

import { ContractsController } from "../controllers/contracts.js";
import { address } from "../schemas/schemas.js";

export const register = (server: Contracts.Api.ApiServer): void => {
const controller = server.app.app.resolve(ContractsController);
server.bind(controller);

server.route({
handler: (request: Hapi.Request) => controller.index(request),
method: "GET",
path: "/contracts",
});

server.route({
handler: (request: Hapi.Request) => controller.abi(request),
method: "GET",
options: {
validate: {
params: Joi.object({
implementation: address,
name: Joi.string().min(4).max(15),
}),
},
},
path: "/contracts/{name}/{implementation}/abi",
});
};
20 changes: 20 additions & 0 deletions packages/api-http/test/fixtures/contracts-response.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"consensus": {
"address": "0x535B3D7A252fa034Ed71F0C53ec0C6F784cB64E1",
"proxy": "UUPS",
"implementations": ["0x522B3294E6d06aA25Ad0f1B8891242E335D3B459", "0x622B3294E6d06aA25Ad0f1B8891242E335D3B459"],
"activeImplementation": "0x522B3294E6d06aA25Ad0f1B8891242E335D3B459"
},
"multi-payments": {
"address": "0x83769BeEB7e5405ef0B7dc3C66C43E3a51A6d27f",
"proxy": null,
"implementations": ["0x83769BeEB7e5405ef0B7dc3C66C43E3a51A6d27f"],
"activeImplementation": "0x83769BeEB7e5405ef0B7dc3C66C43E3a51A6d27f"
},
"usernames": {
"address": "0x2c1DE3b4Dbb4aDebEbB5dcECAe825bE2a9fc6eb6",
"proxy": "UUPS",
"implementations": ["0xc051134F56d56160E8c8ed9bB3c439c78AB27cCc"],
"activeImplementation": "0xc051134F56d56160E8c8ed9bB3c439c78AB27cCc"
}
}
50 changes: 50 additions & 0 deletions packages/api-http/test/fixtures/contracts.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
[
{
"name": "consensus",
"address": "0x535B3D7A252fa034Ed71F0C53ec0C6F784cB64E1",
"proxy": "UUPS",
"implementations": [
{
"address": "0x522B3294E6d06aA25Ad0f1B8891242E335D3B459",
"abi": {
"consensus": 1
}
},
{
"address": "0x622B3294E6d06aA25Ad0f1B8891242E335D3B459",
"abi": {
"consensus": 2
}
}
],
"activeImplementation": "0x522B3294E6d06aA25Ad0f1B8891242E335D3B459"
},
{
"name": "multi-payments",
"address": "0x83769BeEB7e5405ef0B7dc3C66C43E3a51A6d27f",
"proxy": null,
"implementations": [
{
"address": "0x83769BeEB7e5405ef0B7dc3C66C43E3a51A6d27f",
"abi": {
"multi-payments": 1
}
}
],
"activeImplementation": "0x83769BeEB7e5405ef0B7dc3C66C43E3a51A6d27f"
},
{
"name": "usernames",
"address": "0x2c1DE3b4Dbb4aDebEbB5dcECAe825bE2a9fc6eb6",
"proxy": "UUPS",
"implementations": [
{
"address": "0xc051134F56d56160E8c8ed9bB3c439c78AB27cCc",
"abi": {
"usernames": 1
}
}
],
"activeImplementation": "0xc051134F56d56160E8c8ed9bB3c439c78AB27cCc"
}
]
12 changes: 6 additions & 6 deletions packages/api-http/test/helpers/prepare-sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ export class ApiContext {
)();
}

public get contractRepository(): ApiDatabaseContracts.ContractRepository {
return this.app.get<ApiDatabaseContracts.ContractRepositoryFactory>(
ApiDatabaseIdentifiers.ContractRepositoryFactory,
)();
}

public get transactionRepository(): ApiDatabaseContracts.TransactionRepository {
return this.app.get<ApiDatabaseContracts.TransactionRepositoryFactory>(
ApiDatabaseIdentifiers.TransactionRepositoryFactory,
Expand All @@ -44,12 +50,6 @@ export class ApiContext {
)();
}

public get mempoolTransactionRepository(): ApiDatabaseContracts.MempoolTransactionRepository {
return this.app.get<ApiDatabaseContracts.MempoolTransactionRepositoryFactory>(
ApiDatabaseIdentifiers.MempoolTransactionRepositoryFactory,
)();
}

public get walletRepository(): ApiDatabaseContracts.WalletRepository {
return this.app.get<ApiDatabaseContracts.WalletRepositoryFactory>(
ApiDatabaseIdentifiers.WalletRepositoryFactory,
Expand Down
Loading
Loading