@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,127 @@
1
+ import { ITransactionSender, BaseEnvArgs } from '@clonegod/ttd-base-common'
2
+ import { AppConfig, log_info, log_warn } from '@clonegod/ttd-core'
3
+ import { BlockRazorChannel, BloxrouteChannel, DirectRpcChannel, SendChannel } from './channels'
4
+
5
+ /** TradeTrace duck-type 接口(避免对 ttd-base-common 的 TradeTrace 实现强依赖) */
6
+ interface TraceWriter {
7
+ mark(name: string): void
8
+ }
9
+
10
+ /**
11
+ * BASE 链 trader 端 TransactionSender
12
+ *
13
+ * ## 与 BSC 的差异
14
+ *
15
+ * BSC:3 builder bundle 并发(BlockRazor / 48Club / BLXR),主 tx + 3 tip tx 并行签名,bundle 投递
16
+ * BASE:无原生 bundle 通道;私有路由商(BlockRazor / bloXroute)走单笔 `eth_sendRawTransaction`
17
+ *
18
+ * ## 发送策略(fan-out 多端并发)
19
+ *
20
+ * 同一笔签名好的 raw tx 并发提交到所有启用的通道,先到的成功,
21
+ * 后到的同 nonce/同 signature 在 sequencer / mempool 端自然去重。
22
+ *
23
+ * - `direct_rpc`:直发 BASE sequencer(endpoint = 通用 `RPC_ENDPOINT`,mainnet.base.org / 自家 endpoint,可启用 MEV Protection)
24
+ * - `backup_rpc`:可选第二个 direct_rpc(`BACKUP_RPC_ENDPOINT`,不同 provider 做 RPC 故障冗余)
25
+ * - `blockrazor`:BlockRazor BASE Fast(强制 tip,调用方需在 tx 中含 tip transfer 或抬高 priorityFee)
26
+ * - `bloxroute`:bloXroute `blxr_tx`(可选 BackRunMe 回扣)
27
+ *
28
+ * ## ⚠️ BASE 上不存在 revert protection
29
+ *
30
+ * 失败的 tx **会**上链并扣 gas(跟 ETH 主网 Flashbots Protect 不同)。
31
+ * → 调用方必须先做链下模拟(eth_call / estimateGas),保证 tx 必成功才发
32
+ */
33
+ export class TransactionSender implements ITransactionSender {
34
+ private channels: SendChannel[] = []
35
+ private timeoutMs: number
36
+
37
+ constructor(appConfig: AppConfig) {
38
+ const env = appConfig.env_args as BaseEnvArgs
39
+
40
+ if (env.send_tx_direct_rpc) {
41
+ this.channels.push(new DirectRpcChannel(env.rpc_endpoint || 'https://mainnet.base.org'))
42
+ }
43
+ if (env.send_tx_backup_rpc && env.backup_rpc_endpoint) {
44
+ this.channels.push(new DirectRpcChannel(env.backup_rpc_endpoint))
45
+ }
46
+ if (env.send_tx_blockrazor) {
47
+ if (!env.blockrazor_auth_token) {
48
+ log_warn('[TransactionSender] SEND_TX_BLOCKRAZOR=true 但 BLOCKRAZOR_AUTH_TOKEN 未配置,跳过 BlockRazor 通道')
49
+ } else {
50
+ this.channels.push(new BlockRazorChannel(env.blockrazor_endpoint, env.blockrazor_auth_token))
51
+ }
52
+ }
53
+ if (env.send_tx_bloxroute) {
54
+ if (!env.bloxroute_auth_token) {
55
+ log_warn('[TransactionSender] SEND_TX_BLOXROUTE=true 但 BLOXROUTE_AUTH_TOKEN 未配置,跳过 bloXroute 通道')
56
+ } else {
57
+ this.channels.push(new BloxrouteChannel({
58
+ url: env.bloxroute_endpoint,
59
+ authToken: env.bloxroute_auth_token,
60
+ backrunmeRewardAddress: env.bloxroute_backrunme_address || undefined,
61
+ }))
62
+ }
63
+ }
64
+
65
+ this.timeoutMs = env.send_tx_timeout_ms || 3000
66
+
67
+ if (this.channels.length === 0) {
68
+ // 兜底:起码开主通道,避免空 sender 静默吞 tx
69
+ log_warn('[TransactionSender] 没有启用任何发送通道,回退到默认 direct_rpc')
70
+ this.channels.push(new DirectRpcChannel(env.rpc_endpoint || 'https://mainnet.base.org'))
71
+ }
72
+
73
+ log_info(`[TransactionSender] BASE 启动,通道=${this.channels.map(c => c.name).join(',')}, timeout=${this.timeoutMs}ms`)
74
+
75
+ // 预热所有通道(fire-and-forget,不阻塞构造)。建立 TCP+TLS 连接复用给后续 sendTransaction,
76
+ // 第一笔真实交易免握手。
77
+ for (const ch of this.channels) {
78
+ ch.warmup?.().catch(() => { /* warmup 失败已在通道内 log,此处吞掉 */ })
79
+ }
80
+ }
81
+
82
+ /**
83
+ * @param signedMainTx hex-encoded 主交易(无 tip 版本,发给非 tip 通道)
84
+ * @param channelTipTxMap channel name → 已签名 tx(同 nonce 的"含 tip"版本)
85
+ * 举例:{ 'blockrazor' => withTipSignedTx }
86
+ * 不在 map 中的 channel 收到 signedMainTx;在 map 中的 channel 收到对应 tx。
87
+ * Ethereum nonce 唯一性保证链上只有一笔执行,多通道并发安全。
88
+ * @param order_trace_id 订单追踪 id(日志用)
89
+ * @param pair 交易对名(日志用)
90
+ * @param _only_bundle BASE 忽略(无 bundle 概念)
91
+ * @param trace 可选 TradeTrace
92
+ */
93
+ async sendTransaction(
94
+ signedMainTx: string,
95
+ channelTipTxMap: Map<string, string>,
96
+ order_trace_id: string,
97
+ pair: string = '',
98
+ _only_bundle: boolean = false,
99
+ trace?: TraceWriter,
100
+ ): Promise<string[]> {
101
+ trace?.mark('send_start')
102
+
103
+ const sends = this.channels.map(ch => {
104
+ const tx = channelTipTxMap?.get(ch.name) ?? signedMainTx
105
+ return ch.send(tx, this.timeoutMs)
106
+ })
107
+ const results = await Promise.all(sends)
108
+
109
+ trace?.mark('send_done')
110
+
111
+ const ok = results.filter(r => !!r.txHash)
112
+ const fail = results.filter(r => !!r.error)
113
+
114
+ for (const r of ok) {
115
+ log_info(`[send] ✅ channel=${r.channel} trace=${order_trace_id} pair=${pair} ${r.elapsedMs}ms tx=${r.txHash}`)
116
+ }
117
+ for (const r of fail) {
118
+ log_warn(`[send] ❌ channel=${r.channel} trace=${order_trace_id} pair=${pair} ${r.elapsedMs}ms err=${r.error}`)
119
+ }
120
+
121
+ if (ok.length === 0) {
122
+ throw new Error(`所有 send 通道失败: ${fail.map(f => `${f.channel}=${f.error}`).join(' | ')}`)
123
+ }
124
+
125
+ return results.map(r => r.txHash ? `${r.channel}: ${r.txHash}` : `${r.channel}: ERR ${r.error}`)
126
+ }
127
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2020",
4
+ "module": "commonjs",
5
+ "lib": ["es2020"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": false,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "declaration": true,
14
+ "sourceMap": false
15
+ },
16
+ "include": ["src/**/*"],
17
+ "exclude": ["node_modules", "dist"]
18
+ }