Skip to content

Commit

Permalink
Merge pull request #20 from oasysgames/feature/routing
Browse files Browse the repository at this point in the history
Feature/routing
  • Loading branch information
Tsuyoshi-Ishikawa authored Mar 24, 2023
2 parents 4e9c53e + 8395c56 commit a1eea01
Show file tree
Hide file tree
Showing 15 changed files with 652 additions and 304 deletions.
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,10 @@ $ npm run test:cov
```

### Deploy
#### 1. Set PORT
#### 1. Set Environment Variables
```bash
export PORT=[YOUR_PROXY_PORT]
export VERSE_URL=[YOUR_VERSE_URL]
```

#### 2. Set allow list config
Expand Down Expand Up @@ -284,3 +285,19 @@ The default worker count is 1.
```bash
CLUSTER_PROCESS=4
```

## Master-Verse-Node and Read-Verse-node
You can create a verse and its replica to reduce the access load on the verse.
A verse can be set on the master-node and a replica on the read-node in a proxy.
It will send read-transactions to the read-node and write-transactions to the master-node.

You can set Master-Verse-Node and Read-Verse-node by the environment variable.
```bash
VERSE_MASTER_NODE_URL=[YOUR_VERSE_URL]
VERSE_READ_NODE_URL=[YOUR_VERSE_REPLICA_URL]
```

### Check Master-Verse-Node
To check the behavior of requests to the Master-Verse-Node, an endpoint named `/master` is provided.

All transactions sent to `/master` are sent to the Master-Verse-Node.
6 changes: 5 additions & 1 deletion src/config/configuration.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
export default () => ({
verseUrl: process.env.VERSE_URL ?? 'http://localhost:8545',
verseMasterNodeUrl:
process.env.VERSE_MASTER_NODE_URL ||
process.env.VERSE_URL ||
'http://localhost:8545',
verseReadNodeUrl: process.env.VERSE_READ_NODE_URL,
datastore: process.env.DATASTORE ?? '',
allowedMethods: [
/^net_version$/,
Expand Down
176 changes: 172 additions & 4 deletions src/controllers/__tests__/proxy.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigService } from '@nestjs/config';
import { ForbiddenException } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios';
import { Response } from 'express';
import {
Expand All @@ -16,6 +15,7 @@ import { DatastoreService } from 'src/repositories';

describe('ProxyController', () => {
let typeCheckService: TypeCheckService;
let configService: ConfigService;
let proxyService: ProxyService;
let moduleRef: TestingModule;

Expand All @@ -32,10 +32,15 @@ describe('ProxyController', () => {
ProxyService,
RateLimitService,
DatastoreService,
ConfigService,
],
})
.useMocker((token) => {
switch (token) {
case ConfigService:
return {
get: jest.fn(),
};
case TypeCheckService:
return {
isJsonrpcArray: jest.fn(),
Expand All @@ -50,6 +55,7 @@ describe('ProxyController', () => {
})
.compile();

configService = moduleRef.get<ConfigService>(ConfigService);
typeCheckService = moduleRef.get<TypeCheckService>(TypeCheckService);
proxyService = moduleRef.get<ProxyService>(ProxyService);
});
Expand All @@ -59,7 +65,160 @@ describe('ProxyController', () => {
jest.resetAllMocks();
});

it('verseReadNodeUrl is set', () => {
const verseReadNodeUrl = 'http://localhost:8545';
const headers = { host: 'localhost' };
const body = {
jsonrpc: '2.0',
method: 'net_version',
params: [],
id: 1,
};
const res = {
send: () => {
return;
},
status: (code: number) => res,
} as Response;

jest.spyOn(configService, 'get').mockImplementation((key: string) => {
switch (key) {
case 'verseReadNodeUrl':
return verseReadNodeUrl;
}
});

const controller = new ProxyController(
configService,
typeCheckService,
proxyService,
);
const handler = jest.spyOn(controller, 'handler');

expect(async () => controller.post(headers, body, res)).not.toThrow();
expect(handler).toHaveBeenCalledWith(true, headers, body, res);
});

it('verseReadNodeUrl is not set', () => {
const verseReadNodeUrl = undefined;
const headers = { host: 'localhost' };
const body = {
jsonrpc: '2.0',
method: 'net_version',
params: [],
id: 1,
};
const res = {
send: () => {
return;
},
status: (code: number) => res,
} as Response;

jest.spyOn(configService, 'get').mockImplementation((key: string) => {
switch (key) {
case 'verseReadNodeUrl':
return verseReadNodeUrl;
}
});

const controller = new ProxyController(
configService,
typeCheckService,
proxyService,
);
const handler = jest.spyOn(controller, 'handler');

expect(async () => controller.post(headers, body, res)).not.toThrow();
expect(handler).toHaveBeenCalledWith(false, headers, body, res);
});
});

describe('postMaster', () => {
beforeEach(() => {
jest.resetAllMocks();
});

it('verseReadNodeUrl is set', () => {
const verseReadNodeUrl = 'http://localhost:8545';
const headers = { host: 'localhost' };
const body = {
jsonrpc: '2.0',
method: 'net_version',
params: [],
id: 1,
};
const res = {
send: () => {
return;
},
status: (code: number) => res,
} as Response;

jest.spyOn(configService, 'get').mockImplementation((key: string) => {
switch (key) {
case 'verseReadNodeUrl':
return verseReadNodeUrl;
}
});

const controller = new ProxyController(
configService,
typeCheckService,
proxyService,
);
const handler = jest.spyOn(controller, 'handler');

expect(async () =>
controller.postMaster(headers, body, res),
).not.toThrow();
expect(handler).toHaveBeenCalledWith(false, headers, body, res);
});

it('verseReadNodeUrl is not set', () => {
const verseReadNodeUrl = undefined;
const headers = { host: 'localhost' };
const body = {
jsonrpc: '2.0',
method: 'net_version',
params: [],
id: 1,
};
const res = {
send: () => {
return;
},
status: (code: number) => res,
} as Response;

jest.spyOn(configService, 'get').mockImplementation((key: string) => {
switch (key) {
case 'verseReadNodeUrl':
return verseReadNodeUrl;
}
});

const controller = new ProxyController(
configService,
typeCheckService,
proxyService,
);
const handler = jest.spyOn(controller, 'handler');

expect(async () =>
controller.postMaster(headers, body, res),
).not.toThrow();
expect(handler).toHaveBeenCalledWith(false, headers, body, res);
});
});

describe('handler', () => {
beforeEach(() => {
jest.resetAllMocks();
});

it('body is JsonrpcArray', () => {
const isUseReadNode = true;
const headers = { host: 'localhost' };
const body = [
{
Expand Down Expand Up @@ -97,12 +256,15 @@ describe('ProxyController', () => {
);

const controller = moduleRef.get<ProxyController>(ProxyController);
expect(async () => controller.post(headers, body, res)).not.toThrow();
expect(async () =>
controller.handler(isUseReadNode, headers, body, res),
).not.toThrow();
expect(handleBatchRequestMock).toHaveBeenCalled();
expect(handleSingleRequestMock).not.toHaveBeenCalled();
});

it('body is Jsonrpc', () => {
const isUseReadNode = true;
const headers = { host: 'localhost' };
const body = {
jsonrpc: '2.0',
Expand Down Expand Up @@ -133,12 +295,15 @@ describe('ProxyController', () => {
);

const controller = moduleRef.get<ProxyController>(ProxyController);
expect(async () => controller.post(headers, body, res)).not.toThrow();
expect(async () =>
controller.handler(isUseReadNode, headers, body, res),
).not.toThrow();
expect(handleBatchRequestMock).not.toHaveBeenCalled();
expect(handleSingleRequestMock).toHaveBeenCalled();
});

it('body is not Jsonrpc or JsonrpcArray', async () => {
const isUseReadNode = true;
const headers = { host: 'localhost' };
const body = {};
const res = {
Expand All @@ -165,7 +330,10 @@ describe('ProxyController', () => {
);

const controller = moduleRef.get<ProxyController>(ProxyController);
await expect(controller.post(headers, body, res)).rejects.toThrow(errMsg);

await expect(
controller.handler(isUseReadNode, headers, body, res),
).rejects.toThrow(errMsg);
expect(handleBatchRequestMock).not.toHaveBeenCalled();
expect(handleSingleRequestMock).not.toHaveBeenCalled();
});
Expand Down
37 changes: 35 additions & 2 deletions src/controllers/proxy.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ForbiddenException,
Res,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { IncomingHttpHeaders } from 'http';
import { Response } from 'express';
import { ProxyService, TypeCheckService } from 'src/services';
Expand All @@ -14,6 +15,7 @@ import { VerseRequestResponse } from 'src/entities';
@Controller()
export class ProxyController {
constructor(
private configService: ConfigService,
private readonly typeCheckService: TypeCheckService,
private readonly proxyService: ProxyService,
) {}
Expand All @@ -23,15 +25,46 @@ export class ProxyController {
@Headers() headers: IncomingHttpHeaders,
@Body() body: any,
@Res() res: Response,
) {
const isUseReadNode = !!this.configService.get<string>('verseReadNodeUrl');
await this.handler(isUseReadNode, headers, body, res);
}

@Post('master')
async postMaster(
@Headers() headers: IncomingHttpHeaders,
@Body() body: any,
@Res() res: Response,
) {
const isUseReadNode = false;
await this.handler(isUseReadNode, headers, body, res);
}

async handler(
isUseReadNode: boolean,
headers: IncomingHttpHeaders,
body: any,
res: Response,
) {
const callback = (result: VerseRequestResponse) => {
const { status, data } = result;
res.status(status).send(data);
};

if (this.typeCheckService.isJsonrpcArrayRequestBody(body)) {
await this.proxyService.handleBatchRequest(headers, body, callback);
await this.proxyService.handleBatchRequest(
isUseReadNode,
headers,
body,
callback,
);
} else if (this.typeCheckService.isJsonrpcRequestBody(body)) {
await this.proxyService.handleSingleRequest(headers, body, callback);
await this.proxyService.handleSingleRequest(
isUseReadNode,
headers,
body,
callback,
);
} else {
throw new ForbiddenException(`invalid request`);
}
Expand Down
14 changes: 13 additions & 1 deletion src/entities/Jsonrpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,20 @@ export interface JsonrpcRequestBody {
params?: JsonrpcParams | null;
}

export interface JsonrpcTxResponse {
export interface JsonrpcTxSuccessResponse {
jsonrpc: JsonrpcVersion;
id: JsonrpcId;
result: string;
}

interface JsonrpcError {
code: number;
message: string;
data?: any;
}

export interface JsonrpcErrorResponse {
jsonrpc: JsonrpcVersion;
id: JsonrpcId;
error: JsonrpcError;
}
Loading

0 comments on commit a1eea01

Please sign in to comment.