Skip to content

Commit

Permalink
Merge pull request #19 from MystenLabs/SE-634-integrate-linter
Browse files Browse the repository at this point in the history
SE-634 integrate linter
  • Loading branch information
Tzal3x authored Jun 18, 2024
2 parents d3ea633 + b29e714 commit c7f1c9e
Show file tree
Hide file tree
Showing 15 changed files with 499 additions and 371 deletions.
21 changes: 21 additions & 0 deletions .github/workflows/check-format.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Check Code Formatting

on: [push, pull_request]

jobs:
prettier-check:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v2

- uses: oven-sh/setup-bun@v1
with:
bun-version: 1.0.11

- name: Install dependencies
run: bun install

- name: Run Prettier check
run: bun run check-format
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@ Then, to set up the cluster simply run:
> Tip: to quickly test your changes back to back, rebuild the services use `docker compose down && docker compose up -d --build --force-recreate`.
This will generate a network of the containers:

- `request_handler`: The web server (producer) that accepts requests (jobs) and saves them to the `queue` service.
You can access a [dashboard](https://github.com/felixmosh/bull-board) to monitor all the jobs on `localhost:3000`.
You can access a [dashboard](https://github.com/felixmosh/bull-board) to monitor all the jobs on `localhost:3000`.
- `queue`: A redis database that contains the queue of the requests (jobs) to be processed.
- `request_processor`: A worker that processes (consumes) the requests that have been queued up.
- `notifier`: A websocket server that exposes (publishes) the results of the jobs to clients.
You can open a websocket connection in your terminal with `websocat ws://localhost:3001`.
You can open a websocket connection in your terminal with `websocat ws://localhost:3001`.
- `structurizr`: This is a service enables you to explore the C4 diagram of our implementation with an interactive UI on `localhost:8080`.

It is also necessary to create a `request_processor/smart_contract_config.yaml` where for each function
Expand All @@ -42,14 +43,15 @@ e.g. assuming the smart contract has a function `mint_nft` that takes an `object

```yaml
smart_contract_functions:
- name: "mint_nft" #
- name: "mint_nft" #
types_of_arguments: ["object"]
- name: "modify_nft" #
- name: "modify_nft" #
types_of_arguments: ["pure", "object"]
```
So if you want to test the system (calling the `mint_nft`),
you can send a POST request to the `request_handler` service with the following curl command:

```bash
curl --request POST \
--url 'http://localhost:3000/?=' \
Expand Down
Binary file added bun.lockb
Binary file not shown.
636 changes: 348 additions & 288 deletions docs/workspace.json

Large diffs are not rendered by default.

46 changes: 24 additions & 22 deletions notifier/index.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
const topic = "job-events"
const topic = "job-events";
const server = Bun.serve({
port: 3001,
fetch(request, server){
if (server.upgrade(request)){
return;
}
return new Response("Established websocket connection with the notifier service.");
},
websocket:{
open(ws){
ws.subscribe(topic);
console.log("Notifier is up and running! Websocket connection is open.");
},
message(ws, message){
console.log(`Incoming message: ${message}`);
// TODO: only broadcast when receiving message from request processor
// ie. authenticate the request processor using a token
server.publish(topic, message);
},
close(_){
console.log("Connection to notifier service has been closed!");
}
port: 3001,
fetch(request, server) {
if (server.upgrade(request)) {
return;
}
return new Response(
"Established websocket connection with the notifier service.",
);
},
websocket: {
open(ws) {
ws.subscribe(topic);
console.log("Notifier is up and running! Websocket connection is open.");
},
message(ws, message) {
console.log(`Incoming message: ${message}`);
// TODO: only broadcast when receiving message from request processor
// ie. authenticate the request processor using a token
server.publish(topic, message);
},
close(_) {
console.log("Connection to notifier service has been closed!");
},
},
});

console.log(`Websocket server listening on ${server.port}`);
2 changes: 1 addition & 1 deletion notifier/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,

// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
Expand Down
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"dependencies": {},
"devDependencies": {
"prettier": "3.3.2"
},
"scripts": {
"fix-format": "bun prettier . --write",
"check-format": "bun prettier . --check"
}
}
6 changes: 4 additions & 2 deletions request_handler/buffer.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {QueueObject, enqueRequest} from "./queue";
import { QueueObject, enqueRequest } from "./queue";
import express from "express";

const maxBatchSize = parseInt(process.env.BUFFER_SIZE ?? "10");
const staleBufferTimeout = parseInt(process.env.STALE_BUFFER_TIMEOUT_MS ?? "10000");
const staleBufferTimeout = parseInt(
process.env.STALE_BUFFER_TIMEOUT_MS ?? "10000",
);
let staleBufferIntervalRunning = false;
let batchBuffer = new Array<QueueObject>();

Expand Down
2 changes: 1 addition & 1 deletion request_handler/queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@ export async function enqueRequest(queueObjects: Array<QueueObject>) {
type: "exponential",
delay: Number(process.env.JOB_BACKOFF_DELAY ?? 1000),
},
}
},
);
}
50 changes: 29 additions & 21 deletions request_handler/tests/endpoints.test.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,57 @@
import { expect, test } from "bun:test";
import { app } from "../app";
import request from "supertest";
import request from "supertest";

test("Healthcheck endpoint returns status 200", async () => {
await request(app)
.get("/healthcheck")
.then(response => {
.then((response) => {
expect(response.statusCode).toBe(200);
})
})
});
});

test("Accept request for processing", async () => {
await request(app)
.post("/")
.send({
"smart_contract_function_name": "mint_nft",
"smart_contract_function_arguments": ["0x9320eaaf945570b1baf7607f98a9cf5585fdcb8ed09d46da93199fee16b48196"],
"receiver_address": "0xe40c8cf8b53822829b3a6dc9aea84b62653f60b771e9da4bd4e214cae851b87b"
smart_contract_function_name: "mint_nft",
smart_contract_function_arguments: [
"0x9320eaaf945570b1baf7607f98a9cf5585fdcb8ed09d46da93199fee16b48196",
],
receiver_address:
"0xe40c8cf8b53822829b3a6dc9aea84b62653f60b771e9da4bd4e214cae851b87b",
})
.then(response => {
.then((response) => {
expect(response.statusCode).toBe(202);
})
})
});
});

test("Accept request without receiver_address", async () => {
await request(app)
.post("/")
.send({
"smart_contract_function_name": "mint_nft",
"smart_contract_function_arguments": ["0x9320eaaf945570b1baf7607f98a9cf5585fdcb8ed09d46da93199fee16b48196"],
smart_contract_function_name: "mint_nft",
smart_contract_function_arguments: [
"0x9320eaaf945570b1baf7607f98a9cf5585fdcb8ed09d46da93199fee16b48196",
],
})
.then(response => {
.then((response) => {
expect(response.statusCode).toBe(202);
})
})
});
});

test("Reject when smart_contract_function_name is empty", async () => {
await request(app)
.post("/")
.send({
"smart_contract_function_name": "",
"smart_contract_function_arguments": ["0x9320eaaf945570b1baf7607f98a9cf5585fdcb8ed09d46da93199fee16b48196"],
"receiver_address": "0xe40c8cf8b53822829b3a6dc9aea84b62653f60b771e9da4bd4e214cae851b87b"
smart_contract_function_name: "",
smart_contract_function_arguments: [
"0x9320eaaf945570b1baf7607f98a9cf5585fdcb8ed09d46da93199fee16b48196",
],
receiver_address:
"0xe40c8cf8b53822829b3a6dc9aea84b62653f60b771e9da4bd4e214cae851b87b",
})
.then(response => {
.then((response) => {
expect(response.statusCode).toBe(400);
})
})
});
});
4 changes: 3 additions & 1 deletion request_processor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ const worker = new Worker(
async (job) => {
try {
job.updateProgress(10);
console.log(`Executing transactions in bulk: ${JSON.stringify(job.data)}`);
console.log(
`Executing transactions in bulk: ${JSON.stringify(job.data)}`,
);
const resp = await executeTransaction(job.data);
job.updateProgress(90);
if (resp.status === "failure") {
Expand Down
40 changes: 22 additions & 18 deletions request_processor/moveCalls/executeTransaction.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,50 @@
import { fromB64 } from "@mysten/sui/utils";
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
import { SuiClient, getFullnodeUrl } from "@mysten/sui/client";
import { aggregateMoveCallsIntoATransaction} from "./prepareTransaction";
import { aggregateMoveCallsIntoATransaction } from "./prepareTransaction";
import { ParallelTransactionExecutor } from "@mysten/sui/transactions";
import { QueueObject } from "../../request_handler/queue";
import { envVariables } from "../utils/config";

type SuiNetwork = 'mainnet' | 'testnet' | 'devnet';
type SuiNetwork = "mainnet" | "testnet" | "devnet";
const parseNetwork = (network: string | undefined): SuiNetwork => {
if (network === 'mainnet' || network === 'testnet' || network === 'devnet') {
if (network === "mainnet" || network === "testnet" || network === "devnet") {
return network;
} else {
console.warn(`Invalid network: ${network}, defaulting to testnet`);
return 'testnet';
return "testnet";
}
}
const suiClient = new SuiClient({url: getFullnodeUrl(
parseNetwork(process.env.SUI_NETWORK)
)});
};
const suiClient = new SuiClient({
url: getFullnodeUrl(parseNetwork(process.env.SUI_NETWORK)),
});

let adminPrivateKeyArray = Uint8Array.from(
Array.from(fromB64(envVariables.ADMIN_SECRET_KEY!))
Array.from(fromB64(envVariables.ADMIN_SECRET_KEY!)),
);

const adminKeypair = Ed25519Keypair.fromSecretKey(
adminPrivateKeyArray.slice(1)
adminPrivateKeyArray.slice(1),
);

const executor = new ParallelTransactionExecutor({
client: suiClient,
signer: adminKeypair,
coinBatchSize: parseInt(process.env.PTE_COIN_BATCH_SIZE ?? '20'),
initialCoinBalance: BigInt(process.env.PTE_INITIAL_COIN_BALANCE ?? 5_000_000_000),
minimumCoinBalance: BigInt(process.env.PTE_MINIMUM_COIN_BALANCE ?? 500_000_000),
coinBatchSize: parseInt(process.env.PTE_COIN_BATCH_SIZE ?? "20"),
initialCoinBalance: BigInt(
process.env.PTE_INITIAL_COIN_BALANCE ?? 5_000_000_000,
),
minimumCoinBalance: BigInt(
process.env.PTE_MINIMUM_COIN_BALANCE ?? 500_000_000,
),
// The maximum number of gas coins to keep in the gas pool,
// which also limits the maximum number of concurrent transactions
maxPoolSize: parseInt(process.env.PTE_MAX_POOL_SIZE ?? '10'),
})
maxPoolSize: parseInt(process.env.PTE_MAX_POOL_SIZE ?? "10"),
});

export async function executeTransaction(receivers: QueueObject[]) {
const transaction = await aggregateMoveCallsIntoATransaction(receivers)
const res = await executor.executeTransaction(transaction)
const transaction = await aggregateMoveCallsIntoATransaction(receivers);
const res = await executor.executeTransaction(transaction);

return {status: res.effects, digest: res.digest};
return { status: res.effects, digest: res.digest };
}
26 changes: 17 additions & 9 deletions request_processor/moveCalls/prepareTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,23 @@ const addMoveCall = async (queueObject: QueueObject, tx: Transaction) => {
const availableFunctionsInContract =
smartContractFunctionConfig.smartContractFunctions.map((x) => x.name);

const invalidMoveCall = !availableFunctionsInContract.includes(queueObject.smartContractFunctionName);
const invalidMoveCall = !availableFunctionsInContract.includes(
queueObject.smartContractFunctionName,
);
if (invalidMoveCall) {
throw new Error(`Function ${queueObject.smartContractFunctionArguments} not present in the smart contract. Available functions: ${availableFunctionsInContract}`);
throw new Error(
`Function ${queueObject.smartContractFunctionArguments} not present in the smart contract. Available functions: ${availableFunctionsInContract}`,
);
}

const functionArgumentsTypes = smartContractFunctionConfig
.smartContractFunctions
.filter(f => f.name == queueObject.smartContractFunctionName)
.map(f => f.typesOfArguments)
const functionArgumentsTypes =
smartContractFunctionConfig.smartContractFunctions
.filter((f) => f.name == queueObject.smartContractFunctionName)
.map((f) => f.typesOfArguments);

let suiObject;
const noFunctionArgumentsDeclaredinContract = functionArgumentsTypes.length == 0;
const noFunctionArgumentsDeclaredinContract =
functionArgumentsTypes.length == 0;
if (noFunctionArgumentsDeclaredinContract) {
suiObject = tx.moveCall({
target: `${envVariables.PACKAGE_ADDRESS!}::${envVariables.MODULE_NAME}::${queueObject.smartContractFunctionName}`,
Expand All @@ -54,14 +59,17 @@ const addMoveCall = async (queueObject: QueueObject, tx: Transaction) => {
return tx.pure(argument as any);
}
}
}
},
),
});
}

// Transfer the sui object to the receiver address if it is present.
if (suiObject && queueObject.receiverAddress) {
tx.transferObjects([suiObject], tx.pure.address(queueObject.receiverAddress));
tx.transferObjects(
[suiObject],
tx.pure.address(queueObject.receiverAddress),
);
}

return tx;
Expand Down
10 changes: 9 additions & 1 deletion request_processor/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
{
"type": "module",
"dependencies": { "@mysten/sui": "^1.0.5", "@types/bun": "^1.1.4", "@types/js-yaml": "^4.0.9", "bullmq": "^5.7.11", "dotenv": "^16.4.5", "js-yaml": "^4.1.0", "yaml": "^2.4.5" },
"dependencies": {
"@mysten/sui": "^1.0.5",
"@types/bun": "^1.1.4",
"@types/js-yaml": "^4.0.9",
"bullmq": "^5.7.11",
"dotenv": "^16.4.5",
"js-yaml": "^4.1.0",
"yaml": "^2.4.5"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "bun index.ts",
Expand Down
7 changes: 4 additions & 3 deletions request_processor/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ export const envVariables = getEnvVariables();
export type SmartContractFunctionConfig = {
smartContractFunctions: {
name: string;
typesOfArguments: [string]
}[];
typesOfArguments: [string];
}[];
};

/*
Expand All @@ -49,4 +49,5 @@ async function getSmartContractFunctionsConfig(): Promise<SmartContractFunctionC
const contractSetup = YAML.parse(yaml_contents);
return contractSetup as SmartContractFunctionConfig;
}
export const smartContractFunctionConfig: SmartContractFunctionConfig = await getSmartContractFunctionsConfig();
export const smartContractFunctionConfig: SmartContractFunctionConfig =
await getSmartContractFunctionsConfig();

0 comments on commit c7f1c9e

Please sign in to comment.