Skip to content
This repository was archived by the owner on Dec 10, 2025. It is now read-only.

Commit 97f12ff

Browse files
authored
Add ws api (#8)
* add simple ws server * feat: add websocket sub snapshot * add simple emitter * add simple block emitter * refactor frontend to use websocket * add tx type * render tx type * frontend api network switching * build: ci tag with commid id
1 parent e359919 commit 97f12ff

40 files changed

+2415
-1166
lines changed

.env.example

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@ TESTNET_WS_RPC_URL = "wss://testnet.ckb.dev/ws"
77
MAINNET_HTTP_RPC_URL = "https://mainnet.ckb.dev"
88
TESTNET_HTTP_RPC_URL = "https://testnet.ckb.dev"
99

10-
API_HTTP_PORT = 3000
11-
API_WS_PORT = 3001
10+
API_TESTNET_PORT = 3000
11+
API_MAINNET_PORT = 3001
1212
ALLOW_ORIGIN = "*" # for multiple allow origins, separate with , eg: "domain1.com,domain2.com"

.github/workflows/pkg.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ jobs:
1818
steps:
1919
- name: Checkout repository
2020
uses: actions/checkout@v4
21+
with:
22+
fetch-depth: 0 # get full history to create short-commit-ids
2123

2224
- name: Log in to the Container registry
2325
uses: docker/login-action@v2
@@ -31,6 +33,15 @@ jobs:
3133
uses: docker/metadata-action@v4
3234
with:
3335
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
36+
tags: |
37+
${{ github.ref == 'refs/heads/master' && 'latest' || '' }}
38+
${{ github.ref_type == 'tag' && github.ref_name || format('{0}-{1}', github.head_ref || github.ref_name, github.sha) }}
39+
labels: |
40+
org.label-schema.schema-version=1.0
41+
org.label-schema.vcs-url=${{ github.server_url }}/${{ github.repository }}
42+
org.label-schema.vcs-ref=${{ github.sha }}
43+
org.label-schema.build-date=${{ github.event.push.head_commit.timestamp }}
44+
3445
3546
- name: Build and push Docker image
3647
uses: docker/build-push-action@v4

frontend/.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
VITE_TESTNET_HTTP_API_URL = http://localhost:3000
2+
VITE_TESTNET_WS_API_URL = ws://localhost:3000
3+
VITE_MAINNET_HTTP_API_URL = http://localhost:3001
4+
VITE_MAINNET_WS_API_URL = ws://localhost:3001

frontend/src/components/elevator/car.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import Matter from "matter-js";
22
import { useEffect, useRef, useState } from "preact/hooks";
3-
import { BlockHeader, Transaction } from "../../service/chain";
3+
import {
4+
BlockHeader,
5+
Transaction,
6+
TransactionType,
7+
TransactionTypeEnum,
8+
} from "../../service/type";
49
import {
510
boxSizeToMatterSize,
611
carBoxCenterPosX,
712
carBoxSize,
8-
randomFillStyleColor,
913
transactionSquareSize,
1014
} from "./util";
1115
import { useAtomValue } from "jotai";
@@ -32,6 +36,7 @@ const ElevatorCar: React.FC<ElevatorCarProp> = (props) => {
3236
setDoorClosing(false);
3337
createScene();
3438
}
39+
3540
function createScene() {
3641
let Engine = Matter.Engine;
3742
let Render = Matter.Render;
@@ -56,9 +61,13 @@ const ElevatorCar: React.FC<ElevatorCarProp> = (props) => {
5661
// create two boxes and a ground
5762
const txBoxes = transactions.map((tx, i) => {
5863
const size = transactionSquareSize(tx.size);
64+
const color =
65+
tx.type != null
66+
? TransactionType.toBgColor(+tx.type as TransactionTypeEnum)
67+
: "black";
5968
const box = Bodies.rectangle(carBoxCenterPosX, 80, size, size, {
6069
render: {
61-
fillStyle: randomFillStyleColor(),
70+
fillStyle: color,
6271
strokeStyle: "black",
6372
lineWidth: 3,
6473
},
@@ -137,7 +146,7 @@ const ElevatorCar: React.FC<ElevatorCarProp> = (props) => {
137146

138147
useEffect(() => {
139148
run();
140-
}, [transactions.length]);
149+
}, [blockHeader?.block_number]);
141150

142151
useEffect(() => {
143152
if (setFromDoorClosing) {

frontend/src/components/elevator/index.tsx

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,47 @@
11
import { useEffect, useState } from "preact/hooks";
2-
import { ChainService } from "../../service/chain";
2+
import { ChainService } from "../../service/api";
33
import ElevatorCar from "./car";
44
import ElevatorUpButton from "./up-btn";
55
import ElevatorPanel from "./panel";
66
import ElevatorHeader from "./header";
77
import { useAtomValue } from "jotai";
88
import { ChainTheme, chainThemeAtom } from "../../states/atoms";
9+
import { Network, TipBlockResponse } from "../../service/type";
910

1011
export default function Elevator() {
1112
const chainTheme = useAtomValue(chainThemeAtom);
12-
const [proposedTxs, setProposedTxs] = useState([]);
13-
const [committedTxs, setCommittedTxs] = useState([]);
14-
const [blockHeader, setBlockHeader] = useState(undefined);
15-
const [currentTipNumber, setCurrentTipNumber] = useState(undefined);
13+
const [tipBlock, setTipBlock] = useState<TipBlockResponse>(undefined);
1614
const [doorClosing, setDoorClosing] = useState(false);
1715

18-
// Update effect to fetch all data
16+
// subscribe to new block
17+
// todo: need unscribe when component unmount
18+
const subNewBlock = async () => {
19+
const network =
20+
chainTheme === ChainTheme.mainnet
21+
? Network.Mainnet
22+
: Network.Testnet;
23+
const chainService = new ChainService(network);
24+
chainService.wsClient.connect(() => {
25+
chainService.subscribeNewBlock((newBlock) => {
26+
if (newBlock.blockHeader) {
27+
setTipBlock((prev) => {
28+
if (
29+
prev == null ||
30+
prev?.blockHeader?.block_number <
31+
newBlock.blockHeader.block_number
32+
) {
33+
return newBlock || undefined;
34+
} else {
35+
return prev;
36+
}
37+
});
38+
}
39+
});
40+
});
41+
};
1942
useEffect(() => {
20-
const fetchData = async () => {
21-
const [tipBlockTxs] = await Promise.all([
22-
ChainService.getTipBlockTransactions(),
23-
]);
24-
setProposedTxs(tipBlockTxs.proposedTransactions);
25-
setCommittedTxs(tipBlockTxs.committedTransactions);
26-
setBlockHeader(tipBlockTxs.blockHeader);
27-
};
28-
const task = setInterval(fetchData, 3000);
29-
return () => clearInterval(task);
30-
}, []);
43+
subNewBlock();
44+
}, [chainTheme]);
3145

3246
const bgElevatorFrame =
3347
chainTheme === ChainTheme.mainnet
@@ -49,20 +63,20 @@ export default function Elevator() {
4963
className={`${bgElevatorFrame} flex flex-col justify-center w-min mx-auto rounded-lg border-[20px] ${borderBlack}`}
5064
>
5165
<ElevatorHeader
52-
blockNumber={+blockHeader?.block_number}
66+
blockNumber={+tipBlock?.blockHeader.block_number}
5367
doorClosing={doorClosing}
5468
/>
5569
<div className={"px-20"}>
5670
<ElevatorCar
57-
transactions={committedTxs}
58-
blockHeader={blockHeader}
71+
blockHeader={tipBlock?.blockHeader}
72+
transactions={tipBlock?.committedTransactions}
5973
setFromDoorClosing={setDoorClosing}
6074
/>
6175
</div>
6276
</div>
6377

6478
<ElevatorPanel
65-
transactionNumber={committedTxs.length}
79+
transactionNumber={tipBlock?.committedTransactions.length}
6680
sizeBytes={20}
6781
occupationPercentage={20}
6882
/>

frontend/src/components/ground.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const Ground: FunctionComponent<GroundProps> = ({}) => {
2323
{Object.values(TransactionTypeEnum)
2424
.filter((t) => typeof t === "number")
2525
.map((type) => {
26-
const bgColor = `${TransactionType.toBgColor(type)}`;
26+
const bgColor = `${TransactionType.toBgTailwindCSS(type)}`;
2727
return (
2828
<div
2929
class={`flex justify-start items-center align-middle gap-1`}

frontend/src/components/pool/index.tsx

Lines changed: 65 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import Matter from "matter-js";
21
import { FunctionalComponent } from "preact";
3-
import { useEffect, useRef, useState } from "preact/hooks";
4-
import { ChainService, Transaction } from "../../service/chain";
5-
import PoolScene from "./scene";
6-
import QueueComponent from "./motion";
2+
import { useEffect, useState } from "preact/hooks";
3+
import { ChainService } from "../../service/api";
4+
import QueueComponent from "./queue";
5+
import { Network, Transaction } from "../../service/type";
6+
import { useAtomValue } from "jotai";
7+
import { ChainTheme, chainThemeAtom } from "../../states/atoms";
78

89
type TxStatus = "pending" | "proposing" | "proposed" | "committed" | "none";
910

@@ -14,7 +15,7 @@ interface TxChange {
1415
}
1516

1617
const Pool: FunctionalComponent = () => {
17-
const [blockHeader, setBlockHeader] = useState(undefined);
18+
const chainTheme = useAtomValue(chainThemeAtom);
1819

1920
const [initProposedTxs, setInitProposedTxs] = useState<
2021
Transaction[] | null
@@ -43,65 +44,69 @@ const Pool: FunctionalComponent = () => {
4344

4445
const [stateChanges, setStateChanges] = useState<TxChange[]>([]); // 存储状态变化信息
4546

46-
const fetchData = async () => {
47-
const [tipBlockTxs, pendingTxs, proposingTxs, proposedTransactions] =
48-
await Promise.all([
49-
ChainService.getTipBlockTransactions(),
50-
ChainService.getPendingTransactions(),
51-
ChainService.getProposingTransactions(),
52-
ChainService.getProposedTransactions(),
53-
]);
54-
55-
if (initCommittedTxs == null) {
56-
setInitCommittedTxs(tipBlockTxs.committedTransactions);
57-
}
58-
if (initProposedTxs == null) {
59-
setInitProposedTxs(proposedTransactions);
60-
}
61-
if (initPendingTxs == null) {
62-
setInitPendingTxs(pendingTxs);
63-
}
64-
if (initProposingTxs == null) {
65-
setInitProposingTxs(proposingTxs);
66-
}
67-
68-
// 拿到所有tx
69-
const newTxs = {
70-
pending: pendingTxs,
71-
proposing: proposingTxs,
72-
proposed: proposedTransactions,
73-
committed: tipBlockTxs.committedTransactions,
74-
};
47+
const subNewSnapshot = async () => {
48+
const network =
49+
chainTheme === ChainTheme.mainnet
50+
? Network.Mainnet
51+
: Network.Testnet;
52+
const chainService = new ChainService(network);
53+
chainService.wsClient.connect(() => {
54+
chainService.subscribeNewSnapshot((newSnapshot) => {
55+
const {
56+
tipCommittedTransactions,
57+
pendingTransactions,
58+
proposingTransactions,
59+
proposedTransactions,
60+
} = newSnapshot;
61+
62+
if (initCommittedTxs == null) {
63+
setInitCommittedTxs(tipCommittedTransactions);
64+
}
65+
if (initProposedTxs == null) {
66+
setInitProposedTxs(proposedTransactions);
67+
}
68+
if (initPendingTxs == null) {
69+
setInitPendingTxs(pendingTransactions);
70+
}
71+
if (initProposingTxs == null) {
72+
setInitProposingTxs(proposingTransactions);
73+
}
74+
75+
// 拿到所有tx
76+
const newTxs = {
77+
pending: pendingTransactions,
78+
proposing: proposingTransactions,
79+
proposed: proposedTransactions,
80+
committed: tipCommittedTransactions,
81+
};
82+
83+
// diff 数据
84+
if (previousTxs) {
85+
const changes = detectStateChanges(previousTxs, newTxs);
86+
setStateChanges(changes);
87+
}
88+
89+
// 更新历史记录
90+
// 使用函数式更新
91+
setPreviousTxs((prev) => {
92+
if (prev) {
93+
const changes = detectStateChanges(prev, newTxs);
94+
setStateChanges(changes);
95+
}
96+
return newTxs;
97+
});
7598

76-
// diff 数据
77-
if (previousTxs) {
78-
const changes = detectStateChanges(previousTxs, newTxs);
79-
setStateChanges(changes);
80-
}
81-
82-
// 更新历史记录
83-
// 使用函数式更新
84-
setPreviousTxs((prev) => {
85-
if (prev) {
86-
const changes = detectStateChanges(prev, newTxs);
87-
setStateChanges(changes);
88-
}
89-
return newTxs;
99+
setPendingTxs(pendingTransactions);
100+
setProposingTxs(proposingTransactions);
101+
setProposedTxs(proposedTransactions);
102+
setCommittedTxs(tipCommittedTransactions);
103+
});
90104
});
91-
92-
setPendingTxs(pendingTxs);
93-
94-
setProposingTxs(proposingTxs);
95-
setProposedTxs(proposedTransactions);
96-
97-
setCommittedTxs(tipBlockTxs.committedTransactions);
98-
setBlockHeader(tipBlockTxs.blockHeader);
99105
};
100106

101107
useEffect(() => {
102-
const task = setInterval(fetchData, 1000);
103-
return () => clearInterval(task);
104-
}, []);
108+
subNewSnapshot();
109+
}, [chainTheme]);
105110

106111
// 状态变化检测
107112
const detectStateChanges = (

0 commit comments

Comments
 (0)