Skip to content

Commit

Permalink
Merge pull request #2178 from hirosystems/beta
Browse files Browse the repository at this point in the history
Cut release `v8.3.0`
  • Loading branch information
zone117x authored Dec 10, 2024
2 parents 52cec8a + 361c4d4 commit 1dd0328
Show file tree
Hide file tree
Showing 11 changed files with 294 additions and 11 deletions.
7 changes: 7 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,13 @@
"TS_NODE_SKIP_IGNORE": "true"
}
},
{
"type": "node",
"request": "launch",
"name": "docs: openapi-generator",
"runtimeArgs": ["-r", "ts-node/register/transpile-only"],
"args": ["${workspaceFolder}/src/openapi-generator.ts"]
},
{
"type": "node",
"request": "launch",
Expand Down
2 changes: 1 addition & 1 deletion client/src/socket-io/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export class StacksApiSocketClient {

handleSubscription(topic: Topic, subscribe = false, listener?: (...args: any[]) => void) {
const subsQuery = this.socket.io.opts.query?.subscriptions as string | undefined;
const subscriptions = new Set(subsQuery?.split(',') ?? []);
const subscriptions = new Set(subsQuery ? subsQuery.split(',') : []);
if (subscribe) {
this.socket.emit('subscribe', topic, error => {
if (error) console.error(`Error subscribing: ${error}`);
Expand Down
38 changes: 31 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@
"strict-event-emitter-types": "2.0.0",
"tiny-secp256k1": "2.2.1",
"ts-unused-exports": "7.0.3",
"undici": "6.21.0",
"uuid": "8.3.2",
"ws": "7.5.10",
"zone-file": "2.0.0-beta.3"
Expand Down
55 changes: 55 additions & 0 deletions src/api/routes/core-node-rpc-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,26 @@ function getReqUrl(req: { url: string; hostname: string }): URL {
return new URL(req.url, `http://${req.hostname}`);
}

// https://github.com/stacks-network/stacks-core/blob/20d5137438c7d169ea97dd2b6a4d51b8374a4751/stackslib/src/chainstate/stacks/db/blocks.rs#L338
const MINIMUM_TX_FEE_RATE_PER_BYTE = 1;

interface FeeEstimation {
fee: number;
fee_rate: number;
}
interface FeeEstimateResponse {
cost_scalar_change_by_byte: number;
estimated_cost: {
read_count: number;
read_length: number;
runtime: number;
write_count: number;
write_length: number;
};
estimated_cost_scalar: number;
estimations: [FeeEstimation, FeeEstimation, FeeEstimation];
}

export const CoreNodeRpcProxyRouter: FastifyPluginAsync<
Record<never, never>,
Server,
Expand Down Expand Up @@ -117,10 +137,22 @@ export const CoreNodeRpcProxyRouter: FastifyPluginAsync<
}
);

let feeEstimationModifier: number | null = null;
fastify.addHook('onReady', () => {
const feeEstEnvVar = process.env['STACKS_CORE_FEE_ESTIMATION_MODIFIER'];
if (feeEstEnvVar) {
const parsed = parseFloat(feeEstEnvVar);
if (!isNaN(parsed) && parsed > 0) {
feeEstimationModifier = parsed;
}
}
});

await fastify.register(fastifyHttpProxy, {
upstream: `http://${stacksNodeRpcEndpoint}`,
rewritePrefix: '/v2',
http2: false,
globalAgent: true,
preValidation: async (req, reply) => {
if (getReqUrl(req).pathname !== '/v2/transactions') {
return;
Expand Down Expand Up @@ -201,6 +233,29 @@ export const CoreNodeRpcProxyRouter: FastifyPluginAsync<
const txId = responseBuffer.toString();
await logTxBroadcast(txId);
await reply.send(responseBuffer);
} else if (
getReqUrl(req).pathname === '/v2/fees/transaction' &&
reply.statusCode === 200 &&
feeEstimationModifier !== null
) {
const reqBody = req.body as {
estimated_len?: number;
transaction_payload: string;
};
// https://github.com/stacks-network/stacks-core/blob/20d5137438c7d169ea97dd2b6a4d51b8374a4751/stackslib/src/net/api/postfeerate.rs#L200-L201
const txSize = Math.max(
reqBody.estimated_len ?? 0,
reqBody.transaction_payload.length / 2
);
const minFee = txSize * MINIMUM_TX_FEE_RATE_PER_BYTE;
const modifier = feeEstimationModifier;
const responseBuffer = await readRequestBody(response as ServerResponse);
const responseJson = JSON.parse(responseBuffer.toString()) as FeeEstimateResponse;
responseJson.estimations.forEach(estimation => {
// max(min fee, estimate returned by node * configurable modifier)
estimation.fee = Math.max(minFee, Math.round(estimation.fee * modifier));
});
await reply.removeHeader('content-length').send(JSON.stringify(responseJson));
} else {
await reply.send(response);
}
Expand Down
5 changes: 4 additions & 1 deletion src/api/routes/ws/channels/socket-io-channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,10 @@ export class SocketIOChannel extends WebSocketChannel {
io.use((socket, next) => {
const subscriptions = socket.handshake.query['subscriptions'];
if (subscriptions) {
const topics = [...[subscriptions]].flat().flatMap(r => r.split(','));
const topics = [...[subscriptions]]
.flat()
.flatMap(r => r.split(','))
.filter(r => !!r);
const invalidSubs = this.getInvalidSubscriptionTopics(topics as Topic[]);
if (invalidSubs) {
const error = new Error(`Invalid topic: ${invalidSubs.join(', ')}`);
Expand Down
3 changes: 2 additions & 1 deletion src/api/schemas/responses/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ import { NakamotoBlockSchema, SignerSignatureSchema } from '../entities/block';
export const ErrorResponseSchema = Type.Object(
{
error: Type.String(),
message: Type.Optional(Type.String()),
},
{ title: 'Error Response' }
{ title: 'Error Response', additionalProperties: true }
);

export const ServerStatusResponseSchema = Type.Object(
Expand Down
34 changes: 34 additions & 0 deletions src/event-stream/event-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -757,17 +757,51 @@ export async function startEventServer(opts: {
}

const bodyLimit = 1_000_000 * 500; // 500MB body limit

const reqLogSerializer = (req: FastifyRequest) => ({
method: req.method,
url: req.url,
version: req.headers?.['accept-version'] as string,
hostname: req.hostname,
remoteAddress: req.ip,
remotePort: req.socket?.remotePort,
bodySize: parseInt(req.headers?.['content-length'] as string) || 'unknown',
});

const loggerOpts: FastifyServerOptions['logger'] = {
...PINO_LOGGER_CONFIG,
name: 'stacks-node-event',
serializers: {
req: reqLogSerializer,
res: reply => ({
statusCode: reply.statusCode,
method: reply.request?.method,
url: reply.request?.url,
requestBodySize: parseInt(reply.request?.headers['content-length'] as string) || 'unknown',
responseBodySize: parseInt(reply.getHeader?.('content-length') as string) || 'unknown',
}),
},
};

const app = Fastify({
bodyLimit,
trustProxy: true,
logger: loggerOpts,
ignoreTrailingSlash: true,
});

app.addHook('onRequest', (req, reply, done) => {
req.raw.on('close', () => {
if (req.raw.aborted) {
req.log.warn(
reqLogSerializer(req),
`Request was aborted by the client: ${req.method} ${req.url}`
);
}
});
done();
});

const handleRawEventRequest = async (req: FastifyRequest) => {
await messageHandler.handleRawEventRequest(req.url, req.body, db);

Expand Down
13 changes: 12 additions & 1 deletion src/openapi-generator.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import Fastify from 'fastify';
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
import { TSchema, TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
import FastifySwagger from '@fastify/swagger';
import { writeFileSync } from 'fs';
import { OpenApiSchemaOptions } from './api/schemas/openapi';
import { StacksApiRoutes } from './api/init';
import { ErrorResponseSchema } from './api/schemas/responses/responses';

/**
* Generates `openapi.yaml` based on current Swagger definitions.
Expand All @@ -14,6 +15,16 @@ async function generateOpenApiFiles() {
logger: true,
}).withTypeProvider<TypeBoxTypeProvider>();

// If a response schema is defined but lacks a '4xx' response, add it
fastify.addHook(
'onRoute',
(route: { schema?: { response: Record<string | number, TSchema> } }) => {
if (route.schema?.response && !route.schema?.response['4xx']) {
route.schema.response['4xx'] = ErrorResponseSchema;
}
}
);

await fastify.register(FastifySwagger, OpenApiSchemaOptions);
await fastify.register(StacksApiRoutes);
await fastify.ready();
Expand Down
57 changes: 57 additions & 0 deletions tests/api/socket-io.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
NftEvent,
Transaction,
} from 'client/src/types';
import { Socket } from 'node:net';

describe('socket-io', () => {
let apiServer: ApiServer;
Expand All @@ -40,6 +41,62 @@ describe('socket-io', () => {
await migrate('down');
});

test('socket-io-client > reconnect', async () => {
const serverSocketConnectWaiter = waiter<Socket>();
apiServer.server.once('upgrade', (_req, socket: Socket) => {
serverSocketConnectWaiter.finish(socket);
});

const client = new StacksApiSocketClient({
url: `http://${apiServer.address}`,
// socketOpts: { reconnection: false },
});

const updateWaiter: Waiter<Block> = waiter();
const subResult = client.subscribeBlocks(block => updateWaiter.finish(block));

// subscriptions should be saved in the client query obj
expect(client.socket.io.opts.query).toMatchObject({ subscriptions: 'block' });

// wait for initial client connection
await new Promise<void>(resolve => client.socket.once('connect', resolve));

const connectAttempt = waiter();
client.socket.io.once('reconnect_attempt', attempt => {
// subscriptions should be saved in the client query obj
expect(client.socket.io.opts.query).toMatchObject({ subscriptions: 'block' });
connectAttempt.finish();
});

const reconnectWaiter = waiter();
client.socket.io.once('reconnect', () => reconnectWaiter.finish());

// force kill client connection on the server to trigger reconnect
const serverSocket = await serverSocketConnectWaiter;
serverSocket.resetAndDestroy();

await connectAttempt;
await reconnectWaiter;

// ensure client still waiting for block update
expect(updateWaiter.isFinished).toBe(false);

const block = new TestBlockBuilder({ block_hash: '0x1234', burn_block_hash: '0x5454' })
.addTx({ tx_id: '0x4321' })
.build();
await db.update(block);

const result = await updateWaiter;
try {
expect(result.hash).toEqual('0x1234');
expect(result.burn_block_hash).toEqual('0x5454');
expect(result.txs[0]).toEqual('0x4321');
} finally {
subResult.unsubscribe();
client.socket.close();
}
});

test('socket-io-client > block updates', async () => {
const client = new StacksApiSocketClient({
url: `http://${apiServer.address}`,
Expand Down
Loading

0 comments on commit 1dd0328

Please sign in to comment.