@aztec/bot 0.0.0-test.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.
@@ -0,0 +1,23 @@
1
+ import type { ApiSchemaFor } from '@aztec/stdlib/schemas';
2
+
3
+ import { z } from 'zod';
4
+
5
+ import { type BotConfig, BotConfigSchema } from './config.js';
6
+
7
+ export interface BotRunnerApi {
8
+ start(): Promise<void>;
9
+ stop(): Promise<void>;
10
+ run(): Promise<void>;
11
+ setup(): Promise<void>;
12
+ getConfig(): Promise<BotConfig>;
13
+ update(config: BotConfig): Promise<void>;
14
+ }
15
+
16
+ export const BotRunnerApiSchema: ApiSchemaFor<BotRunnerApi> = {
17
+ start: z.function().args().returns(z.void()),
18
+ stop: z.function().args().returns(z.void()),
19
+ run: z.function().args().returns(z.void()),
20
+ setup: z.function().args().returns(z.void()),
21
+ getConfig: z.function().args().returns(BotConfigSchema),
22
+ update: z.function().args(BotConfigSchema).returns(z.void()),
23
+ };
package/src/rpc.ts ADDED
@@ -0,0 +1,21 @@
1
+ import type { ApiHandler } from '@aztec/foundation/json-rpc/server';
2
+ import { createTracedJsonRpcServer } from '@aztec/telemetry-client';
3
+
4
+ import { BotRunnerApiSchema } from './interface.js';
5
+ import type { BotRunner } from './runner.js';
6
+
7
+ /**
8
+ * Wraps a bot runner with a JSON RPC HTTP server.
9
+ * @param botRunner - The BotRunner.
10
+ * @returns An JSON-RPC HTTP server
11
+ */
12
+ export function createBotRunnerRpcServer(botRunner: BotRunner) {
13
+ createTracedJsonRpcServer(botRunner, BotRunnerApiSchema, {
14
+ http200OnError: false,
15
+ healthCheck: botRunner.isHealthy.bind(botRunner),
16
+ });
17
+ }
18
+
19
+ export function getBotRunnerApiHandler(botRunner: BotRunner): ApiHandler {
20
+ return [botRunner, BotRunnerApiSchema, botRunner.isHealthy.bind(botRunner)];
21
+ }
package/src/runner.ts ADDED
@@ -0,0 +1,167 @@
1
+ import { type AztecNode, type PXE, createAztecNodeClient, createLogger } from '@aztec/aztec.js';
2
+ import { RunningPromise } from '@aztec/foundation/running-promise';
3
+ import { type TelemetryClient, type Traceable, type Tracer, makeTracedFetch, trackSpan } from '@aztec/telemetry-client';
4
+
5
+ import { Bot } from './bot.js';
6
+ import { type BotConfig, getVersions } from './config.js';
7
+ import type { BotRunnerApi } from './interface.js';
8
+
9
+ export class BotRunner implements BotRunnerApi, Traceable {
10
+ private log = createLogger('bot');
11
+ private bot?: Promise<Bot>;
12
+ private pxe?: PXE;
13
+ private node: AztecNode;
14
+ private runningPromise: RunningPromise;
15
+ private consecutiveErrors = 0;
16
+ private healthy = true;
17
+
18
+ public readonly tracer: Tracer;
19
+
20
+ public constructor(
21
+ private config: BotConfig,
22
+ dependencies: { pxe?: PXE; node?: AztecNode; telemetry: TelemetryClient },
23
+ ) {
24
+ this.tracer = dependencies.telemetry.getTracer('Bot');
25
+ this.pxe = dependencies.pxe;
26
+ if (!dependencies.node && !config.nodeUrl) {
27
+ throw new Error(`Missing node URL in config or dependencies`);
28
+ }
29
+ this.node =
30
+ dependencies.node ?? createAztecNodeClient(config.nodeUrl!, getVersions(), makeTracedFetch([1, 2, 3], true));
31
+ this.runningPromise = new RunningPromise(() => this.#work(), this.log, config.txIntervalSeconds * 1000);
32
+ }
33
+
34
+ /** Initializes the bot if needed. Blocks until the bot setup is finished. */
35
+ public async setup() {
36
+ if (!this.bot) {
37
+ await this.doSetup();
38
+ }
39
+ }
40
+
41
+ @trackSpan('Bot.setup')
42
+ private async doSetup() {
43
+ this.log.verbose(`Setting up bot`);
44
+ await this.#createBot();
45
+ this.log.info(`Bot set up completed`);
46
+ }
47
+
48
+ /**
49
+ * Initializes the bot if needed and starts sending txs at regular intervals.
50
+ * Blocks until the bot setup is finished.
51
+ */
52
+ public async start() {
53
+ await this.setup();
54
+ if (!this.runningPromise.isRunning()) {
55
+ this.log.info(`Starting bot with interval of ${this.config.txIntervalSeconds}s`);
56
+ this.runningPromise.start();
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Stops sending txs. Returns once all ongoing txs are finished.
62
+ */
63
+ public async stop() {
64
+ if (this.runningPromise.isRunning()) {
65
+ this.log.verbose(`Stopping bot`);
66
+ await this.runningPromise.stop();
67
+ }
68
+ this.log.info(`Stopped bot`);
69
+ }
70
+
71
+ public isHealthy() {
72
+ return this.runningPromise.isRunning() && this.healthy;
73
+ }
74
+
75
+ /** Returns whether the bot is running. */
76
+ public isRunning() {
77
+ return this.runningPromise.isRunning();
78
+ }
79
+
80
+ /**
81
+ * Updates the bot config and recreates the bot. Will stop and restart the bot automatically if it was
82
+ * running when this method was called. Blocks until the new bot is set up.
83
+ */
84
+ public async update(config: BotConfig) {
85
+ this.log.verbose(`Updating bot config`);
86
+ const wasRunning = this.isRunning();
87
+ if (wasRunning) {
88
+ await this.stop();
89
+ }
90
+ this.config = { ...this.config, ...config };
91
+ this.runningPromise.setPollingIntervalMS(this.config.txIntervalSeconds * 1000);
92
+ await this.#createBot();
93
+ this.log.info(`Bot config updated`);
94
+ if (wasRunning) {
95
+ await this.start();
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Triggers a single iteration of the bot. Requires the bot to be initialized.
101
+ * Blocks until the run is finished.
102
+ */
103
+ public async run() {
104
+ if (!this.bot) {
105
+ this.log.error(`Trying to run with uninitialized bot`);
106
+ throw new Error(`Bot is not initialized`);
107
+ }
108
+
109
+ let bot;
110
+ try {
111
+ bot = await this.bot;
112
+ } catch (err) {
113
+ this.log.error(`Error awaiting bot set up: ${err}`);
114
+ throw err;
115
+ }
116
+
117
+ try {
118
+ await bot.run();
119
+ this.consecutiveErrors = 0;
120
+ } catch (err) {
121
+ this.consecutiveErrors += 1;
122
+ this.log.error(`Error running bot consecutiveCount=${this.consecutiveErrors}: ${err}`);
123
+ throw err;
124
+ }
125
+ }
126
+
127
+ /** Returns the current configuration for the bot. */
128
+ public getConfig() {
129
+ return Promise.resolve(this.config);
130
+ }
131
+
132
+ async #createBot() {
133
+ try {
134
+ this.bot = Bot.create(this.config, { pxe: this.pxe, node: this.node });
135
+ await this.bot;
136
+ } catch (err) {
137
+ this.log.error(`Error setting up bot: ${err}`);
138
+ throw err;
139
+ }
140
+ }
141
+
142
+ @trackSpan('Bot.work')
143
+ async #work() {
144
+ if (this.config.maxPendingTxs > 0) {
145
+ const pendingTxs = await this.node.getPendingTxs();
146
+ if (pendingTxs.length >= this.config.maxPendingTxs) {
147
+ this.log.verbose(`Not sending bot tx since node has ${pendingTxs.length} pending txs`);
148
+ return;
149
+ }
150
+ }
151
+
152
+ try {
153
+ await this.run();
154
+ } catch (err) {
155
+ // Already logged in run()
156
+ if (this.config.maxConsecutiveErrors > 0 && this.consecutiveErrors >= this.config.maxConsecutiveErrors) {
157
+ this.log.error(`Too many errors bot is unhealthy`);
158
+ this.healthy = false;
159
+ }
160
+ }
161
+
162
+ if (!this.healthy && this.config.stopWhenUnhealthy) {
163
+ this.log.fatal(`Stopping bot due to errors`);
164
+ process.exit(1); // workaround docker not restarting the container if its unhealthy. We have to exit instead
165
+ }
166
+ }
167
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,27 @@
1
+ import type { EasyPrivateTokenContract } from '@aztec/noir-contracts.js/EasyPrivateToken';
2
+ import type { TokenContract } from '@aztec/noir-contracts.js/Token';
3
+ import type { AztecAddress } from '@aztec/stdlib/aztec-address';
4
+
5
+ /**
6
+ * Gets the private and public balance of the given token for the given address.
7
+ * @param token - Token contract.
8
+ * @param who - Address to get the balance for.
9
+ * @returns - Private and public token balances as bigints.
10
+ */
11
+ export async function getBalances(
12
+ token: TokenContract,
13
+ who: AztecAddress,
14
+ ): Promise<{ privateBalance: bigint; publicBalance: bigint }> {
15
+ const privateBalance = await token.methods.balance_of_private(who).simulate();
16
+ const publicBalance = await token.methods.balance_of_public(who).simulate();
17
+ return { privateBalance, publicBalance };
18
+ }
19
+
20
+ export async function getPrivateBalance(token: EasyPrivateTokenContract, who: AztecAddress): Promise<bigint> {
21
+ const privateBalance = await token.methods.get_balance(who).simulate();
22
+ return privateBalance;
23
+ }
24
+
25
+ export function isStandardTokenContract(token: TokenContract | EasyPrivateTokenContract): token is TokenContract {
26
+ return 'mint_to_public' in token.methods;
27
+ }