@clonegod/ttd-base-send-tx 0.1.1

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.
@@ -0,0 +1,37 @@
1
+ /**
2
+ * BlockRazor BASE Fast 通道
3
+ *
4
+ * - 文档:https://blockrazor.gitbook.io/blockrazor/tc/transaction-submission/fast/base
5
+ * - 协议:HTTP POST `eth_sendRawTransaction`(标准 JSON-RPC),与 direct_rpc 同形态
6
+ * - 认证:`Authorization: Bearer <token>` header
7
+ * - Endpoint:`http://base-fast.blockrazor.io`(默认)
8
+ * - 路由:私有 mempool + Speed Boost 转发给 sequencer
9
+ *
10
+ * ## 强制 tip 要求
11
+ *
12
+ * BlockRazor BASE Fast **强制**每笔 tx 给 tip:
13
+ * 方式 A:tx 内含 transfer ≥ 0.000003 ETH 到 `0x9D70AC39166ca154307a93fa6b595CF7962fe8e5`
14
+ * 方式 B:`maxPriorityFeePerGas` ≥ 估算成本的 5%
15
+ *
16
+ * **本通道只负责转发已签名 raw tx**。调用方(trade encode 侧)必须事先满足
17
+ * tip 要求,否则 BlockRazor 会 reject。
18
+ *
19
+ * ## TPS 限制
20
+ *
21
+ * - 默认 10 TPS(不分套餐);联系 BlockRazor 升级。
22
+ */
23
+ import { SendChannel, SendChannelResult } from './types';
24
+ export declare class BlockRazorChannel implements SendChannel {
25
+ private url;
26
+ readonly name = "blockrazor";
27
+ private pool;
28
+ private origin;
29
+ private path;
30
+ private authHeader;
31
+ private pingTimer?;
32
+ constructor(url: string, authToken: string);
33
+ warmup(timeoutMs?: number): Promise<void>;
34
+ send(signedTx: string, timeoutMs: number): Promise<SendChannelResult>;
35
+ private rpcCall;
36
+ close(): Promise<void>;
37
+ }
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ /**
3
+ * BlockRazor BASE Fast 通道
4
+ *
5
+ * - 文档:https://blockrazor.gitbook.io/blockrazor/tc/transaction-submission/fast/base
6
+ * - 协议:HTTP POST `eth_sendRawTransaction`(标准 JSON-RPC),与 direct_rpc 同形态
7
+ * - 认证:`Authorization: Bearer <token>` header
8
+ * - Endpoint:`http://base-fast.blockrazor.io`(默认)
9
+ * - 路由:私有 mempool + Speed Boost 转发给 sequencer
10
+ *
11
+ * ## 强制 tip 要求
12
+ *
13
+ * BlockRazor BASE Fast **强制**每笔 tx 给 tip:
14
+ * 方式 A:tx 内含 transfer ≥ 0.000003 ETH 到 `0x9D70AC39166ca154307a93fa6b595CF7962fe8e5`
15
+ * 方式 B:`maxPriorityFeePerGas` ≥ 估算成本的 5%
16
+ *
17
+ * **本通道只负责转发已签名 raw tx**。调用方(trade encode 侧)必须事先满足
18
+ * tip 要求,否则 BlockRazor 会 reject。
19
+ *
20
+ * ## TPS 限制
21
+ *
22
+ * - 默认 10 TPS(不分套餐);联系 BlockRazor 升级。
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.BlockRazorChannel = void 0;
26
+ const ttd_core_1 = require("@clonegod/ttd-core");
27
+ const undici_1 = require("undici");
28
+ const PING_INTERVAL_MS = 30000;
29
+ class BlockRazorChannel {
30
+ constructor(url, authToken) {
31
+ this.url = url;
32
+ this.name = 'blockrazor';
33
+ if (!authToken) {
34
+ throw new Error('[BlockRazorChannel] authToken is required');
35
+ }
36
+ const u = new URL(url);
37
+ this.origin = `${u.protocol}//${u.host}`;
38
+ this.path = `${u.pathname}${u.search}` || '/';
39
+ this.authHeader = `Bearer ${authToken}`;
40
+ this.pool = new undici_1.Pool(this.origin, {
41
+ connections: 4,
42
+ pipelining: 1,
43
+ keepAliveTimeout: 10 * 60000,
44
+ keepAliveMaxTimeout: 10 * 60000,
45
+ headersTimeout: 5000,
46
+ bodyTimeout: 5000,
47
+ });
48
+ }
49
+ async warmup(timeoutMs = 5000) {
50
+ const t0 = Date.now();
51
+ try {
52
+ const json = await this.rpcCall('eth_chainId', [], timeoutMs);
53
+ if (json?.result) {
54
+ (0, ttd_core_1.log_info)(`[${this.name}] warmup OK ${this.url} chainId=${json.result} ${Date.now() - t0}ms`);
55
+ }
56
+ else {
57
+ (0, ttd_core_1.log_warn)(`[${this.name}] warmup soft-fail ${this.url} ${Date.now() - t0}ms err=${json?.error?.message || 'no result'}`);
58
+ }
59
+ }
60
+ catch (e) {
61
+ (0, ttd_core_1.log_warn)(`[${this.name}] warmup err ${this.url} ${Date.now() - t0}ms: ${e?.message || e}`);
62
+ }
63
+ if (!this.pingTimer) {
64
+ this.pingTimer = setInterval(() => {
65
+ this.rpcCall('eth_chainId', [], 3000).catch(() => { });
66
+ }, PING_INTERVAL_MS);
67
+ this.pingTimer.unref?.();
68
+ }
69
+ }
70
+ async send(signedTx, timeoutMs) {
71
+ const t0 = Date.now();
72
+ try {
73
+ const json = await this.rpcCall('eth_sendRawTransaction', [signedTx], timeoutMs);
74
+ if (json?.error) {
75
+ return { channel: this.name, elapsedMs: Date.now() - t0, error: `RPC: ${json.error.message}` };
76
+ }
77
+ return { channel: this.name, elapsedMs: Date.now() - t0, txHash: json?.result };
78
+ }
79
+ catch (e) {
80
+ const msg = e?.name === 'AbortError' || e?.message?.includes('aborted') ? `timeout(${timeoutMs}ms)` : (e?.message || String(e));
81
+ (0, ttd_core_1.log_warn)(`[${this.name}] send error: ${msg}`);
82
+ return { channel: this.name, elapsedMs: Date.now() - t0, error: msg };
83
+ }
84
+ }
85
+ async rpcCall(method, params, timeoutMs) {
86
+ const controller = new AbortController();
87
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
88
+ try {
89
+ const { statusCode, body } = await this.pool.request({
90
+ method: 'POST',
91
+ path: this.path,
92
+ headers: {
93
+ 'content-type': 'application/json',
94
+ 'authorization': this.authHeader,
95
+ },
96
+ body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }),
97
+ signal: controller.signal,
98
+ });
99
+ if (statusCode < 200 || statusCode >= 300) {
100
+ try {
101
+ await body.text();
102
+ }
103
+ catch { /* ignore */ }
104
+ throw new Error(`HTTP ${statusCode}`);
105
+ }
106
+ const text = await body.text();
107
+ return JSON.parse(text);
108
+ }
109
+ finally {
110
+ clearTimeout(timer);
111
+ }
112
+ }
113
+ async close() {
114
+ if (this.pingTimer) {
115
+ clearInterval(this.pingTimer);
116
+ this.pingTimer = undefined;
117
+ }
118
+ await this.pool.close();
119
+ }
120
+ }
121
+ exports.BlockRazorChannel = BlockRazorChannel;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * bloXroute BASE 通道
3
+ *
4
+ * - 文档:https://docs.bloxroute.com/base/submit-transactions/submit-transactions
5
+ * - Speed Boost:https://docs.bloxroute.com/base/submit-transactions/speed-boost
6
+ * - 协议:HTTP POST `blxr_tx`(**非** eth_sendRawTransaction,是 bloXroute 私有方法)
7
+ * - 认证:`Authorization: <token>`(注意**无** Bearer 前缀)
8
+ * - Endpoint:`https://api.blxrbdn.com`
9
+ *
10
+ * ## 参数差异(vs 标准 JSON-RPC)
11
+ *
12
+ * - `transaction`: raw transactions bytes,**without `0x` prefix**
13
+ * - `blockchain_network`: 必须 `Base-Mainnet`
14
+ * - `backrunme_reward_address`: 可选——开启时自动注册 BackRunMe,回扣 MEV 利润到此地址
15
+ *
16
+ * ## 与 BlockRazor 的不同
17
+ *
18
+ * - **无强制 tip**:不需要 caller 在 tx 内含 transfer 或抬高 priorityFee
19
+ * - Speed Boost 需另购(通过 portal);标准提交已经走私有路由
20
+ *
21
+ * ## 本通道做什么
22
+ *
23
+ * - 接收已签名 raw tx(带 0x 前缀),自动去前缀适配 bloXroute 协议
24
+ * - 注入 `blockchain_network = Base-Mainnet`
25
+ * - 可选注入 `backrunme_reward_address`
26
+ */
27
+ import { SendChannel, SendChannelResult } from './types';
28
+ export interface BloxrouteChannelOptions {
29
+ /** 完整 url,例如 https://api.blxrbdn.com */
30
+ url: string;
31
+ /** Authorization header 完整值(bloXroute 不带 Bearer 前缀) */
32
+ authToken: string;
33
+ /** 可选:BackRunMe 回扣收款地址(开启回扣服务时填) */
34
+ backrunmeRewardAddress?: string;
35
+ }
36
+ export declare class BloxrouteChannel implements SendChannel {
37
+ readonly name = "bloxroute";
38
+ private pool;
39
+ private origin;
40
+ private path;
41
+ private authHeader;
42
+ private backrunmeRewardAddress?;
43
+ private pingTimer?;
44
+ constructor(opts: BloxrouteChannelOptions);
45
+ /**
46
+ * bloXroute 没有 `eth_chainId` 这种标准 ping。
47
+ * 这里只建 TCP+TLS(请求一个肯定返回的 404/method-not-found,仍能让连接进入复用池)。
48
+ */
49
+ warmup(timeoutMs?: number): Promise<void>;
50
+ send(signedTx: string, timeoutMs: number): Promise<SendChannelResult>;
51
+ private postRaw;
52
+ close(): Promise<void>;
53
+ }
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ /**
3
+ * bloXroute BASE 通道
4
+ *
5
+ * - 文档:https://docs.bloxroute.com/base/submit-transactions/submit-transactions
6
+ * - Speed Boost:https://docs.bloxroute.com/base/submit-transactions/speed-boost
7
+ * - 协议:HTTP POST `blxr_tx`(**非** eth_sendRawTransaction,是 bloXroute 私有方法)
8
+ * - 认证:`Authorization: <token>`(注意**无** Bearer 前缀)
9
+ * - Endpoint:`https://api.blxrbdn.com`
10
+ *
11
+ * ## 参数差异(vs 标准 JSON-RPC)
12
+ *
13
+ * - `transaction`: raw transactions bytes,**without `0x` prefix**
14
+ * - `blockchain_network`: 必须 `Base-Mainnet`
15
+ * - `backrunme_reward_address`: 可选——开启时自动注册 BackRunMe,回扣 MEV 利润到此地址
16
+ *
17
+ * ## 与 BlockRazor 的不同
18
+ *
19
+ * - **无强制 tip**:不需要 caller 在 tx 内含 transfer 或抬高 priorityFee
20
+ * - Speed Boost 需另购(通过 portal);标准提交已经走私有路由
21
+ *
22
+ * ## 本通道做什么
23
+ *
24
+ * - 接收已签名 raw tx(带 0x 前缀),自动去前缀适配 bloXroute 协议
25
+ * - 注入 `blockchain_network = Base-Mainnet`
26
+ * - 可选注入 `backrunme_reward_address`
27
+ */
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.BloxrouteChannel = void 0;
30
+ const ttd_core_1 = require("@clonegod/ttd-core");
31
+ const undici_1 = require("undici");
32
+ const PING_INTERVAL_MS = 30000;
33
+ class BloxrouteChannel {
34
+ constructor(opts) {
35
+ this.name = 'bloxroute';
36
+ if (!opts.authToken) {
37
+ throw new Error('[BloxrouteChannel] authToken is required');
38
+ }
39
+ const u = new URL(opts.url);
40
+ this.origin = `${u.protocol}//${u.host}`;
41
+ this.path = `${u.pathname}${u.search}` || '/';
42
+ this.authHeader = opts.authToken;
43
+ this.backrunmeRewardAddress = opts.backrunmeRewardAddress || undefined;
44
+ this.pool = new undici_1.Pool(this.origin, {
45
+ connections: 4,
46
+ pipelining: 1,
47
+ keepAliveTimeout: 10 * 60000,
48
+ keepAliveMaxTimeout: 10 * 60000,
49
+ headersTimeout: 5000,
50
+ bodyTimeout: 5000,
51
+ });
52
+ }
53
+ /**
54
+ * bloXroute 没有 `eth_chainId` 这种标准 ping。
55
+ * 这里只建 TCP+TLS(请求一个肯定返回的 404/method-not-found,仍能让连接进入复用池)。
56
+ */
57
+ async warmup(timeoutMs = 5000) {
58
+ const t0 = Date.now();
59
+ try {
60
+ await this.postRaw('{"jsonrpc":"2.0","id":1,"method":"ping","params":[]}', timeoutMs);
61
+ (0, ttd_core_1.log_info)(`[${this.name}] warmup OK ${this.origin} ${Date.now() - t0}ms`);
62
+ }
63
+ catch (e) {
64
+ (0, ttd_core_1.log_warn)(`[${this.name}] warmup err ${this.origin} ${Date.now() - t0}ms: ${e?.message || e}`);
65
+ }
66
+ if (!this.pingTimer) {
67
+ this.pingTimer = setInterval(() => {
68
+ this.postRaw('{"jsonrpc":"2.0","id":1,"method":"ping","params":[]}', 3000)
69
+ .catch(() => { });
70
+ }, PING_INTERVAL_MS);
71
+ this.pingTimer.unref?.();
72
+ }
73
+ }
74
+ async send(signedTx, timeoutMs) {
75
+ const t0 = Date.now();
76
+ // bloXroute 要求 raw tx **去掉 0x 前缀**
77
+ const rawTx = signedTx.startsWith('0x') ? signedTx.slice(2) : signedTx;
78
+ const params = {
79
+ transaction: rawTx,
80
+ blockchain_network: 'Base-Mainnet',
81
+ };
82
+ if (this.backrunmeRewardAddress) {
83
+ params.backrunme_reward_address = this.backrunmeRewardAddress;
84
+ }
85
+ try {
86
+ const body = JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'blxr_tx', params });
87
+ const text = await this.postRaw(body, timeoutMs);
88
+ const json = JSON.parse(text);
89
+ if (json.error) {
90
+ return { channel: this.name, elapsedMs: Date.now() - t0, error: `RPC: ${json.error.message}` };
91
+ }
92
+ const hash = json.result?.txHash;
93
+ const normalized = hash && !hash.startsWith('0x') ? `0x${hash}` : hash;
94
+ return { channel: this.name, elapsedMs: Date.now() - t0, txHash: normalized };
95
+ }
96
+ catch (e) {
97
+ const msg = e?.name === 'AbortError' || e?.message?.includes('aborted') ? `timeout(${timeoutMs}ms)` : (e?.message || String(e));
98
+ (0, ttd_core_1.log_warn)(`[${this.name}] send error: ${msg}`);
99
+ return { channel: this.name, elapsedMs: Date.now() - t0, error: msg };
100
+ }
101
+ }
102
+ async postRaw(body, timeoutMs) {
103
+ const controller = new AbortController();
104
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
105
+ try {
106
+ const { statusCode, body: respBody } = await this.pool.request({
107
+ method: 'POST',
108
+ path: this.path,
109
+ headers: {
110
+ 'content-type': 'application/json',
111
+ 'authorization': this.authHeader,
112
+ },
113
+ body,
114
+ signal: controller.signal,
115
+ });
116
+ const text = await respBody.text();
117
+ if (statusCode < 200 || statusCode >= 300) {
118
+ throw new Error(`HTTP ${statusCode}: ${text.slice(0, 200)}`);
119
+ }
120
+ return text;
121
+ }
122
+ finally {
123
+ clearTimeout(timer);
124
+ }
125
+ }
126
+ async close() {
127
+ if (this.pingTimer) {
128
+ clearInterval(this.pingTimer);
129
+ this.pingTimer = undefined;
130
+ }
131
+ await this.pool.close();
132
+ }
133
+ }
134
+ exports.BloxrouteChannel = BloxrouteChannel;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * 直发 sequencer RPC 通道(BASE 主路径)
3
+ *
4
+ * - HTTP POST eth_sendRawTransaction 到 BASE sequencer
5
+ * - 默认 https://mainnet.base.org(Coinbase 官方)
6
+ * - 生产建议换 Alchemy / QuickNode 的 private endpoint:跟 sequencer 有专线,延迟更低
7
+ *
8
+ * ## BASE 上为什么不需要"MEV 保护通道"
9
+ *
10
+ * BASE 是单 sequencer rollup(Coinbase 运维),具备 ETH 主网用 Flashbots Protect 想达到的特性:
11
+ * 1. **无公开 mempool**:sequencer 接收到的 tx 不广播给公网
12
+ * 2. **无 builder 竞价**:没有第三方 builder 看你的 tx,没有 PBS 拍卖
13
+ * 3. **Flashblocks 排序锁定**:tx 一旦进入 Flashblock 就不能被高 fee tx 越过
14
+ *
15
+ * Flashbots Protect 当前**不支持 BASE**(只支持 ETH 主网 + Sepolia),且事实上也不需要。
16
+ * 直发 sequencer 已经是 BASE 上的"最优策略"——速度即护身符。
17
+ *
18
+ * ## 延迟优化(vs Node 全局 fetch)
19
+ *
20
+ * 用持久化 undici Pool 替代全局 fetch,单独控制:
21
+ * 1. **keepAliveTimeout=10min**:服务端单方面断连前一直保活,arb 低谷期不掉
22
+ * 2. **keepAliveMaxTimeout=10min**:极限保活上限
23
+ * 3. **pipelining=1**:避免请求重排序导致的 nonce 抢跑
24
+ * 4. **connections=4**:单 host 4 个并发连接(足够 arb 突发)
25
+ * 5. **periodic ping**:每 30s 发 eth_chainId,强制走"已建立"连接防服务端 idle 断
26
+ *
27
+ * 累计收益:消除 ~100-300ms 冷启动 + arb 低谷期回弹的一致性问题。
28
+ *
29
+ * ## 注意事项
30
+ *
31
+ * - **无 revert protection**:失败的 tx 会上链扣 gas → 业务必须先做链下模拟
32
+ */
33
+ import { SendChannel, SendChannelResult } from './types';
34
+ export declare class DirectRpcChannel implements SendChannel {
35
+ private url;
36
+ readonly name = "direct_rpc";
37
+ private pool;
38
+ private origin;
39
+ private path;
40
+ private pingTimer?;
41
+ constructor(url: string);
42
+ /**
43
+ * 启动期预热 + 启动后台 ping。
44
+ * 预热建立 TCP+TLS,让第一笔真实交易免握手;
45
+ * 后台 ping 每 30s 走一次连接防服务端 idle 断。
46
+ */
47
+ warmup(timeoutMs?: number): Promise<void>;
48
+ send(signedTx: string, timeoutMs: number): Promise<SendChannelResult>;
49
+ private rpcCall;
50
+ /** 测试/优雅停机用:销毁连接池 + 停 ping */
51
+ close(): Promise<void>;
52
+ }
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ /**
3
+ * 直发 sequencer RPC 通道(BASE 主路径)
4
+ *
5
+ * - HTTP POST eth_sendRawTransaction 到 BASE sequencer
6
+ * - 默认 https://mainnet.base.org(Coinbase 官方)
7
+ * - 生产建议换 Alchemy / QuickNode 的 private endpoint:跟 sequencer 有专线,延迟更低
8
+ *
9
+ * ## BASE 上为什么不需要"MEV 保护通道"
10
+ *
11
+ * BASE 是单 sequencer rollup(Coinbase 运维),具备 ETH 主网用 Flashbots Protect 想达到的特性:
12
+ * 1. **无公开 mempool**:sequencer 接收到的 tx 不广播给公网
13
+ * 2. **无 builder 竞价**:没有第三方 builder 看你的 tx,没有 PBS 拍卖
14
+ * 3. **Flashblocks 排序锁定**:tx 一旦进入 Flashblock 就不能被高 fee tx 越过
15
+ *
16
+ * Flashbots Protect 当前**不支持 BASE**(只支持 ETH 主网 + Sepolia),且事实上也不需要。
17
+ * 直发 sequencer 已经是 BASE 上的"最优策略"——速度即护身符。
18
+ *
19
+ * ## 延迟优化(vs Node 全局 fetch)
20
+ *
21
+ * 用持久化 undici Pool 替代全局 fetch,单独控制:
22
+ * 1. **keepAliveTimeout=10min**:服务端单方面断连前一直保活,arb 低谷期不掉
23
+ * 2. **keepAliveMaxTimeout=10min**:极限保活上限
24
+ * 3. **pipelining=1**:避免请求重排序导致的 nonce 抢跑
25
+ * 4. **connections=4**:单 host 4 个并发连接(足够 arb 突发)
26
+ * 5. **periodic ping**:每 30s 发 eth_chainId,强制走"已建立"连接防服务端 idle 断
27
+ *
28
+ * 累计收益:消除 ~100-300ms 冷启动 + arb 低谷期回弹的一致性问题。
29
+ *
30
+ * ## 注意事项
31
+ *
32
+ * - **无 revert protection**:失败的 tx 会上链扣 gas → 业务必须先做链下模拟
33
+ */
34
+ Object.defineProperty(exports, "__esModule", { value: true });
35
+ exports.DirectRpcChannel = void 0;
36
+ const ttd_core_1 = require("@clonegod/ttd-core");
37
+ const undici_1 = require("undici");
38
+ const PING_INTERVAL_MS = 30000;
39
+ class DirectRpcChannel {
40
+ constructor(url) {
41
+ this.url = url;
42
+ this.name = 'direct_rpc';
43
+ const u = new URL(url);
44
+ this.origin = `${u.protocol}//${u.host}`;
45
+ this.path = `${u.pathname}${u.search}` || '/';
46
+ // 持久化 undici Pool:keep-alive 极长,确保 idle 期间不断
47
+ this.pool = new undici_1.Pool(this.origin, {
48
+ connections: 4,
49
+ pipelining: 1,
50
+ keepAliveTimeout: 10 * 60000, // 10 分钟
51
+ keepAliveMaxTimeout: 10 * 60000,
52
+ headersTimeout: 5000,
53
+ bodyTimeout: 5000,
54
+ });
55
+ }
56
+ /**
57
+ * 启动期预热 + 启动后台 ping。
58
+ * 预热建立 TCP+TLS,让第一笔真实交易免握手;
59
+ * 后台 ping 每 30s 走一次连接防服务端 idle 断。
60
+ */
61
+ async warmup(timeoutMs = 5000) {
62
+ const t0 = Date.now();
63
+ try {
64
+ const json = await this.rpcCall('eth_chainId', [], timeoutMs);
65
+ if (json?.result) {
66
+ (0, ttd_core_1.log_info)(`[${this.name}] warmup OK ${this.url} chainId=${json.result} ${Date.now() - t0}ms`);
67
+ }
68
+ else {
69
+ (0, ttd_core_1.log_warn)(`[${this.name}] warmup soft-fail ${this.url} ${Date.now() - t0}ms err=${json?.error?.message || 'no result'}`);
70
+ }
71
+ }
72
+ catch (e) {
73
+ (0, ttd_core_1.log_warn)(`[${this.name}] warmup err ${this.url} ${Date.now() - t0}ms: ${e?.message || e}`);
74
+ }
75
+ // 启动后台 ping(fire-and-forget,进程退出时随之回收)
76
+ if (!this.pingTimer) {
77
+ this.pingTimer = setInterval(() => {
78
+ this.rpcCall('eth_chainId', [], 3000).catch(() => { });
79
+ }, PING_INTERVAL_MS);
80
+ this.pingTimer.unref?.();
81
+ }
82
+ }
83
+ async send(signedTx, timeoutMs) {
84
+ const t0 = Date.now();
85
+ try {
86
+ const json = await this.rpcCall('eth_sendRawTransaction', [signedTx], timeoutMs);
87
+ if (json?.error) {
88
+ return { channel: this.name, elapsedMs: Date.now() - t0, error: `RPC: ${json.error.message}` };
89
+ }
90
+ return { channel: this.name, elapsedMs: Date.now() - t0, txHash: json?.result };
91
+ }
92
+ catch (e) {
93
+ const msg = e?.name === 'AbortError' || e?.message?.includes('aborted') ? `timeout(${timeoutMs}ms)` : (e?.message || String(e));
94
+ (0, ttd_core_1.log_warn)(`[${this.name}] send error: ${msg}`);
95
+ return { channel: this.name, elapsedMs: Date.now() - t0, error: msg };
96
+ }
97
+ }
98
+ async rpcCall(method, params, timeoutMs) {
99
+ const controller = new AbortController();
100
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
101
+ try {
102
+ const { statusCode, body } = await this.pool.request({
103
+ method: 'POST',
104
+ path: this.path,
105
+ headers: { 'content-type': 'application/json' },
106
+ body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }),
107
+ signal: controller.signal,
108
+ });
109
+ if (statusCode < 200 || statusCode >= 300) {
110
+ // 消费 body 防内存泄露
111
+ try {
112
+ await body.text();
113
+ }
114
+ catch { /* ignore */ }
115
+ throw new Error(`HTTP ${statusCode}`);
116
+ }
117
+ const text = await body.text();
118
+ return JSON.parse(text);
119
+ }
120
+ finally {
121
+ clearTimeout(timer);
122
+ }
123
+ }
124
+ /** 测试/优雅停机用:销毁连接池 + 停 ping */
125
+ async close() {
126
+ if (this.pingTimer) {
127
+ clearInterval(this.pingTimer);
128
+ this.pingTimer = undefined;
129
+ }
130
+ await this.pool.close();
131
+ }
132
+ }
133
+ exports.DirectRpcChannel = DirectRpcChannel;
@@ -0,0 +1,4 @@
1
+ export * from './types';
2
+ export * from './direct_rpc';
3
+ export * from './blockrazor';
4
+ export * from './bloxroute';
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./types"), exports);
18
+ __exportStar(require("./direct_rpc"), exports);
19
+ __exportStar(require("./blockrazor"), exports);
20
+ __exportStar(require("./bloxroute"), exports);
@@ -0,0 +1,32 @@
1
+ /**
2
+ * 发送通道抽象 — 所有 BASE 发送通道(Flashbots Protect / 直发 sequencer / 未来的私有 RPC)
3
+ * 都通过一个统一接口暴露给 TransactionSender。
4
+ *
5
+ * 设计目标:
6
+ * - 通道是纯函数:拿到 signedTx + timeout,返回结果或抛错;不关心策略/优先级
7
+ * - TransactionSender 负责并发 / 顺序 / 重试编排
8
+ * - 通道之间相互独立,可热插拔
9
+ */
10
+ export interface SendChannelResult {
11
+ /** 通道名(用于日志/分析)*/
12
+ channel: string;
13
+ /** sequencer / relay 返回的 tx hash(成功);undefined = 失败但无 hash */
14
+ txHash?: string;
15
+ /** 总耗时(ms)*/
16
+ elapsedMs: number;
17
+ /** 错误信息(失败时)*/
18
+ error?: string;
19
+ }
20
+ export interface SendChannel {
21
+ readonly name: string;
22
+ /**
23
+ * @param signedTx hex-encoded raw transaction(必须以 0x 开头)
24
+ * @param timeoutMs 单次发送超时(ms);超时即视为失败
25
+ */
26
+ send(signedTx: string, timeoutMs: number): Promise<SendChannelResult>;
27
+ /**
28
+ * 可选:启动期连接预热(建立 TCP+TLS,复用给后续 send)。
29
+ * 实现方应 fire-and-forget:失败不抛错,仅 log。
30
+ */
31
+ warmup?(timeoutMs?: number): Promise<void>;
32
+ }
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ /**
3
+ * 发送通道抽象 — 所有 BASE 发送通道(Flashbots Protect / 直发 sequencer / 未来的私有 RPC)
4
+ * 都通过一个统一接口暴露给 TransactionSender。
5
+ *
6
+ * 设计目标:
7
+ * - 通道是纯函数:拿到 signedTx + timeout,返回结果或抛错;不关心策略/优先级
8
+ * - TransactionSender 负责并发 / 顺序 / 重试编排
9
+ * - 通道之间相互独立,可热插拔
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,2 @@
1
+ export { TransactionSender } from './transaction_sender';
2
+ export { BASE_EOA_ADDRESS } from '@clonegod/ttd-base-common';
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BASE_EOA_ADDRESS = exports.TransactionSender = void 0;
4
+ var transaction_sender_1 = require("./transaction_sender");
5
+ Object.defineProperty(exports, "TransactionSender", { enumerable: true, get: function () { return transaction_sender_1.TransactionSender; } });
6
+ var ttd_base_common_1 = require("@clonegod/ttd-base-common");
7
+ Object.defineProperty(exports, "BASE_EOA_ADDRESS", { enumerable: true, get: function () { return ttd_base_common_1.BASE_EOA_ADDRESS; } });
@@ -0,0 +1,47 @@
1
+ import { ITransactionSender } from '@clonegod/ttd-base-common';
2
+ import { AppConfig } from '@clonegod/ttd-core';
3
+ /** TradeTrace duck-type 接口(避免对 ttd-base-common 的 TradeTrace 实现强依赖) */
4
+ interface TraceWriter {
5
+ mark(name: string): void;
6
+ }
7
+ /**
8
+ * BASE 链 trader 端 TransactionSender
9
+ *
10
+ * ## 与 BSC 的差异
11
+ *
12
+ * BSC:3 builder bundle 并发(BlockRazor / 48Club / BLXR),主 tx + 3 tip tx 并行签名,bundle 投递
13
+ * BASE:无原生 bundle 通道;私有路由商(BlockRazor / bloXroute)走单笔 `eth_sendRawTransaction`
14
+ *
15
+ * ## 发送策略(fan-out 多端并发)
16
+ *
17
+ * 同一笔签名好的 raw tx 并发提交到所有启用的通道,先到的成功,
18
+ * 后到的同 nonce/同 signature 在 sequencer / mempool 端自然去重。
19
+ *
20
+ * - `direct_rpc`:直发 BASE sequencer(endpoint = 通用 `RPC_ENDPOINT`,mainnet.base.org / 自家 endpoint,可启用 MEV Protection)
21
+ * - `backup_rpc`:可选第二个 direct_rpc(`BACKUP_RPC_ENDPOINT`,不同 provider 做 RPC 故障冗余)
22
+ * - `blockrazor`:BlockRazor BASE Fast(强制 tip,调用方需在 tx 中含 tip transfer 或抬高 priorityFee)
23
+ * - `bloxroute`:bloXroute `blxr_tx`(可选 BackRunMe 回扣)
24
+ *
25
+ * ## ⚠️ BASE 上不存在 revert protection
26
+ *
27
+ * 失败的 tx **会**上链并扣 gas(跟 ETH 主网 Flashbots Protect 不同)。
28
+ * → 调用方必须先做链下模拟(eth_call / estimateGas),保证 tx 必成功才发
29
+ */
30
+ export declare class TransactionSender implements ITransactionSender {
31
+ private channels;
32
+ private timeoutMs;
33
+ constructor(appConfig: AppConfig);
34
+ /**
35
+ * @param signedMainTx hex-encoded 主交易(无 tip 版本,发给非 tip 通道)
36
+ * @param channelTipTxMap channel name → 已签名 tx(同 nonce 的"含 tip"版本)
37
+ * 举例:{ 'blockrazor' => withTipSignedTx }
38
+ * 不在 map 中的 channel 收到 signedMainTx;在 map 中的 channel 收到对应 tx。
39
+ * Ethereum nonce 唯一性保证链上只有一笔执行,多通道并发安全。
40
+ * @param order_trace_id 订单追踪 id(日志用)
41
+ * @param pair 交易对名(日志用)
42
+ * @param _only_bundle BASE 忽略(无 bundle 概念)
43
+ * @param trace 可选 TradeTrace
44
+ */
45
+ sendTransaction(signedMainTx: string, channelTipTxMap: Map<string, string>, order_trace_id: string, pair?: string, _only_bundle?: boolean, trace?: TraceWriter): Promise<string[]>;
46
+ }
47
+ export {};