Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
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
3 changes: 2 additions & 1 deletion packages/api-http/integration/routes/tokens.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,8 @@ describe<{
} else {
const { statusCode, data } = await request(endpoint, options);
assert.equal(statusCode, result.statusCode);
assert.equal(data.results, result.data);
assert.equal(data.data, result.data);
assert.equal(data.meta.totalCount, result.data.length);
}
}
});
Expand Down
2 changes: 1 addition & 1 deletion packages/api-http/source/controllers/commits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class CommitsController extends Controller {
// map bitmask -> indexes -> round.validators
const packed = BigInt(block.validatorSet);
const unpacked = validatorSetUnpack(packed, round.validators.length);
validators = unpacked.filter(Boolean).map((_, index) => round.validators[index]);
validators = round.validators.filter((_, index) => unpacked[index]);
}

return {
Expand Down
37 changes: 31 additions & 6 deletions packages/api-http/source/controllers/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,15 @@ export class NodeController extends Controller {

public async fees(request: Types.HapiRequest): Promise<object> {
const configuration = await this.getConfiguration();
const cryptoConfiguration = configuration.cryptoConfiguration as Contracts.Crypto.NetworkConfig;
const genesisTimestamp = cryptoConfiguration.genesisBlock.block.timestamp;
const cryptoConfiguration = configuration.cryptoConfiguration as Contracts.Crypto.NetworkConfig | undefined;

const result = await this.transactionRepositoryFactory().getFeeStatistics(genesisTimestamp, request.query.days);
// A fresh database has no configuration row (and no transactions yet).
const result = cryptoConfiguration
? await this.transactionRepositoryFactory().getFeeStatistics(
cryptoConfiguration.genesisBlock.block.timestamp,
request.query.days,
)
: undefined;

const grouped = {
evmCall: {
Expand All @@ -73,9 +78,14 @@ export class NodeController extends Controller {

public async configuration(request: Types.HapiRequest): Promise<object> {
const configuration = await this.getConfiguration();
const plugins = await this.getPlugins();

const cryptoConfiguration = configuration.cryptoConfiguration as Contracts.Crypto.NetworkConfig;
const cryptoConfiguration = configuration.cryptoConfiguration as Contracts.Crypto.NetworkConfig | undefined;
if (!cryptoConfiguration) {
// A fresh database has no configuration row yet.
return { data: {} };
}

const plugins = await this.getPlugins();
const network = cryptoConfiguration.network;

return {
Expand Down Expand Up @@ -128,7 +138,22 @@ export class NodeController extends Controller {
const pluginRepository = this.pluginRepositoryFactory();

let plugins = await pluginRepository.createQueryBuilder().select().getMany();
plugins = [{ configuration: this.apiConfiguration.all(), name: "@mainsail/api-http" }, ...plugins];

// Report this API's own port: lift enabled/port out of the server section
// (http preferred, https otherwise) into the shape buildPortMapping expects.
// Appended last so the live configuration wins over the raw configuration
// row that api-sync stores for this package, which carries no top-level port.
const { server } = this.apiConfiguration.all() as {
server: { http: { enabled: boolean; port: number }; https: { enabled: boolean; port: number } };
};
const activeServer = server.http.enabled ? server.http : server.https;
plugins = [
...plugins,
{
configuration: { enabled: activeServer.enabled, port: activeServer.port },
name: "@mainsail/api-http",
} as Models.Plugin,
];

const mappings: Record<string, Models.Plugin> = {};

Expand Down
2 changes: 2 additions & 0 deletions packages/api-http/source/controllers/receipts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,11 @@ export class ReceiptsController extends Controller {
#getReceiptColumns(fullReceipt?: boolean): string[] {
let columns = [
"Transaction.hash",
"Transaction.blockNumber",
"Transaction.status",
"Transaction.gasUsed",
"Transaction.gasRefunded",
"Transaction.cumulativeGasUsed",
"Transaction.deployedContractAddress",
"Transaction.decodedError",
];
Expand Down
4 changes: 4 additions & 0 deletions packages/api-http/source/controllers/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,16 @@ export class TokensController extends Controller {
return Boom.notFound("Token not found");
}

const pagination = this.getQueryPagination(request.query);

const [tokenHolders, totalCount] = await this.tokenHolderRepositoryFactory()
.createQueryBuilder()
.select()
.where("token_address = :address", { address: request.params.address })
.orderBy("balance", "DESC")
.addOrderBy("address", "ASC")
.offset(pagination.offset)
.limit(pagination.limit)
.getManyAndCount();

return this.toPagination(
Expand Down
129 changes: 129 additions & 0 deletions packages/api-http/source/defaults.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { describe } from "@mainsail/test-runner";

import { defaults } from "./defaults.js";

const ENV_KEYS = [
"MAINSAIL_API_NO_ESTIMATED_TOTAL_COUNT",
"MAINSAIL_API_CACHE",
"MAINSAIL_API_LOG",
"MAINSAIL_API_RATE_LIMIT_BLACKLIST",
"MAINSAIL_API_RATE_LIMIT_USER_EXPIRES",
"MAINSAIL_API_RATE_LIMIT_DISABLED",
"MAINSAIL_API_RATE_LIMIT_USER_LIMIT",
"MAINSAIL_API_RATE_LIMIT_WHITELIST",
"MAINSAIL_API_TRUST_PROXY",
"MAINSAIL_API_DISABLED",
"MAINSAIL_API_HOST",
"MAINSAIL_API_PORT",
"MAINSAIL_API_SSL",
"MAINSAIL_API_SSL_HOST",
"MAINSAIL_API_SSL_PORT",
"MAINSAIL_API_SSL_CERT",
"MAINSAIL_API_SSL_KEY",
"MAINSAIL_API_TOKENS_DEFAULT_MINIMUM_BALANCE",
];

// The module reads the environment at import time, so a fresh copy is imported
// with a cache-busting query once the environment has been prepared.
const importFresh = async (): Promise<typeof defaults> => {
const { defaults: freshDefaults } = await import(`./defaults.js?bust=${Math.random()}`);
return freshDefaults;
};

describe<{
previousEnv: Record<string, string | undefined>;
}>("defaults", ({ it, beforeEach, afterEach, assert }) => {
beforeEach((context) => {
context.previousEnv = {};
for (const key of ENV_KEYS) {
context.previousEnv[key] = process.env[key];
delete process.env[key];
}
});

afterEach(({ previousEnv }) => {
for (const key of ENV_KEYS) {
if (previousEnv[key] === undefined) {
delete process.env[key];
} else {
process.env[key] = previousEnv[key];
}
}
});

it("uses the documented defaults when no environment variables are set", async () => {
const fresh = await importFresh();

assert.true(fresh.options.estimateTotalCount);
assert.false(fresh.plugins.cache.enabled);
assert.false(fresh.plugins.log.enabled);
assert.equal(fresh.plugins.pagination.limit, 100);
assert.equal(fresh.plugins.rateLimit, {
blacklist: [],
duration: 60,
enabled: true,
points: 100,
whitelist: [],
});
assert.false(fresh.plugins.trustProxy);
assert.equal(fresh.plugins.whitelist, ["*"]);
assert.true(fresh.server.http.enabled);
assert.equal(fresh.server.http.host, "0.0.0.0");
assert.equal(fresh.server.http.port, 4003);
assert.false(fresh.server.https.enabled);
assert.equal(fresh.server.https.port, 8443);
assert.undefined(fresh.server.https.tls.cert);
assert.undefined(fresh.server.https.tls.key);
assert.equal(fresh.tokens.defaultMinimumBalance, 0.01);
});

it("honors the environment variable overrides", async () => {
process.env.MAINSAIL_API_NO_ESTIMATED_TOTAL_COUNT = "true";
process.env.MAINSAIL_API_CACHE = "true";
process.env.MAINSAIL_API_LOG = "1";
process.env.MAINSAIL_API_RATE_LIMIT_BLACKLIST = "1.1.1.1,2.2.2.2";
process.env.MAINSAIL_API_RATE_LIMIT_WHITELIST = "3.3.3.3";
process.env.MAINSAIL_API_RATE_LIMIT_DISABLED = "true";
process.env.MAINSAIL_API_TRUST_PROXY = "true";
process.env.MAINSAIL_API_DISABLED = "true";
process.env.MAINSAIL_API_HOST = "127.0.0.1";
process.env.MAINSAIL_API_SSL = "true";
process.env.MAINSAIL_API_SSL_CERT = "/tmp/cert.pem";
process.env.MAINSAIL_API_SSL_KEY = "/tmp/key.pem";

const fresh = await importFresh();

assert.false(fresh.options.estimateTotalCount);
assert.true(fresh.plugins.cache.enabled);
assert.true(fresh.plugins.log.enabled);
assert.equal(fresh.plugins.rateLimit.blacklist, ["1.1.1.1", "2.2.2.2"]);
assert.equal(fresh.plugins.rateLimit.whitelist, ["3.3.3.3"]);
assert.false(fresh.plugins.rateLimit.enabled);
assert.true(fresh.plugins.trustProxy);
assert.false(fresh.server.http.enabled);
assert.equal(fresh.server.http.host, "127.0.0.1");
assert.true(fresh.server.https.enabled);
assert.equal(fresh.server.https.tls.cert, "/tmp/cert.pem");
assert.equal(fresh.server.https.tls.key, "/tmp/key.pem");
});

it("passes numeric environment overrides through as strings", async () => {
process.env.MAINSAIL_API_PORT = "8080";
process.env.MAINSAIL_API_SSL_HOST = "10.0.0.1";
process.env.MAINSAIL_API_SSL_PORT = "9443";
process.env.MAINSAIL_API_RATE_LIMIT_USER_EXPIRES = "120";
process.env.MAINSAIL_API_RATE_LIMIT_USER_LIMIT = "50";
process.env.MAINSAIL_API_TOKENS_DEFAULT_MINIMUM_BALANCE = "0.5";

const fresh = await importFresh();

// Environment.get performs no numeric coercion: raw strings are passed
// through and only coerced later by the Joi configuration schema.
assert.is(fresh.server.http.port, "8080");
assert.is(fresh.server.https.host, "10.0.0.1");
assert.is(fresh.server.https.port, "9443");
assert.is(fresh.plugins.rateLimit.duration, "120");
assert.is(fresh.plugins.rateLimit.points, "50");
assert.is(fresh.tokens.defaultMinimumBalance, "0.5");
});
});
4 changes: 3 additions & 1 deletion packages/api-http/source/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import * as index from "./index";
import { describe } from "@mainsail/test-runner";

import { ServiceProvider } from "./service-provider";

describe("Index", ({ assert, it }) => {
it("should export ServiceProvider", () => {
assert.defined(index.ServiceProvider);
assert.is(index.ServiceProvider, ServiceProvider);
});
});
Loading
Loading