@clonegod/ttd-bsc-send-tx 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -0
- package/dist/_48club/http.d.ts +9 -0
- package/dist/_48club/http.js +69 -0
- package/dist/_48club/index.d.ts +4 -0
- package/dist/_48club/index.js +11 -0
- package/dist/_48club/member.d.ts +1 -0
- package/dist/_48club/member.js +21 -0
- package/dist/_48club/sp_signature.d.ts +4 -0
- package/dist/_48club/sp_signature.js +27 -0
- package/dist/_48club/ws.d.ts +9 -0
- package/dist/_48club/ws.js +63 -0
- package/dist/blockrazor/index.d.ts +7 -0
- package/dist/blockrazor/index.js +67 -0
- package/dist/bloxroute/index.d.ts +1 -0
- package/dist/bloxroute/index.js +5 -0
- package/dist/bloxroute/ws.d.ts +10 -0
- package/dist/bloxroute/ws.js +65 -0
- package/dist/constants.d.ts +5 -0
- package/dist/constants.js +9 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7 -0
- package/dist/rpc/index.d.ts +6 -0
- package/dist/rpc/index.js +47 -0
- package/dist/transaction_sender.d.ts +9 -0
- package/dist/transaction_sender.js +116 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.js +2 -0
- package/dist/ws_client.d.ts +6 -0
- package/dist/ws_client.js +18 -0
- package/dist/ws_server.d.ts +1 -0
- package/dist/ws_server.js +93 -0
- package/package.json +29 -0
- package/scripts/deploy.sh +29 -0
- package/scripts/start-ttd-bsc-send-tx-ws.sh +24 -0
- package/src/_48club/http.ts +60 -0
- package/src/_48club/index.ts +4 -0
- package/src/_48club/member.ts +19 -0
- package/src/_48club/sp_signature.ts +28 -0
- package/src/_48club/ws.ts +47 -0
- package/src/blockrazor/index.ts +57 -0
- package/src/bloxroute/index.ts +1 -0
- package/src/bloxroute/ws.ts +53 -0
- package/src/constants.ts +5 -0
- package/src/index.ts +2 -0
- package/src/rpc/index.ts +42 -0
- package/src/transaction_sender.ts +123 -0
- package/src/types/index.ts +8 -0
- package/src/ws_client.ts +22 -0
- package/src/ws_server.ts +104 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BscSendTxProxy = void 0;
|
|
4
|
+
const ttd_core_1 = require("@clonegod/ttd-core");
|
|
5
|
+
class BscSendTxProxy {
|
|
6
|
+
constructor() {
|
|
7
|
+
const host = process.env.SEND_TX_WS_HOST || '127.0.0.1';
|
|
8
|
+
this.wsUrl = `ws://${host}:${ttd_core_1.SERVICE_PORT.SEND_TX_WS}/bsc/send_tx`;
|
|
9
|
+
this.ws = new ttd_core_1.WebSocketClient(this.wsUrl);
|
|
10
|
+
this.ws.onOpen(() => console.log('BscSendTxProxy connected:', this.wsUrl));
|
|
11
|
+
this.ws.onMessage((message) => console.log('BscSendTxProxy response:', message));
|
|
12
|
+
this.ws.connect();
|
|
13
|
+
}
|
|
14
|
+
sendTransaction(builder, pair, mainTx, tipTx, _48spSign = null) {
|
|
15
|
+
this.ws.send(JSON.stringify({ builder, pair, mainTx, tipTx, _48spSign }));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
exports.BscSendTxProxy = BscSendTxProxy;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
require('dotenv').config();
|
|
13
|
+
const ttd_core_1 = require("@clonegod/ttd-core");
|
|
14
|
+
const ws_1 = require("ws");
|
|
15
|
+
const _48club_1 = require("./_48club");
|
|
16
|
+
const bloxroute_1 = require("./bloxroute");
|
|
17
|
+
class BscSendTxWebSocketServer {
|
|
18
|
+
constructor(appConfig, serverPort = ttd_core_1.SERVICE_PORT.SEND_TX_WS) {
|
|
19
|
+
this.wss = null;
|
|
20
|
+
this.latestBlockNumber = 0;
|
|
21
|
+
this._48clubWsSender = null;
|
|
22
|
+
this.bloxrouteWsSender = null;
|
|
23
|
+
this.appConfig = appConfig;
|
|
24
|
+
this.serverPort = serverPort;
|
|
25
|
+
this.appConfig.arb_event_subscriber.subscribe_new_block(ttd_core_1.CHAIN_ID.BSC, (data) => {
|
|
26
|
+
try {
|
|
27
|
+
this.latestBlockNumber = JSON.parse(data).blockNumber;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
(0, ttd_core_1.log_error)('parse block update event failed', error);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
this.initSenders();
|
|
34
|
+
}
|
|
35
|
+
initSenders() {
|
|
36
|
+
if (process.env.SEND_TX_48CLUB_WS === 'true') {
|
|
37
|
+
this._48clubWsSender = new _48club_1._48ClubWsSender();
|
|
38
|
+
this._48clubWsSender.connect();
|
|
39
|
+
}
|
|
40
|
+
if (process.env.SEND_TX_BLOXROUTE_WS === 'true') {
|
|
41
|
+
this.bloxrouteWsSender = new bloxroute_1.BloXRouteWsSender();
|
|
42
|
+
this.bloxrouteWsSender.connect();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
handleMessage(ws, message) {
|
|
46
|
+
var _a, _b;
|
|
47
|
+
try {
|
|
48
|
+
const request = JSON.parse(message);
|
|
49
|
+
if (!request.mainTx || !request.builder || !request.tipTx) {
|
|
50
|
+
ws.send(JSON.stringify({ error: 'Missing required fields: mainTx, builder, tipTx' }));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
request.blockNumber = this.latestBlockNumber;
|
|
54
|
+
(0, ttd_core_1.log_info)(`[WS_SERVER] recv bundle request`, { builder: request.builder, pair: request.pair });
|
|
55
|
+
switch (request.builder) {
|
|
56
|
+
case '48club':
|
|
57
|
+
(_a = this._48clubWsSender) === null || _a === void 0 ? void 0 : _a.sendTransaction(request);
|
|
58
|
+
break;
|
|
59
|
+
case 'bloxroute':
|
|
60
|
+
(_b = this.bloxrouteWsSender) === null || _b === void 0 ? void 0 : _b.sendTransaction(request);
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
(0, ttd_core_1.log_error)(`[WS_SERVER] handle request failed`, error);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
start() {
|
|
69
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
70
|
+
if (this.wss)
|
|
71
|
+
return;
|
|
72
|
+
this.wss = new ws_1.WebSocketServer({ port: this.serverPort });
|
|
73
|
+
this.wss.on('connection', (ws) => {
|
|
74
|
+
(0, ttd_core_1.log_info)(`[WS_SERVER] client connected`);
|
|
75
|
+
ws.on('message', (msg) => this.handleMessage(ws, msg.toString()));
|
|
76
|
+
ws.on('close', () => (0, ttd_core_1.log_info)(`[WS_SERVER] client disconnected`));
|
|
77
|
+
ws.on('error', (err) => (0, ttd_core_1.log_error)(`[WS_SERVER] ws error`, err));
|
|
78
|
+
});
|
|
79
|
+
(0, ttd_core_1.log_info)(`[WS_SERVER] started on port ${this.serverPort}`);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
process.on('uncaughtException', (err) => { (0, ttd_core_1.log_error)(`[BSC_SEND_TX] uncaught exception`, err); process.exit(1); });
|
|
84
|
+
process.on('unhandledRejection', (reason) => { (0, ttd_core_1.log_error)(`[BSC_SEND_TX] unhandled rejection`, reason instanceof Error ? reason : new Error(String(reason))); process.exit(1); });
|
|
85
|
+
const main = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
86
|
+
const appConfig = new ttd_core_1.AppConfig();
|
|
87
|
+
yield appConfig.init();
|
|
88
|
+
yield appConfig.subscribe_config_change();
|
|
89
|
+
const wsPort = ttd_core_1.SERVICE_PORT.SEND_TX_WS;
|
|
90
|
+
const server = new BscSendTxWebSocketServer(appConfig, wsPort);
|
|
91
|
+
yield server.start();
|
|
92
|
+
});
|
|
93
|
+
main().catch((err) => { (0, ttd_core_1.log_error)(`[BSC_SEND_TX] startup failed`, err); process.exit(1); });
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@clonegod/ttd-bsc-send-tx",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "BSC 交易发送模块(HTTP直发 + WS bundle 转发)",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"clean": "rm -rf dist node_modules",
|
|
10
|
+
"build": "tsc --outDir ./dist",
|
|
11
|
+
"start": "node dist/ws_server.js",
|
|
12
|
+
"push": "npm run build && npm publish"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@clonegod/ttd-core": "3.0.8",
|
|
16
|
+
"axios": "^1.12.0",
|
|
17
|
+
"dotenv": "^16.4.7",
|
|
18
|
+
"ethers": "^5.8.0",
|
|
19
|
+
"ws": "^8.18.3"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^22.14.0",
|
|
23
|
+
"@types/ws": "^8.5.12",
|
|
24
|
+
"typescript": "^5.8.2"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
echo $PWD
|
|
2
|
+
|
|
3
|
+
module_name="ttd-bsc-send-tx"
|
|
4
|
+
|
|
5
|
+
# Delete Old File
|
|
6
|
+
rm -rf dist
|
|
7
|
+
rm -rf deploy
|
|
8
|
+
|
|
9
|
+
# Create deploy folder
|
|
10
|
+
date_time=`date +%Y%m%d-%H%M%S`
|
|
11
|
+
target_dir=deploy/${date_time}/${module_name}
|
|
12
|
+
mkdir -p ${target_dir}
|
|
13
|
+
|
|
14
|
+
# Build
|
|
15
|
+
npm run build
|
|
16
|
+
|
|
17
|
+
# Copy dist and package.json
|
|
18
|
+
cp -r dist ${target_dir}
|
|
19
|
+
cp package.json ${target_dir}
|
|
20
|
+
|
|
21
|
+
# Delete .d.ts file
|
|
22
|
+
find ${target_dir} -maxdepth 3 -name "*.d.ts" -type f -delete
|
|
23
|
+
|
|
24
|
+
# Show files
|
|
25
|
+
tree ${target_dir}
|
|
26
|
+
|
|
27
|
+
cat package.json | grep "ttd-core"
|
|
28
|
+
cat package.json | grep "ttd-bsc-common" || true
|
|
29
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
echo "args count: $#"
|
|
4
|
+
echo "$@"
|
|
5
|
+
echo
|
|
6
|
+
|
|
7
|
+
# args
|
|
8
|
+
app_id="ttd-bsc-send-tx-ws"
|
|
9
|
+
app_ns="BSC"
|
|
10
|
+
|
|
11
|
+
# sync code
|
|
12
|
+
git pull
|
|
13
|
+
yarn install
|
|
14
|
+
npm run build
|
|
15
|
+
|
|
16
|
+
# start app
|
|
17
|
+
echo "$app_id ($app_ns)"
|
|
18
|
+
|
|
19
|
+
pm2 del $app_id 2> /dev/null || true && pm2 start "dist/ws_server.js" --name $app_id --namespace $app_ns
|
|
20
|
+
|
|
21
|
+
echo
|
|
22
|
+
|
|
23
|
+
pm2 log $app_id
|
|
24
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { SoulPointSignature } from './sp_signature';
|
|
3
|
+
import { get_48club_sp_private_key } from './member';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 48Club HTTP API
|
|
7
|
+
* https://docs.48.club/puissant-builder
|
|
8
|
+
*/
|
|
9
|
+
export class _48ClubTrade {
|
|
10
|
+
private rpcUrl: string;
|
|
11
|
+
|
|
12
|
+
constructor() {
|
|
13
|
+
this.rpcUrl = process.env._48CLUB_RPC_URL || 'https://puissant-bsc.48.club';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async sendPrivateTransactionWith48SP(signedTx: string): Promise<string> {
|
|
17
|
+
const spSign = await this.get48SPSignature([signedTx]);
|
|
18
|
+
|
|
19
|
+
const response = await axios.post(this.rpcUrl, {
|
|
20
|
+
jsonrpc: "2.0", id: 1,
|
|
21
|
+
method: "eth_sendPrivateTransaction",
|
|
22
|
+
params: [{ tx: signedTx, preferences: { '48spSign': spSign } }]
|
|
23
|
+
}, {
|
|
24
|
+
headers: { 'Content-Type': 'application/json' }
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (response.data.error) {
|
|
28
|
+
throw new Error(`48club private tx: ${response.data.error.message}`);
|
|
29
|
+
}
|
|
30
|
+
return response.data.result;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async sendBundle(params: { txs: string[] }): Promise<string> {
|
|
34
|
+
const currentTimestamp = Math.floor(Date.now() / 1000);
|
|
35
|
+
const spSign = await this.get48SPSignature(params.txs);
|
|
36
|
+
|
|
37
|
+
const response = await axios.post(this.rpcUrl, {
|
|
38
|
+
jsonrpc: "2.0", id: 1,
|
|
39
|
+
method: "eth_sendBundle",
|
|
40
|
+
params: [{
|
|
41
|
+
txs: params.txs,
|
|
42
|
+
maxTimestamp: currentTimestamp + 2,
|
|
43
|
+
'48spSign': spSign
|
|
44
|
+
}]
|
|
45
|
+
}, {
|
|
46
|
+
headers: { 'Content-Type': 'application/json' }
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (response.data.error) {
|
|
50
|
+
throw new Error(`48club bundle: ${response.data.error.message}`);
|
|
51
|
+
}
|
|
52
|
+
return response.data.result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async get48SPSignature(txs: string[]): Promise<string> {
|
|
56
|
+
const privateKey = get_48club_sp_private_key();
|
|
57
|
+
if (!privateKey) return '';
|
|
58
|
+
return SoulPointSignature.generate48SPSignature(privateKey, txs);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { load_wallet } from '@clonegod/ttd-core';
|
|
2
|
+
|
|
3
|
+
var _48club_sp_wallet_private_key: string | null = null;
|
|
4
|
+
|
|
5
|
+
export const get_48club_sp_private_key = (): string | null => {
|
|
6
|
+
if (_48club_sp_wallet_private_key) {
|
|
7
|
+
return _48club_sp_wallet_private_key;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const wallet = load_wallet(process.env._48CLUB_SP_WALLET_ID || 'TTD-PAYMENT');
|
|
12
|
+
console.log('Load 48club SP wallet success, wallet address:', wallet.public_key);
|
|
13
|
+
_48club_sp_wallet_private_key = wallet.private_key;
|
|
14
|
+
return _48club_sp_wallet_private_key;
|
|
15
|
+
} catch (error: any) {
|
|
16
|
+
console.error('Load 48club SP wallet failed:' + error.message);
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ethers } from 'ethers';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 48 SoulPoint 签名
|
|
5
|
+
* https://docs.48.club/puissant-builder/48-soulpoint-benefits
|
|
6
|
+
*/
|
|
7
|
+
export class SoulPointSignature {
|
|
8
|
+
static generate48SPSignature(privateKey: string, txs: string[]): string {
|
|
9
|
+
const wallet = new ethers.Wallet(privateKey);
|
|
10
|
+
const txHashes = txs.map(tx => tx.startsWith('0x') ? ethers.utils.keccak256(tx) : tx);
|
|
11
|
+
const concatenatedHashes = ethers.utils.concat(txHashes);
|
|
12
|
+
const messageHash = ethers.utils.keccak256(concatenatedHashes);
|
|
13
|
+
const signature = wallet._signingKey().signDigest(messageHash);
|
|
14
|
+
return signature.compact;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static verify48SPSignature(signature: string, txs: string[], expectedSignerAddress: string): boolean {
|
|
18
|
+
try {
|
|
19
|
+
const txHashes = txs.map(tx => tx.startsWith('0x') ? ethers.utils.keccak256(tx) : tx);
|
|
20
|
+
const concatenatedHashes = ethers.utils.concat(txHashes);
|
|
21
|
+
const messageHash = ethers.utils.keccak256(concatenatedHashes);
|
|
22
|
+
const recoveredAddress = ethers.utils.recoverAddress(messageHash, signature);
|
|
23
|
+
return recoveredAddress.toLowerCase() === expectedSignerAddress.toLowerCase();
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { log_info, log_warn, WebSocketClient } from '@clonegod/ttd-core';
|
|
2
|
+
import { BscSendTxRequestType } from '../types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 48Club WebSocket 发送器(长连接发送 bundle 到 builder)
|
|
6
|
+
*/
|
|
7
|
+
export class _48ClubWsSender {
|
|
8
|
+
private wsClient: WebSocketClient | null = null;
|
|
9
|
+
private wsUrl: string;
|
|
10
|
+
|
|
11
|
+
constructor() {
|
|
12
|
+
this.wsUrl = process.env._48CLUB_WS_URL || 'wss://puissant-builder.48.club/';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
connect(): void {
|
|
16
|
+
if (this.wsClient?.isConnected()) return;
|
|
17
|
+
if (this.wsClient) { this.wsClient.disconnect(); this.wsClient = null; }
|
|
18
|
+
|
|
19
|
+
this.wsClient = new WebSocketClient(this.wsUrl, { reconnectInterval: 1000 });
|
|
20
|
+
this.wsClient.onOpen(() => log_info(`[48club-ws] connected`));
|
|
21
|
+
this.wsClient.onMessage((msg: any) => log_info(`[48club-ws] response:`, msg));
|
|
22
|
+
this.wsClient.connect();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
isConnected(): boolean {
|
|
26
|
+
return this.wsClient?.isConnected() || false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async sendTransaction(request: BscSendTxRequestType): Promise<void> {
|
|
30
|
+
if (!this.isConnected()) { log_warn('[48club-ws] not connected'); return; }
|
|
31
|
+
if (!request.tipTx) { log_warn('[48club-ws] tipTx required for bundle'); return; }
|
|
32
|
+
|
|
33
|
+
const currentTimestamp = Math.floor(Date.now() / 1000);
|
|
34
|
+
const bundleParams: any = {
|
|
35
|
+
txs: [request.mainTx, request.tipTx],
|
|
36
|
+
maxTimestamp: currentTimestamp + 2,
|
|
37
|
+
};
|
|
38
|
+
if (request.blockNumber > 0) bundleParams.maxBlockNumber = request.blockNumber + 2;
|
|
39
|
+
if (request._48spSign) bundleParams['48spSign'] = request._48spSign;
|
|
40
|
+
|
|
41
|
+
this.wsClient!.send(JSON.stringify({
|
|
42
|
+
jsonrpc: '2.0', id: '1',
|
|
43
|
+
method: 'eth_sendBundle',
|
|
44
|
+
params: [bundleParams]
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { log_error } from '@clonegod/ttd-core';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* BlockRazor HTTP API
|
|
6
|
+
* https://blockrazor.gitbook.io/blockrazor/bsc/block-builder
|
|
7
|
+
*/
|
|
8
|
+
export class BlockRazorTrade {
|
|
9
|
+
private rpcUrl: string;
|
|
10
|
+
private authToken: string;
|
|
11
|
+
|
|
12
|
+
constructor() {
|
|
13
|
+
this.rpcUrl = process.env.BLOCKRAZOR_RPC_URL || 'https://rpc.blockrazor.builders';
|
|
14
|
+
this.authToken = process.env.BLOCKRAZOR_AUTH_TOKEN || '';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async sendPrivateTransaction(signedTx: string): Promise<string> {
|
|
18
|
+
try {
|
|
19
|
+
const response = await axios.post(this.rpcUrl, {
|
|
20
|
+
jsonrpc: "2.0", id: "1",
|
|
21
|
+
method: "eth_sendPrivateTransaction",
|
|
22
|
+
params: [signedTx]
|
|
23
|
+
}, {
|
|
24
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': this.authToken }
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (response.data.error) {
|
|
28
|
+
throw new Error(`${response.data.error.code} - ${response.data.error.message}`);
|
|
29
|
+
}
|
|
30
|
+
return response.data.result;
|
|
31
|
+
} catch (error: any) {
|
|
32
|
+
log_error('blockrazor sendPrivateTransaction failed', error);
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async sendBundle(transactions: string[]): Promise<string> {
|
|
38
|
+
try {
|
|
39
|
+
const currentTimestamp = Math.floor(Date.now() / 1000);
|
|
40
|
+
const response = await axios.post(this.rpcUrl, {
|
|
41
|
+
jsonrpc: "2.0", id: "1",
|
|
42
|
+
method: "eth_sendBundle",
|
|
43
|
+
params: [{ txs: transactions, maxTimestamp: currentTimestamp + 5 }]
|
|
44
|
+
}, {
|
|
45
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': this.authToken }
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (response.data.error) {
|
|
49
|
+
throw new Error(`Bundle: ${response.data.error.message}`);
|
|
50
|
+
}
|
|
51
|
+
return response.data.result;
|
|
52
|
+
} catch (error: any) {
|
|
53
|
+
log_error('blockrazor sendBundle failed', error);
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { BloXRouteWsSender } from './ws';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { log_info, log_warn, WebSocketClient } from '@clonegod/ttd-core';
|
|
2
|
+
import { BscSendTxRequestType } from '../types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* BloXRoute WebSocket 发送器(长连接发送 bundle 到 builder)
|
|
6
|
+
* https://docs.bloxroute.com/bsc-and-eth/apis/transaction-bundles
|
|
7
|
+
*/
|
|
8
|
+
export class BloXRouteWsSender {
|
|
9
|
+
private wsClient: WebSocketClient | null = null;
|
|
10
|
+
private wsUrl: string;
|
|
11
|
+
private authToken: string;
|
|
12
|
+
|
|
13
|
+
constructor() {
|
|
14
|
+
this.wsUrl = process.env.BLOXROUTE_WS_URL || 'wss://api.blxrbdn.com/ws';
|
|
15
|
+
this.authToken = process.env.BLOX_AUTH_KEY || '';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
connect(): void {
|
|
19
|
+
if (this.wsClient?.isConnected()) return;
|
|
20
|
+
if (this.wsClient) { this.wsClient.disconnect(); this.wsClient = null; }
|
|
21
|
+
|
|
22
|
+
const headers: { [key: string]: string } = {};
|
|
23
|
+
if (this.authToken) headers['Authorization'] = this.authToken;
|
|
24
|
+
|
|
25
|
+
this.wsClient = new WebSocketClient(this.wsUrl, { headers, reconnectInterval: 1000 });
|
|
26
|
+
this.wsClient.onOpen(() => log_info(`[bloxroute-ws] connected`));
|
|
27
|
+
this.wsClient.onMessage((msg: any) => log_info(`[bloxroute-ws] response:`, msg));
|
|
28
|
+
this.wsClient.connect();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
isConnected(): boolean {
|
|
32
|
+
return this.wsClient?.isConnected() || false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async sendTransaction(request: BscSendTxRequestType): Promise<void> {
|
|
36
|
+
if (!this.isConnected()) { log_warn('[bloxroute-ws] not connected'); return; }
|
|
37
|
+
if (!request.tipTx) { log_warn('[bloxroute-ws] tipTx required for bundle'); return; }
|
|
38
|
+
|
|
39
|
+
const targetBlockNumber = request.blockNumber + 4;
|
|
40
|
+
|
|
41
|
+
this.wsClient!.send(JSON.stringify({
|
|
42
|
+
id: "1",
|
|
43
|
+
method: "blxr_submit_bundle",
|
|
44
|
+
params: {
|
|
45
|
+
transaction: [request.mainTx, request.tipTx],
|
|
46
|
+
blockchain_network: "BSC-Mainnet",
|
|
47
|
+
block_number: `0x${targetBlockNumber.toString(16)}`,
|
|
48
|
+
max_timestamp: Math.floor(Date.now() / 1000) + 2,
|
|
49
|
+
mev_builders: { "all": "" }
|
|
50
|
+
}
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/constants.ts
ADDED
package/src/index.ts
ADDED
package/src/rpc/index.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { log_warn } from '@clonegod/ttd-core';
|
|
2
|
+
|
|
3
|
+
export class BscMainnetRpc {
|
|
4
|
+
private url: string;
|
|
5
|
+
private headers: Headers;
|
|
6
|
+
|
|
7
|
+
constructor(rpc_endpoint: string) {
|
|
8
|
+
this.url = rpc_endpoint || process.env.BSC_RPC_ENDPOINT || '';
|
|
9
|
+
this.headers = new Headers();
|
|
10
|
+
this.headers.append("Content-Type", "application/json");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async eth_sendRawTransaction(signedTx: string): Promise<string> {
|
|
14
|
+
setTimeout(async () => {
|
|
15
|
+
console.log(`Sending transaction to ${this.url}`);
|
|
16
|
+
|
|
17
|
+
const raw = JSON.stringify({
|
|
18
|
+
jsonrpc: "2.0",
|
|
19
|
+
method: "eth_sendRawTransaction",
|
|
20
|
+
params: [signedTx],
|
|
21
|
+
id: 1
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const requestOptions = {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: this.headers,
|
|
27
|
+
body: raw,
|
|
28
|
+
redirect: 'follow' as const
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const response = await fetch(this.url, requestOptions);
|
|
32
|
+
const result = await response.json();
|
|
33
|
+
|
|
34
|
+
if (result.error) {
|
|
35
|
+
log_warn(result.error.message);
|
|
36
|
+
}
|
|
37
|
+
return result.result;
|
|
38
|
+
}, 0);
|
|
39
|
+
|
|
40
|
+
return '';
|
|
41
|
+
}
|
|
42
|
+
}
|