This repository has been archived by the owner on Sep 24, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathindex.js
273 lines (244 loc) · 8.86 KB
/
index.js
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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
import * as dotenv from "dotenv";
import Client, { HTTP } from "drand-client";
import fetch from "node-fetch";
import AbortController from "abort-controller";
import { CosmWasmClient, SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import { assertIsDeliverTxSuccess, calculateFee, logs, GasPrice } from "@cosmjs/stargate";
import { toUtf8 } from "@cosmjs/encoding";
import { Decimal } from "@cosmjs/math";
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";
import { FaucetClient } from "@cosmjs/faucet-client";
import { assert } from "@cosmjs/utils";
import { TxRaw } from "cosmjs-types/cosmos/tx/v1beta1/tx.js";
import { MsgExecuteContract } from "cosmjs-types/cosmwasm/wasm/v1/tx.js";
import chalk from "chalk";
import { shuffle } from "./shuffle.js";
dotenv.config();
global.fetch = fetch;
global.AbortController = AbortController;
const errorColor = chalk.red;
const warningColor = chalk.hex("#FFA500"); // Orange
const successColor = chalk.green;
const infoColor = chalk.gray;
// Required env vars
assert(process.env.PREFIX, "PREFIX must be set");
const prefix = process.env.PREFIX;
assert(process.env.DENOM, "DENOM must be set");
/** The fee denom */
const denom = process.env.DENOM;
assert(process.env.ENDPOINT, "ENDPOINT must be set");
const endpoint = process.env.ENDPOINT;
assert(process.env.NOIS_CONTRACT, "NOIS_CONTRACT must be set");
const noisContract = process.env.NOIS_CONTRACT;
assert(process.env.GAS_PRICE, "GAS_PRICE must be set. E.g. '0.025unois'");
const gasPrice = GasPrice.fromString(process.env.GAS_PRICE);
// Optional env vars
const endpoint2 = process.env.ENDPOINT2 || null;
const endpoint3 = process.env.ENDPOINT3 || null;
/*
CosmJS
*/
const mnemonic = await (async () => {
if (process.env.MNEMONIC) {
return process.env.MNEMONIC;
} else {
let wallet = await DirectSecp256k1HdWallet.generate(12, { prefix });
const newMnemonic = wallet.mnemonic;
const [account] = await wallet.getAccounts();
const address = account.address;
console.log(`Generated new mnemonic: ${newMnemonic} and address ${address}`);
const faucetEndpoint = process.env.FAUCET_ENDPOINT;
if (faucetEndpoint) {
const faucet = new FaucetClient(faucetEndpoint);
await faucet.credit(address, denom);
} else {
console.warn(
"MNEMONIC and FAUCET_ENDPOINT are unset. Bot account has probably has no funds.",
);
}
return newMnemonic;
}
})();
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { prefix });
const [firstAccount] = await wallet.getAccounts();
const client = await SigningCosmWasmClient.connectWithSigner(endpoint, wallet, {
prefix,
gasPrice,
});
console.log(infoColor(`Bot address: ${firstAccount.address}`));
let nextSignData = {
chainId: "",
accountNumber: NaN,
sequence: NaN,
};
function getNextSignData() {
let out = { ...nextSignData }; // copy values
nextSignData.sequence += 1;
return out;
}
// Needed in case an error happened to ensure sequence is in sync
// with chain
async function resetSignData() {
nextSignData = {
chainId: await client.getChainId(),
...(await client.getSequence(firstAccount.address)),
};
console.log(infoColor(`Sign data set to: ${JSON.stringify(nextSignData)}`));
}
/*
DRAND
*/
const chainHash = process.env.CHAIN_HASH; // (hex encoded)
const urls = [
"https://api.drand.sh",
"https://api2.drand.sh",
"https://api3.drand.sh",
"https://drand.cloudflare.com",
// ...
];
// Shuffle enpoints to reduce likelyhood of two bots ending up with the same endpoint
shuffle(urls);
const drandGenesis = 1595431050;
const drandRoundLength = 30;
// See TimeOfRound implementation: https://github.com/drand/drand/blob/eb36ba81e3f28c966f95bcd602f60e7ff8ef4c35/chain/time.go#L30-L33
function timeOfRound(round) {
return drandGenesis + (round - 1) * drandRoundLength;
}
function printableCoin(coin) {
if (coin.denom?.startsWith("u")) {
const ticker = coin.denom.slice(1).toUpperCase();
return Decimal.fromAtomics(coin.amount ?? "0", 6).toString() + " " + ticker;
} else {
return coin.amount + coin.denom;
}
}
function isSet(a) {
return a !== null && a !== undefined;
}
const fee = calculateFee(750_000, gasPrice);
export function ibcPacketsSent(resultLogs) {
const allEvents = resultLogs.flatMap((log) => log.events);
const packetsEvents = allEvents.filter((e) => e.type === "send_packet");
const attributes = packetsEvents.flatMap((e) => e.attributes);
const packetsSentCount = attributes.filter((a) => a.key === "packet_sequence").length;
return packetsSentCount;
}
async function main() {
// See https://github.com/drand/drand-client#api
const drand_options = { chainHash, disableBeaconVerification: true };
const drandClient = await Client.wrap(HTTP.forURLs(urls, chainHash), drand_options);
let broadcaster2 = endpoint2 ? await CosmWasmClient.connect(endpoint2) : null;
let broadcaster3 = endpoint3 ? await CosmWasmClient.connect(endpoint3) : null;
const moniker = process.env.MONIKER;
if (moniker) {
console.info(infoColor("Registering this bot ..."));
await client.execute(
firstAccount.address,
noisContract,
{
register_bot: { moniker: moniker },
},
"auto",
);
}
// Initialize local sign data
await resetSignData();
for await (const res of drandClient.watch()) {
/*
/// Example of response
{
round: 2219943,
randomness: 'f53a54f5...',
signature: '8072acccd...',
previous_signature: '98670f6c6...'
}
Use res.randomness to insert randomness
*/
try {
console.info(infoColor(`Submitting drand round ${res.round} ...`));
const broadcastTime = Date.now() / 1000;
const msg = {
typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract",
value: MsgExecuteContract.fromPartial({
sender: firstAccount.address,
contract: noisContract,
msg: toUtf8(
JSON.stringify({
add_round: {
round: res.round,
signature: res.signature,
previous_signature: res.previous_signature,
},
}),
),
funds: [],
}),
};
const memo = `Insert randomness round: ${res.round}`;
const signData = getNextSignData(); // Do this the manual way to save one query
const signed = await client.sign(firstAccount.address, [msg], fee, memo, signData);
const tx = Uint8Array.from(TxRaw.encode(signed).finish());
const p1 = client.broadcastTx(tx);
const p2 = broadcaster2?.broadcastTx(tx);
const p3 = broadcaster3?.broadcastTx(tx);
p1.then(
() => console.log(infoColor("Broadcast 1 succeeded")),
(err) => console.warn(warningColor(`Broadcast 1 failed: ${err}`)),
);
p2?.then(
() => console.log(infoColor("Broadcast 2 succeeded")),
(err) => console.warn(warningColor(`Broadcast 2 failed: ${err}`)),
);
p3?.then(
() => console.log(infoColor("Broadcast 3 succeeded")),
(err) => console.warn(warningColor(`Broadcast 3 failed: ${err}`)),
);
const result = await Promise.any([p1, p2, p3].filter(isSet));
assertIsDeliverTxSuccess(result);
const parsedLogs = logs.parseRawLog(result.rawLog);
const jobs = ibcPacketsSent(parsedLogs);
console.info(
successColor(
`✔ Round ${res.round} (Gas: ${result.gasUsed}/${result.gasWanted}; Jobs processed: ${jobs}; Transaction: ${result.transactionHash})`,
),
);
const publishTime = timeOfRound(res.round);
const { block } = await client.forceGetTmClient().block(result.height);
const commitTime = block.header.time.getTime() / 1000; // seconds with fractional part
const diff = commitTime - publishTime;
console.info(
infoColor(
`Broadcast time (local): ${broadcastTime}; Drand publish time: ${publishTime}; Commit time: ${commitTime}; Diff: ${diff.toFixed(
3,
)}`,
),
);
// Some seconds after the submission when things are idle, check and log
// the balance of the bot.
setTimeout(() => {
client.getBalance(firstAccount.address, denom).then(
(balance) => {
console.log(infoColor(`Balance: ${printableCoin(balance)}`));
},
(error) => console.warn(warningColor(`Error getting bot balance: ${error}`)),
);
}, 5_000);
} catch (e) {
console.error(errorColor(e.toString()));
// In case of an error, reset the chain ID and sequence to the on-chain values.
// If this also fails, the process is killed since the error here is not caught anymore.
console.info(infoColor("Resetting sign data ..."));
await resetSignData();
}
}
}
main().then(
() => {
console.info("Done");
process.exit(0);
},
(error) => {
console.error(error);
process.exit(1);
},
);