Repository navigation
Expand file tree
/
Copy pathturn.ts
More file actions
140 lines (130 loc) · 4.83 KB
/
Copy pathturn.ts
File metadata and controls
140 lines (130 loc) · 4.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import type { ccc } from "@ckb-ccc/core";
import { projectAccountAvailability } from "../../../src/conversion/projection.ts";
import { BOT_LOCK_UP } from "../../../src/dao.ts";
import { TransactionBroadcastError } from "../../../src/send/sign_and_send_transaction.ts";
import { waitTransaction } from "../../../src/send/wait_transaction.ts";
import { ckbFromUdt, ICKB_DEPOSIT_CAP } from "../../../src/udt.ts";
import type { BotEventEmitter } from "./events.ts";
import { summarizeBotState } from "./support.ts";
import { buildTransaction } from "./transaction.ts";
import type { BotState, BuildTransactionResult, Runtime } from "./types.ts";
type BuiltTransactionResult = Extract<BuildTransactionResult, { kind: "built" }>;
export interface BotTurnContext {
/** Event emitter scoped to this bot run. */
events: BotEventEmitter;
/** Runtime clients, signer, SDK, managers, and primary lock. */
runtime: Runtime;
}
// The wait only lets this turn journal its own commit: the next turn rebuilds from committed
// state. Two minutes covers every commit journalled so far (77 s at most, 54 s at p90). A
// longer wait would only delay the exit after a stuck send; the ten-minute restart, not this
// wait, now dominates how long a failed withdrawal request waits to be rebuilt.
export const BOT_TRANSACTION_WAIT_TIMEOUT_MS = 120_000;
const BOT_TRANSACTION_WAIT_INTERVAL_MS = 10_000;
/**
* Reads bot-owned account state and public market state for one planning attempt.
*
* @remarks The bot places no orders, so nothing here counts or collects any; the market
* side of the state is every order past par. The fee rate is the sampled one, capped by
* the state read at the runtime's `maxFeeRate` when set: the sampled rate is a node
* statistic nothing can verify, so the cap bounds what a node can make the bot pay and
* what it can make the match gate refuse.
*/
export async function readBotState(runtime: Runtime): Promise<BotState> {
const { system, account } = await runtime.sdk.getL1AccountState(
runtime.client,
runtime.accountLocks,
BOT_LOCK_UP,
runtime.maxFeeRate,
);
const projection = projectAccountAvailability(account, { available: [], pending: [] });
return {
system,
receipts: account.receipts,
readyWithdrawals: projection.readyWithdrawals,
notReadyWithdrawals: projection.pendingWithdrawals,
cells: [...account.capacityCells, ...account.nativeUdtCells],
ckb: projection.ckbAvailable,
ickb: projection.ickbAvailable,
pendingCkb: projection.ckbPending,
depositCapacity: ckbFromUdt(ICKB_DEPOSIT_CAP, system.exchangeRatio),
};
}
/**
* Runs one bot turn: read state, decide, and at most one broadcast with its confirmation
* wait. Any failure propagates to the entry point, which journals it and exits 1.
*/
export async function runBotTurn(context: BotTurnContext): Promise<void> {
const state = await readBotState(context.runtime);
const summary = summarizeBotState(state);
context.events.emit({ type: "bot.state.read", ...summary });
const result = await buildTransaction(context.runtime, state);
if (result.kind === "skipped") {
context.events.emit({
type: "bot.decision.skipped",
reason: result.reason,
decision: result.decision,
});
return;
}
context.events.emit({ type: "bot.transaction.built", decision: result.decision });
await sendBuiltTransaction({ context, result });
}
async function sendBuiltTransaction({
context,
result,
}: {
context: BotTurnContext;
result: BuiltTransactionResult;
}): Promise<void> {
const startedAt = Date.now();
let txHash: ccc.Hex;
try {
txHash = await context.runtime.sendTransaction(
result.tx,
result.plannedFee,
result.broadcastBefore,
);
context.events.emit({
type: "bot.transaction.sent",
txHash,
outcome: "broadcasted",
elapsedMs: Date.now() - startedAt,
});
} catch (error) {
if (!(error instanceof TransactionBroadcastError)) {
throw error;
}
// The hash is known, so the node may have the transaction: wait for it.
txHash = error.txHash;
context.events.emit({
type: "bot.transaction.sent",
txHash,
outcome: "broadcast_ambiguous",
elapsedMs: Date.now() - startedAt,
error,
});
}
await confirmTransaction({ context, txHash, startedAt });
}
async function confirmTransaction({
context,
txHash,
startedAt,
}: {
context: BotTurnContext;
txHash: ccc.Hex;
startedAt: number;
}): Promise<void> {
// A terminal status or the timeout throws an SDK error that names the hash and status;
// it ends the turn through `bot.turn.failed`.
await waitTransaction(context.runtime.client, txHash, {
timeout: BOT_TRANSACTION_WAIT_TIMEOUT_MS,
interval: BOT_TRANSACTION_WAIT_INTERVAL_MS,
});
context.events.emit({
type: "bot.transaction.committed",
txHash,
elapsedMs: Date.now() - startedAt,
});
}