@aztec/bot 0.0.1-fake-ceab37513c → 0.0.2-commit.217f559981
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/dest/amm_bot.d.ts +9 -10
- package/dest/amm_bot.d.ts.map +1 -1
- package/dest/amm_bot.js +27 -20
- package/dest/base_bot.d.ts +12 -9
- package/dest/base_bot.d.ts.map +1 -1
- package/dest/base_bot.js +28 -29
- package/dest/bot.d.ts +9 -10
- package/dest/bot.d.ts.map +1 -1
- package/dest/bot.js +12 -10
- package/dest/config.d.ts +82 -63
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +61 -31
- package/dest/cross_chain_bot.d.ts +54 -0
- package/dest/cross_chain_bot.d.ts.map +1 -0
- package/dest/cross_chain_bot.js +140 -0
- package/dest/factory.d.ts +27 -34
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +271 -151
- package/dest/index.d.ts +3 -1
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +2 -0
- package/dest/interface.d.ts +2 -2
- package/dest/interface.d.ts.map +1 -1
- package/dest/interface.js +1 -1
- package/dest/l1_to_l2_seeding.d.ts +8 -0
- package/dest/l1_to_l2_seeding.d.ts.map +1 -0
- package/dest/l1_to_l2_seeding.js +63 -0
- package/dest/rpc.d.ts +1 -1
- package/dest/runner.d.ts +12 -13
- package/dest/runner.d.ts.map +1 -1
- package/dest/runner.js +445 -61
- package/dest/store/bot_store.d.ts +69 -0
- package/dest/store/bot_store.d.ts.map +1 -0
- package/dest/store/bot_store.js +138 -0
- package/dest/store/index.d.ts +2 -0
- package/dest/store/index.d.ts.map +1 -0
- package/dest/store/index.js +1 -0
- package/dest/utils.d.ts +4 -4
- package/dest/utils.d.ts.map +1 -1
- package/dest/utils.js +5 -5
- package/package.json +19 -13
- package/src/amm_bot.ts +40 -32
- package/src/base_bot.ts +27 -39
- package/src/bot.ts +25 -13
- package/src/config.ts +101 -72
- package/src/cross_chain_bot.ts +209 -0
- package/src/factory.ts +313 -177
- package/src/index.ts +2 -0
- package/src/interface.ts +1 -1
- package/src/l1_to_l2_seeding.ts +79 -0
- package/src/runner.ts +33 -24
- package/src/store/bot_store.ts +196 -0
- package/src/store/index.ts +1 -0
- package/src/utils.ts +10 -5
package/dest/factory.js
CHANGED
|
@@ -1,216 +1,293 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { BatchCall,
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { getInitialTestAccountsData } from '@aztec/accounts/testing';
|
|
2
|
+
import { AztecAddress } from '@aztec/aztec.js/addresses';
|
|
3
|
+
import { BatchCall, NO_WAIT } from '@aztec/aztec.js/contracts';
|
|
4
|
+
import { L1FeeJuicePortalManager } from '@aztec/aztec.js/ethereum';
|
|
5
|
+
import { FeeJuicePaymentMethodWithClaim } from '@aztec/aztec.js/fee';
|
|
6
|
+
import { deriveKeys } from '@aztec/aztec.js/keys';
|
|
7
|
+
import { createLogger } from '@aztec/aztec.js/log';
|
|
8
|
+
import { waitForL1ToL2MessageReady } from '@aztec/aztec.js/messaging';
|
|
9
|
+
import { waitForTx } from '@aztec/aztec.js/node';
|
|
10
|
+
import { createEthereumChain } from '@aztec/ethereum/chain';
|
|
11
|
+
import { createExtendedL1Client } from '@aztec/ethereum/client';
|
|
12
|
+
import { RollupContract } from '@aztec/ethereum/contracts';
|
|
13
|
+
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
14
|
+
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
6
15
|
import { Timer } from '@aztec/foundation/timer';
|
|
7
16
|
import { AMMContract } from '@aztec/noir-contracts.js/AMM';
|
|
8
17
|
import { PrivateTokenContract } from '@aztec/noir-contracts.js/PrivateToken';
|
|
9
18
|
import { TokenContract } from '@aztec/noir-contracts.js/Token';
|
|
19
|
+
import { TestContract } from '@aztec/noir-test-contracts.js/Test';
|
|
20
|
+
import { GasSettings } from '@aztec/stdlib/gas';
|
|
10
21
|
import { deriveSigningKey } from '@aztec/stdlib/keys';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
22
|
+
import { SupportedTokenContracts } from './config.js';
|
|
23
|
+
import { seedL1ToL2Message } from './l1_to_l2_seeding.js';
|
|
13
24
|
import { getBalances, getPrivateBalance, isStandardTokenContract } from './utils.js';
|
|
14
25
|
const MINT_BALANCE = 1e12;
|
|
15
26
|
const MIN_BALANCE = 1e3;
|
|
16
27
|
export class BotFactory {
|
|
17
28
|
config;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
29
|
+
wallet;
|
|
30
|
+
store;
|
|
31
|
+
aztecNode;
|
|
32
|
+
aztecNodeAdmin;
|
|
21
33
|
log;
|
|
22
|
-
constructor(config,
|
|
34
|
+
constructor(config, wallet, store, aztecNode, aztecNodeAdmin){
|
|
23
35
|
this.config = config;
|
|
36
|
+
this.wallet = wallet;
|
|
37
|
+
this.store = store;
|
|
38
|
+
this.aztecNode = aztecNode;
|
|
39
|
+
this.aztecNodeAdmin = aztecNodeAdmin;
|
|
24
40
|
this.log = createLogger('bot');
|
|
25
|
-
if (config.flushSetupTransactions && !dependencies.nodeAdmin) {
|
|
26
|
-
throw new Error(`Either a node admin client or node admin url must be provided if transaction flushing is requested`);
|
|
27
|
-
}
|
|
28
|
-
if (config.senderPrivateKey && config.senderPrivateKey.getValue() && !dependencies.node) {
|
|
29
|
-
throw new Error(`Either a node client or node url must be provided for bridging L1 fee juice to deploy an account with private key`);
|
|
30
|
-
}
|
|
31
|
-
if (!dependencies.pxe && !config.pxeUrl) {
|
|
32
|
-
throw new Error(`Either a PXE client or a PXE URL must be provided`);
|
|
33
|
-
}
|
|
34
|
-
this.node = dependencies.node;
|
|
35
|
-
this.nodeAdmin = dependencies.nodeAdmin;
|
|
36
|
-
if (dependencies.pxe) {
|
|
37
|
-
this.log.info(`Using local PXE`);
|
|
38
|
-
this.pxe = dependencies.pxe;
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
this.log.info(`Using remote PXE at ${config.pxeUrl}`);
|
|
42
|
-
this.pxe = createPXEClient(config.pxeUrl, getVersions(), makeTracedFetch([
|
|
43
|
-
1,
|
|
44
|
-
2,
|
|
45
|
-
3
|
|
46
|
-
], false));
|
|
47
41
|
}
|
|
48
42
|
/**
|
|
49
43
|
* Initializes a new bot by setting up the sender account, registering the recipient,
|
|
50
44
|
* deploying the token contract, and minting tokens if necessary.
|
|
51
45
|
*/ async setup() {
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
const token = await this.setupToken(wallet, defaultAccountAddress);
|
|
46
|
+
const defaultAccountAddress = await this.setupAccount();
|
|
47
|
+
const recipient = (await this.wallet.createSchnorrAccount(Fr.random(), Fr.random())).address;
|
|
48
|
+
const token = await this.setupToken(defaultAccountAddress);
|
|
56
49
|
await this.mintTokens(token, defaultAccountAddress);
|
|
57
50
|
return {
|
|
58
|
-
wallet,
|
|
51
|
+
wallet: this.wallet,
|
|
59
52
|
defaultAccountAddress,
|
|
60
53
|
token,
|
|
61
|
-
|
|
54
|
+
node: this.aztecNode,
|
|
62
55
|
recipient
|
|
63
56
|
};
|
|
64
57
|
}
|
|
65
58
|
async setupAmm() {
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
await this.fundAmm(wallet, wallet.getAddress(), amm, token0, token1, liquidityToken);
|
|
59
|
+
const defaultAccountAddress = await this.setupAccount();
|
|
60
|
+
const token0 = await this.setupTokenContract(defaultAccountAddress, this.config.tokenSalt, 'BotToken0', 'BOT0');
|
|
61
|
+
const token1 = await this.setupTokenContract(defaultAccountAddress, this.config.tokenSalt, 'BotToken1', 'BOT1');
|
|
62
|
+
const liquidityToken = await this.setupTokenContract(defaultAccountAddress, this.config.tokenSalt, 'BotLPToken', 'BOTLP');
|
|
63
|
+
const amm = await this.setupAmmContract(defaultAccountAddress, this.config.tokenSalt, token0, token1, liquidityToken);
|
|
64
|
+
await this.fundAmm(defaultAccountAddress, defaultAccountAddress, amm, token0, token1, liquidityToken);
|
|
73
65
|
this.log.info(`AMM initialized and funded`);
|
|
74
66
|
return {
|
|
75
|
-
wallet,
|
|
67
|
+
wallet: this.wallet,
|
|
76
68
|
defaultAccountAddress,
|
|
77
69
|
amm,
|
|
78
70
|
token0,
|
|
79
71
|
token1,
|
|
80
|
-
|
|
72
|
+
node: this.aztecNode
|
|
81
73
|
};
|
|
82
74
|
}
|
|
83
75
|
/**
|
|
76
|
+
* Initializes the cross-chain bot by deploying TestContract, creating an L1 client,
|
|
77
|
+
* seeding initial L1→L2 messages, and waiting for the first to be ready.
|
|
78
|
+
*/ async setupCrossChain() {
|
|
79
|
+
const defaultAccountAddress = await this.setupAccount();
|
|
80
|
+
// Create L1 client (same pattern as bridgeL1FeeJuice)
|
|
81
|
+
const l1RpcUrls = this.config.l1RpcUrls;
|
|
82
|
+
if (!l1RpcUrls?.length) {
|
|
83
|
+
throw new Error('L1 RPC URLs required for cross-chain bot');
|
|
84
|
+
}
|
|
85
|
+
const mnemonicOrPrivateKey = this.config.l1PrivateKey?.getValue() ?? this.config.l1Mnemonic?.getValue();
|
|
86
|
+
if (!mnemonicOrPrivateKey) {
|
|
87
|
+
throw new Error('L1 mnemonic or private key required for cross-chain bot');
|
|
88
|
+
}
|
|
89
|
+
const { l1ChainId, l1ContractAddresses } = await this.aztecNode.getNodeInfo();
|
|
90
|
+
const chain = createEthereumChain(l1RpcUrls, l1ChainId);
|
|
91
|
+
const l1Client = createExtendedL1Client(chain.rpcUrls, mnemonicOrPrivateKey, chain.chainInfo);
|
|
92
|
+
// Fetch Rollup version (needed for Inbox L2Actor struct)
|
|
93
|
+
const rollupContract = new RollupContract(l1Client, l1ContractAddresses.rollupAddress.toString());
|
|
94
|
+
const rollupVersion = await rollupContract.getVersion();
|
|
95
|
+
// Deploy TestContract
|
|
96
|
+
const contract = await this.setupTestContract(defaultAccountAddress);
|
|
97
|
+
// Recover any pending messages from store (clean up stale ones first)
|
|
98
|
+
await this.store.cleanupOldPendingMessages();
|
|
99
|
+
const pendingMessages = await this.store.getUnconsumedL1ToL2Messages();
|
|
100
|
+
// Seed initial L1→L2 messages if pipeline is empty
|
|
101
|
+
const seedCount = Math.max(0, this.config.l1ToL2SeedCount - pendingMessages.length);
|
|
102
|
+
for(let i = 0; i < seedCount; i++){
|
|
103
|
+
await seedL1ToL2Message(l1Client, EthAddress.fromString(l1ContractAddresses.inboxAddress.toString()), contract.address, rollupVersion, this.store, this.log);
|
|
104
|
+
}
|
|
105
|
+
// Block until at least one message is ready
|
|
106
|
+
const allMessages = await this.store.getUnconsumedL1ToL2Messages();
|
|
107
|
+
if (allMessages.length > 0) {
|
|
108
|
+
this.log.info(`Waiting for first L1→L2 message to be ready...`);
|
|
109
|
+
const firstMsg = allMessages[0];
|
|
110
|
+
await waitForL1ToL2MessageReady(this.aztecNode, Fr.fromHexString(firstMsg.msgHash), {
|
|
111
|
+
timeoutSeconds: this.config.l1ToL2MessageTimeoutSeconds,
|
|
112
|
+
// Use forPublicConsumption: false so we wait until the message is in the current world
|
|
113
|
+
// state. With true, it returns one block early which causes gas estimation simulation to
|
|
114
|
+
// fail since it runs against the current state.
|
|
115
|
+
// See https://linear.app/aztec-labs/issue/A-548 for details.
|
|
116
|
+
forPublicConsumption: false
|
|
117
|
+
});
|
|
118
|
+
this.log.info(`First L1→L2 message is ready`);
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
wallet: this.wallet,
|
|
122
|
+
defaultAccountAddress,
|
|
123
|
+
contract,
|
|
124
|
+
node: this.aztecNode,
|
|
125
|
+
l1Client,
|
|
126
|
+
rollupVersion
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
async setupTestContract(deployer) {
|
|
130
|
+
const deployOpts = {
|
|
131
|
+
from: deployer,
|
|
132
|
+
contractAddressSalt: this.config.tokenSalt,
|
|
133
|
+
universalDeploy: true
|
|
134
|
+
};
|
|
135
|
+
const deploy = TestContract.deploy(this.wallet);
|
|
136
|
+
const instance = await this.registerOrDeployContract('TestContract', deploy, deployOpts);
|
|
137
|
+
return TestContract.at(instance.address, this.wallet);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
84
140
|
* Checks if the sender account contract is initialized, and initializes it if necessary.
|
|
85
141
|
* @returns The sender wallet.
|
|
86
142
|
*/ async setupAccount() {
|
|
87
143
|
const privateKey = this.config.senderPrivateKey?.getValue();
|
|
88
144
|
if (privateKey) {
|
|
145
|
+
this.log.info(`Setting up account with provided private key`);
|
|
89
146
|
return await this.setupAccountWithPrivateKey(privateKey);
|
|
90
147
|
} else {
|
|
148
|
+
this.log.info(`Setting up test account`);
|
|
91
149
|
return await this.setupTestAccount();
|
|
92
150
|
}
|
|
93
151
|
}
|
|
94
|
-
async setupAccountWithPrivateKey(
|
|
152
|
+
async setupAccountWithPrivateKey(secret) {
|
|
95
153
|
const salt = this.config.senderSalt ?? Fr.ONE;
|
|
96
|
-
const signingKey = deriveSigningKey(
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
if (
|
|
100
|
-
this.log.info(`Account at ${
|
|
154
|
+
const signingKey = deriveSigningKey(secret);
|
|
155
|
+
const accountManager = await this.wallet.createSchnorrAccount(secret, salt, signingKey);
|
|
156
|
+
const metadata = await this.wallet.getContractMetadata(accountManager.address);
|
|
157
|
+
if (metadata.isContractInitialized) {
|
|
158
|
+
this.log.info(`Account at ${accountManager.address.toString()} already initialized`);
|
|
101
159
|
const timer = new Timer();
|
|
102
|
-
const
|
|
103
|
-
this.log.info(`Account at ${
|
|
104
|
-
|
|
160
|
+
const address = accountManager.address;
|
|
161
|
+
this.log.info(`Account at ${address} registered. duration=${timer.ms()}`);
|
|
162
|
+
await this.store.deleteBridgeClaim(address);
|
|
163
|
+
return address;
|
|
105
164
|
} else {
|
|
106
|
-
const address =
|
|
165
|
+
const address = accountManager.address;
|
|
107
166
|
this.log.info(`Deploying account at ${address}`);
|
|
108
|
-
const claim = await this.
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
paymentMethod
|
|
115
|
-
}
|
|
167
|
+
const claim = await this.getOrCreateBridgeClaim(address);
|
|
168
|
+
const paymentMethod = new FeeJuicePaymentMethodWithClaim(accountManager.address, claim);
|
|
169
|
+
const deployMethod = await accountManager.getDeployMethod();
|
|
170
|
+
const maxFeesPerGas = (await this.aztecNode.getCurrentMinFees()).mul(1 + this.config.minFeePadding);
|
|
171
|
+
const gasSettings = GasSettings.default({
|
|
172
|
+
maxFeesPerGas
|
|
116
173
|
});
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
174
|
+
await this.withNoMinTxsPerBlock(async ()=>{
|
|
175
|
+
const txHash = await deployMethod.send({
|
|
176
|
+
from: AztecAddress.ZERO,
|
|
177
|
+
fee: {
|
|
178
|
+
gasSettings,
|
|
179
|
+
paymentMethod
|
|
180
|
+
},
|
|
181
|
+
wait: NO_WAIT
|
|
182
|
+
});
|
|
183
|
+
this.log.info(`Sent tx for account deployment with hash ${txHash.toString()}`);
|
|
184
|
+
return waitForTx(this.aztecNode, txHash, {
|
|
121
185
|
timeout: this.config.txMinedWaitSeconds
|
|
122
|
-
})
|
|
186
|
+
});
|
|
187
|
+
});
|
|
123
188
|
this.log.info(`Account deployed at ${address}`);
|
|
124
|
-
|
|
189
|
+
// Clean up the consumed bridge claim
|
|
190
|
+
await this.store.deleteBridgeClaim(address);
|
|
191
|
+
return accountManager.address;
|
|
125
192
|
}
|
|
126
193
|
}
|
|
127
194
|
async setupTestAccount() {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
} else {
|
|
132
|
-
this.log.info('Registering funded test account');
|
|
133
|
-
const [account] = await getInitialTestAccounts();
|
|
134
|
-
const manager = await getSchnorrAccount(this.pxe, account.secret, account.signingKey, account.salt);
|
|
135
|
-
wallet = await manager.register();
|
|
136
|
-
this.log.info(`Funded test account registered: ${wallet.getAddress()}`);
|
|
137
|
-
}
|
|
138
|
-
return wallet;
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Registers the recipient for txs in the pxe.
|
|
142
|
-
*/ async registerRecipient() {
|
|
143
|
-
const recipient = await this.pxe.registerAccount(this.config.recipientEncryptionSecret.getValue(), Fr.ONE);
|
|
144
|
-
return recipient.address;
|
|
195
|
+
const [initialAccountData] = await getInitialTestAccountsData();
|
|
196
|
+
const accountManager = await this.wallet.createSchnorrAccount(initialAccountData.secret, initialAccountData.salt, initialAccountData.signingKey);
|
|
197
|
+
return accountManager.address;
|
|
145
198
|
}
|
|
146
199
|
/**
|
|
147
200
|
* Checks if the token contract is deployed and deploys it if necessary.
|
|
148
201
|
* @param wallet - Wallet to deploy the token contract from.
|
|
149
202
|
* @returns The TokenContract instance.
|
|
150
|
-
*/ async setupToken(
|
|
203
|
+
*/ async setupToken(sender) {
|
|
151
204
|
let deploy;
|
|
205
|
+
let tokenInstance;
|
|
152
206
|
const deployOpts = {
|
|
153
207
|
from: sender,
|
|
154
208
|
contractAddressSalt: this.config.tokenSalt,
|
|
155
209
|
universalDeploy: true
|
|
156
210
|
};
|
|
211
|
+
let token;
|
|
157
212
|
if (this.config.contract === SupportedTokenContracts.TokenContract) {
|
|
158
|
-
deploy = TokenContract.deploy(wallet, sender, 'BotToken', 'BOT', 18);
|
|
213
|
+
deploy = TokenContract.deploy(this.wallet, sender, 'BotToken', 'BOT', 18);
|
|
214
|
+
tokenInstance = await deploy.getInstance(deployOpts);
|
|
215
|
+
token = TokenContract.at(tokenInstance.address, this.wallet);
|
|
159
216
|
} else if (this.config.contract === SupportedTokenContracts.PrivateTokenContract) {
|
|
160
|
-
|
|
217
|
+
// Generate keys for the contract since PrivateToken uses SinglePrivateMutable which requires keys
|
|
218
|
+
const tokenSecretKey = Fr.random();
|
|
219
|
+
const tokenPublicKeys = (await deriveKeys(tokenSecretKey)).publicKeys;
|
|
220
|
+
deploy = PrivateTokenContract.deployWithPublicKeys(tokenPublicKeys, this.wallet, MINT_BALANCE, sender);
|
|
161
221
|
deployOpts.skipInstancePublication = true;
|
|
162
222
|
deployOpts.skipClassPublication = true;
|
|
163
223
|
deployOpts.skipInitialization = false;
|
|
224
|
+
// Register the contract with the secret key before deployment
|
|
225
|
+
tokenInstance = await deploy.getInstance(deployOpts);
|
|
226
|
+
token = PrivateTokenContract.at(tokenInstance.address, this.wallet);
|
|
227
|
+
await this.wallet.registerContract(tokenInstance, PrivateTokenContract.artifact, tokenSecretKey);
|
|
228
|
+
// The contract constructor initializes private storage vars that need the contract's own nullifier key.
|
|
229
|
+
deployOpts.additionalScopes = [
|
|
230
|
+
tokenInstance.address
|
|
231
|
+
];
|
|
164
232
|
} else {
|
|
165
233
|
throw new Error(`Unsupported token contract type: ${this.config.contract}`);
|
|
166
234
|
}
|
|
167
|
-
const address = (await deploy.getInstance(deployOpts)).address;
|
|
168
|
-
|
|
235
|
+
const address = tokenInstance?.address ?? (await deploy.getInstance(deployOpts)).address;
|
|
236
|
+
const metadata = await this.wallet.getContractMetadata(address);
|
|
237
|
+
if (metadata.isContractPublished) {
|
|
169
238
|
this.log.info(`Token at ${address.toString()} already deployed`);
|
|
170
|
-
|
|
239
|
+
await deploy.register();
|
|
171
240
|
} else {
|
|
172
241
|
this.log.info(`Deploying token contract at ${address.toString()}`);
|
|
173
|
-
const
|
|
174
|
-
|
|
242
|
+
const txHash = await deploy.send({
|
|
243
|
+
...deployOpts,
|
|
244
|
+
wait: NO_WAIT
|
|
245
|
+
});
|
|
175
246
|
this.log.info(`Sent tx for token setup with hash ${txHash.toString()}`);
|
|
176
|
-
|
|
247
|
+
await this.withNoMinTxsPerBlock(async ()=>{
|
|
248
|
+
await waitForTx(this.aztecNode, txHash, {
|
|
177
249
|
timeout: this.config.txMinedWaitSeconds
|
|
178
|
-
})
|
|
250
|
+
});
|
|
251
|
+
return token;
|
|
252
|
+
});
|
|
179
253
|
}
|
|
254
|
+
return token;
|
|
180
255
|
}
|
|
181
256
|
/**
|
|
182
257
|
* Checks if the token contract is deployed and deploys it if necessary.
|
|
183
258
|
* @param wallet - Wallet to deploy the token contract from.
|
|
184
259
|
* @returns The TokenContract instance.
|
|
185
|
-
*/ setupTokenContract(
|
|
260
|
+
*/ async setupTokenContract(deployer, contractAddressSalt, name, ticker, decimals = 18) {
|
|
186
261
|
const deployOpts = {
|
|
187
262
|
from: deployer,
|
|
188
263
|
contractAddressSalt,
|
|
189
264
|
universalDeploy: true
|
|
190
265
|
};
|
|
191
|
-
const deploy = TokenContract.deploy(wallet, deployer, name, ticker, decimals);
|
|
192
|
-
|
|
266
|
+
const deploy = TokenContract.deploy(this.wallet, deployer, name, ticker, decimals);
|
|
267
|
+
const instance = await this.registerOrDeployContract('Token - ' + name, deploy, deployOpts);
|
|
268
|
+
return TokenContract.at(instance.address, this.wallet);
|
|
193
269
|
}
|
|
194
|
-
async setupAmmContract(
|
|
270
|
+
async setupAmmContract(deployer, contractAddressSalt, token0, token1, lpToken) {
|
|
195
271
|
const deployOpts = {
|
|
196
272
|
from: deployer,
|
|
197
273
|
contractAddressSalt,
|
|
198
274
|
universalDeploy: true
|
|
199
275
|
};
|
|
200
|
-
const deploy = AMMContract.deploy(wallet, token0.address, token1.address, lpToken.address);
|
|
201
|
-
const
|
|
276
|
+
const deploy = AMMContract.deploy(this.wallet, token0.address, token1.address, lpToken.address);
|
|
277
|
+
const instance = await this.registerOrDeployContract('AMM', deploy, deployOpts);
|
|
278
|
+
const amm = AMMContract.at(instance.address, this.wallet);
|
|
202
279
|
this.log.info(`AMM deployed at ${amm.address}`);
|
|
203
|
-
const
|
|
204
|
-
from: deployer
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
timeout: this.config.txMinedWaitSeconds
|
|
280
|
+
const minterReceipt = await lpToken.methods.set_minter(amm.address, true).send({
|
|
281
|
+
from: deployer,
|
|
282
|
+
wait: {
|
|
283
|
+
timeout: this.config.txMinedWaitSeconds
|
|
284
|
+
}
|
|
209
285
|
});
|
|
286
|
+
this.log.info(`Set LP token minter to AMM txHash=${minterReceipt.txHash.toString()}`);
|
|
210
287
|
this.log.info(`Liquidity token initialized`);
|
|
211
288
|
return amm;
|
|
212
289
|
}
|
|
213
|
-
async fundAmm(
|
|
290
|
+
async fundAmm(defaultAccountAddress, liquidityProvider, amm, token0, token1, lpToken) {
|
|
214
291
|
const getPrivateBalances = ()=>Promise.all([
|
|
215
292
|
token0.methods.balance_of_private(liquidityProvider).simulate({
|
|
216
293
|
from: liquidityProvider
|
|
@@ -231,53 +308,60 @@ export class BotFactory {
|
|
|
231
308
|
const [t0Bal, t1Bal, lpBal] = await getPrivateBalances();
|
|
232
309
|
this.log.info(`Minting ${MINT_BALANCE} tokens of each BotToken0 and BotToken1. Current private balances of ${liquidityProvider}: token0=${t0Bal}, token1=${t1Bal}, lp=${lpBal}`);
|
|
233
310
|
// Add authwitnesses for the transfers in AMM::add_liquidity function
|
|
234
|
-
const token0Authwit = await wallet.createAuthWit({
|
|
311
|
+
const token0Authwit = await this.wallet.createAuthWit(defaultAccountAddress, {
|
|
235
312
|
caller: amm.address,
|
|
236
|
-
|
|
313
|
+
call: await token0.methods.transfer_to_public_and_prepare_private_balance_increase(liquidityProvider, amm.address, amount0Max, authwitNonce).getFunctionCall()
|
|
237
314
|
});
|
|
238
|
-
const token1Authwit = await wallet.createAuthWit({
|
|
315
|
+
const token1Authwit = await this.wallet.createAuthWit(defaultAccountAddress, {
|
|
239
316
|
caller: amm.address,
|
|
240
|
-
|
|
317
|
+
call: await token1.methods.transfer_to_public_and_prepare_private_balance_increase(liquidityProvider, amm.address, amount1Max, authwitNonce).getFunctionCall()
|
|
241
318
|
});
|
|
242
|
-
const
|
|
319
|
+
const mintReceipt = await new BatchCall(this.wallet, [
|
|
243
320
|
token0.methods.mint_to_private(liquidityProvider, MINT_BALANCE),
|
|
244
321
|
token1.methods.mint_to_private(liquidityProvider, MINT_BALANCE)
|
|
245
322
|
]).send({
|
|
246
|
-
from: liquidityProvider
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
timeout: this.config.txMinedWaitSeconds
|
|
323
|
+
from: liquidityProvider,
|
|
324
|
+
wait: {
|
|
325
|
+
timeout: this.config.txMinedWaitSeconds
|
|
326
|
+
}
|
|
251
327
|
});
|
|
252
|
-
|
|
328
|
+
this.log.info(`Sent mint tx: ${mintReceipt.txHash.toString()}`);
|
|
329
|
+
const addLiquidityReceipt = await amm.methods.add_liquidity(amount0Max, amount1Max, amount0Min, amount1Min, authwitNonce).send({
|
|
253
330
|
from: liquidityProvider,
|
|
254
331
|
authWitnesses: [
|
|
255
332
|
token0Authwit,
|
|
256
333
|
token1Authwit
|
|
257
|
-
]
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
timeout: this.config.txMinedWaitSeconds
|
|
334
|
+
],
|
|
335
|
+
wait: {
|
|
336
|
+
timeout: this.config.txMinedWaitSeconds
|
|
337
|
+
}
|
|
262
338
|
});
|
|
339
|
+
this.log.info(`Sent tx to add liquidity to the AMM: ${addLiquidityReceipt.txHash.toString()}`);
|
|
263
340
|
this.log.info(`Liquidity added`);
|
|
264
341
|
const [newT0Bal, newT1Bal, newLPBal] = await getPrivateBalances();
|
|
265
|
-
this.log.info(`Updated private balances of ${
|
|
342
|
+
this.log.info(`Updated private balances of ${defaultAccountAddress} after minting and funding AMM: token0=${newT0Bal}, token1=${newT1Bal}, lp=${newLPBal}`);
|
|
266
343
|
}
|
|
267
344
|
async registerOrDeployContract(name, deploy, deployOpts) {
|
|
268
|
-
const
|
|
269
|
-
|
|
345
|
+
const instance = await deploy.getInstance(deployOpts);
|
|
346
|
+
const address = instance.address;
|
|
347
|
+
const metadata = await this.wallet.getContractMetadata(address);
|
|
348
|
+
if (metadata.isContractPublished) {
|
|
270
349
|
this.log.info(`Contract ${name} at ${address.toString()} already deployed`);
|
|
271
|
-
|
|
350
|
+
await deploy.register();
|
|
272
351
|
} else {
|
|
273
352
|
this.log.info(`Deploying contract ${name} at ${address.toString()}`);
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
353
|
+
await this.withNoMinTxsPerBlock(async ()=>{
|
|
354
|
+
const txHash = await deploy.send({
|
|
355
|
+
...deployOpts,
|
|
356
|
+
wait: NO_WAIT
|
|
357
|
+
});
|
|
358
|
+
this.log.info(`Sent contract ${name} setup tx with hash ${txHash.toString()}`);
|
|
359
|
+
return waitForTx(this.aztecNode, txHash, {
|
|
278
360
|
timeout: this.config.txMinedWaitSeconds
|
|
279
|
-
})
|
|
361
|
+
});
|
|
362
|
+
});
|
|
280
363
|
}
|
|
364
|
+
return instance;
|
|
281
365
|
}
|
|
282
366
|
/**
|
|
283
367
|
* Mints private and public tokens for the sender if their balance is below the minimum.
|
|
@@ -304,14 +388,47 @@ export class BotFactory {
|
|
|
304
388
|
this.log.info(`Skipping minting as ${minter.toString()} has enough tokens`);
|
|
305
389
|
return;
|
|
306
390
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
this.
|
|
312
|
-
|
|
391
|
+
// PrivateToken's mint accesses contract-level private storage vars (admin, total_supply).
|
|
392
|
+
const additionalScopes = isStandardToken ? undefined : [
|
|
393
|
+
token.address
|
|
394
|
+
];
|
|
395
|
+
await this.withNoMinTxsPerBlock(async ()=>{
|
|
396
|
+
const txHash = await new BatchCall(token.wallet, calls).send({
|
|
397
|
+
from: minter,
|
|
398
|
+
additionalScopes,
|
|
399
|
+
wait: NO_WAIT
|
|
400
|
+
});
|
|
401
|
+
this.log.info(`Sent token mint tx with hash ${txHash.toString()}`);
|
|
402
|
+
return waitForTx(this.aztecNode, txHash, {
|
|
313
403
|
timeout: this.config.txMinedWaitSeconds
|
|
314
|
-
})
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Gets or creates a bridge claim for the recipient.
|
|
409
|
+
* Checks if a claim already exists in the store and reuses it if valid.
|
|
410
|
+
* Only creates a new bridge if fee juice balance is below threshold.
|
|
411
|
+
*/ async getOrCreateBridgeClaim(recipient) {
|
|
412
|
+
// Check if we have an existing claim in the store
|
|
413
|
+
const existingClaim = await this.store.getBridgeClaim(recipient);
|
|
414
|
+
if (existingClaim) {
|
|
415
|
+
this.log.info(`Found existing bridge claim for ${recipient.toString()}, checking validity...`);
|
|
416
|
+
// Check if the message is ready on L2
|
|
417
|
+
try {
|
|
418
|
+
const messageHash = Fr.fromHexString(existingClaim.claim.messageHash);
|
|
419
|
+
await this.withNoMinTxsPerBlock(()=>waitForL1ToL2MessageReady(this.aztecNode, messageHash, {
|
|
420
|
+
timeoutSeconds: this.config.l1ToL2MessageTimeoutSeconds,
|
|
421
|
+
forPublicConsumption: false
|
|
422
|
+
}));
|
|
423
|
+
return existingClaim.claim;
|
|
424
|
+
} catch (err) {
|
|
425
|
+
this.log.warn(`Failed to verify existing claim, creating new one: ${err}`);
|
|
426
|
+
await this.store.deleteBridgeClaim(recipient);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const claim = await this.bridgeL1FeeJuice(recipient);
|
|
430
|
+
await this.store.saveBridgeClaim(recipient, claim);
|
|
431
|
+
return claim;
|
|
315
432
|
}
|
|
316
433
|
async bridgeL1FeeJuice(recipient) {
|
|
317
434
|
const l1RpcUrls = this.config.l1RpcUrls;
|
|
@@ -322,13 +439,13 @@ export class BotFactory {
|
|
|
322
439
|
if (!mnemonicOrPrivateKey) {
|
|
323
440
|
throw new Error('Either a mnemonic or private key of an L1 account is required to bridge the fee juice to fund the deployment of the account.');
|
|
324
441
|
}
|
|
325
|
-
const { l1ChainId } = await this.
|
|
442
|
+
const { l1ChainId } = await this.aztecNode.getNodeInfo();
|
|
326
443
|
const chain = createEthereumChain(l1RpcUrls, l1ChainId);
|
|
327
444
|
const extendedClient = createExtendedL1Client(chain.rpcUrls, mnemonicOrPrivateKey, chain.chainInfo);
|
|
328
|
-
const portal = await L1FeeJuicePortalManager.new(this.
|
|
445
|
+
const portal = await L1FeeJuicePortalManager.new(this.aztecNode, extendedClient, this.log);
|
|
329
446
|
const mintAmount = await portal.getTokenManager().getMintAmount();
|
|
330
447
|
const claim = await portal.bridgeTokensPublic(recipient, mintAmount, true);
|
|
331
|
-
await this.withNoMinTxsPerBlock(()=>waitForL1ToL2MessageReady(this.
|
|
448
|
+
await this.withNoMinTxsPerBlock(()=>waitForL1ToL2MessageReady(this.aztecNode, Fr.fromHexString(claim.messageHash), {
|
|
332
449
|
timeoutSeconds: this.config.l1ToL2MessageTimeoutSeconds,
|
|
333
450
|
forPublicConsumption: false
|
|
334
451
|
}));
|
|
@@ -336,17 +453,20 @@ export class BotFactory {
|
|
|
336
453
|
return claim;
|
|
337
454
|
}
|
|
338
455
|
async withNoMinTxsPerBlock(fn) {
|
|
339
|
-
if (!this.
|
|
456
|
+
if (!this.aztecNodeAdmin || !this.config.flushSetupTransactions) {
|
|
457
|
+
this.log.verbose(`No node admin client or flushing not requested (not setting minTxsPerBlock to 0)`);
|
|
340
458
|
return fn();
|
|
341
459
|
}
|
|
342
|
-
const { minTxsPerBlock } = await this.
|
|
343
|
-
|
|
460
|
+
const { minTxsPerBlock } = await this.aztecNodeAdmin.getConfig();
|
|
461
|
+
this.log.warn(`Setting sequencer minTxsPerBlock to 0 from ${minTxsPerBlock} to flush setup transactions`);
|
|
462
|
+
await this.aztecNodeAdmin.setConfig({
|
|
344
463
|
minTxsPerBlock: 0
|
|
345
464
|
});
|
|
346
465
|
try {
|
|
347
466
|
return await fn();
|
|
348
467
|
} finally{
|
|
349
|
-
|
|
468
|
+
this.log.warn(`Restoring sequencer minTxsPerBlock to ${minTxsPerBlock}`);
|
|
469
|
+
await this.aztecNodeAdmin.setConfig({
|
|
350
470
|
minTxsPerBlock
|
|
351
471
|
});
|
|
352
472
|
}
|
package/dest/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export { Bot } from './bot.js';
|
|
2
2
|
export { AmmBot } from './amm_bot.js';
|
|
3
|
+
export { CrossChainBot } from './cross_chain_bot.js';
|
|
3
4
|
export { BotRunner } from './runner.js';
|
|
5
|
+
export { BotStore } from './store/bot_store.js';
|
|
4
6
|
export { type BotConfig, getBotConfigFromEnv, getBotDefaultConfig, botConfigMappings, SupportedTokenContracts, } from './config.js';
|
|
5
7
|
export { getBotRunnerApiHandler } from './rpc.js';
|
|
6
8
|
export * from './interface.js';
|
|
7
|
-
//# sourceMappingURL=
|
|
9
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsR0FBRyxFQUFFLE1BQU0sVUFBVSxDQUFDO0FBQy9CLE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSxjQUFjLENBQUM7QUFDdEMsT0FBTyxFQUFFLGFBQWEsRUFBRSxNQUFNLHNCQUFzQixDQUFDO0FBQ3JELE9BQU8sRUFBRSxTQUFTLEVBQUUsTUFBTSxhQUFhLENBQUM7QUFDeEMsT0FBTyxFQUFFLFFBQVEsRUFBRSxNQUFNLHNCQUFzQixDQUFDO0FBQ2hELE9BQU8sRUFDTCxLQUFLLFNBQVMsRUFDZCxtQkFBbUIsRUFDbkIsbUJBQW1CLEVBQ25CLGlCQUFpQixFQUNqQix1QkFBdUIsR0FDeEIsTUFBTSxhQUFhLENBQUM7QUFDckIsT0FBTyxFQUFFLHNCQUFzQixFQUFFLE1BQU0sVUFBVSxDQUFDO0FBQ2xELGNBQWMsZ0JBQWdCLENBQUMifQ==
|
package/dest/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAC/B,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EACL,KAAK,SAAS,EACd,mBAAmB,EACnB,mBAAmB,EACnB,iBAAiB,EACjB,uBAAuB,GACxB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAClD,cAAc,gBAAgB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAC/B,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,OAAO,EACL,KAAK,SAAS,EACd,mBAAmB,EACnB,mBAAmB,EACnB,iBAAiB,EACjB,uBAAuB,GACxB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAClD,cAAc,gBAAgB,CAAC"}
|
package/dest/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { Bot } from './bot.js';
|
|
2
2
|
export { AmmBot } from './amm_bot.js';
|
|
3
|
+
export { CrossChainBot } from './cross_chain_bot.js';
|
|
3
4
|
export { BotRunner } from './runner.js';
|
|
5
|
+
export { BotStore } from './store/bot_store.js';
|
|
4
6
|
export { getBotConfigFromEnv, getBotDefaultConfig, botConfigMappings, SupportedTokenContracts } from './config.js';
|
|
5
7
|
export { getBotRunnerApiHandler } from './rpc.js';
|
|
6
8
|
export * from './interface.js';
|
package/dest/interface.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AztecAddress } from '@aztec/aztec.js';
|
|
1
|
+
import { AztecAddress } from '@aztec/aztec.js/addresses';
|
|
2
2
|
import type { ApiSchemaFor } from '@aztec/stdlib/schemas';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { type BotConfig } from './config.js';
|
|
@@ -20,4 +20,4 @@ export interface BotRunnerApi {
|
|
|
20
20
|
update(config: BotConfig): Promise<void>;
|
|
21
21
|
}
|
|
22
22
|
export declare const BotRunnerApiSchema: ApiSchemaFor<BotRunnerApi>;
|
|
23
|
-
//# sourceMappingURL=
|
|
23
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW50ZXJmYWNlLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvaW50ZXJmYWNlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSwyQkFBMkIsQ0FBQztBQUN6RCxPQUFPLEtBQUssRUFBRSxZQUFZLEVBQUUsTUFBTSx1QkFBdUIsQ0FBQztBQUUxRCxPQUFPLEVBQUUsQ0FBQyxFQUFFLE1BQU0sS0FBSyxDQUFDO0FBRXhCLE9BQU8sRUFBRSxLQUFLLFNBQVMsRUFBbUIsTUFBTSxhQUFhLENBQUM7QUFFOUQsZUFBTyxNQUFNLGFBQWE7Ozs7OztFQUV4QixDQUFDO0FBRUgsTUFBTSxNQUFNLE9BQU8sR0FBRyxDQUFDLENBQUMsS0FBSyxDQUFDLE9BQU8sYUFBYSxDQUFDLENBQUM7QUFFcEQsTUFBTSxXQUFXLFlBQVk7SUFDM0IsS0FBSyxJQUFJLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FBQztJQUN2QixJQUFJLElBQUksT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDO0lBQ3RCLEdBQUcsSUFBSSxPQUFPLENBQUMsSUFBSSxDQUFDLENBQUM7SUFDckIsS0FBSyxJQUFJLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FBQztJQUN2QixTQUFTLElBQUksT0FBTyxDQUFDLFNBQVMsQ0FBQyxDQUFDO0lBQ2hDLE9BQU8sSUFBSSxPQUFPLENBQUMsT0FBTyxDQUFDLENBQUM7SUFDNUIsTUFBTSxDQUFDLE1BQU0sRUFBRSxTQUFTLEdBQUcsT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDO0NBQzFDO0FBRUQsZUFBTyxNQUFNLGtCQUFrQixFQUFFLFlBQVksQ0FBQyxZQUFZLENBUXpELENBQUMifQ==
|
package/dest/interface.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"interface.d.ts","sourceRoot":"","sources":["../src/interface.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"interface.d.ts","sourceRoot":"","sources":["../src/interface.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAE1D,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,KAAK,SAAS,EAAmB,MAAM,aAAa,CAAC;AAE9D,eAAO,MAAM,aAAa;;;;;;EAExB,CAAC;AAEH,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAEpD,MAAM,WAAW,YAAY;IAC3B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACrB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,SAAS,IAAI,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5B,MAAM,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C;AAED,eAAO,MAAM,kBAAkB,EAAE,YAAY,CAAC,YAAY,CAQzD,CAAC"}
|