1 module tagion.testbench.e2e.operational;
2 // Default import list for bdd
3 import core.thread;
4 import core.time;
5 import std.algorithm;
6 import std.file;
7 import std.traits;
8 import std.format;
9 import std.getopt;
10 import std.path;
11 import std.range;
12 import std.conv;
13 import std.datetime;
14 import std.stdio;
15 import std.random;
16 import std.typecons : Tuple;
17 import tagion.behaviour;
18 import tagion.communication.HiRPC;
19 import tagion.hibon.Document;
20 import tagion.hibon.HiBONRecord;
21 import tagion.hibon.HiBONFile;
22 import tagion.script.TagionCurrency;
23 import tagion.script.common;
24 import tagion.basic.Types : FileExtension;
25 import tagion.basic.tagionexceptions;
26 import tagion.testbench.tools.Environment;
27 import tagion.tools.Basic : Main, __verbose_switch;
28 import tagion.tools.wallet.WalletInterface;
29 import tagion.tools.wallet.WalletOptions : WalletOptions;
30 import tagion.utils.JSONCommon;
31 import tagion.utils.StdTime;
32 import tagion.wallet.AccountDetails;
33 
34 alias operational = tagion.testbench.e2e.operational;
35 
36 mixin Main!(_main);
37 
38 enum DurationUnit {
39     minutes,
40     hours,
41     days,
42 }
43 
44 WalletInterface* createInterface(string config, string pin) {
45     WalletOptions opts;
46     opts.load(config);
47     auto wallet_interface = new WalletInterface(opts);
48     check(wallet_interface.load, "Wallet %s could not be loaded".format(config));
49     check(wallet_interface.secure_wallet.login(pin), "Wallet %s %s, not logged in".format(config, pin));
50     writefln("Wallet logged in %s", wallet_interface.secure_wallet.isLoggedin);
51     return wallet_interface;
52 }
53 
54 @recordType("tx_stats")
55 struct TxStats {
56     TagionCurrency total_fees;
57     TagionCurrency total_sent;
58     uint transactions;
59     uint failed_runs;
60     sdt_t start;
61     sdt_t end;
62 
63     mixin HiBONRecord;
64 }
65 
66 int _main(string[] args) {
67     const program = args[0];
68     string[] wallet_config_files;
69     string[] wallet_pins;
70     bool sendkernel = false;
71     uint duration = 3;
72     int max_failed_runs = 5;
73     DurationUnit duration_unit = DurationUnit.days;
74 
75     __verbose_switch = true;
76 
77     auto tx_stats = new TxStats;
78 
79     auto main_args = getopt(args,
80             "w", "wallet config files", &wallet_config_files,
81             "x", "wallet pins", &wallet_pins,
82             "sendkernel", "Send requests directory to the kernel", &sendkernel,
83             "duration", format("The duration the test should run for (current = %s)", duration), &duration,
84             "unit", format("The duration unit on of %s (current = %s)", [EnumMembers!DurationUnit], duration_unit), &duration_unit,
85     "max_failed_runs", format("The maximum amount of failed runs, before the process exits (current = %s)", max_failed_runs), &duration_unit,
86     );
87 
88     if (main_args.helpWanted) {
89         defaultGetoptPrinter(
90                 [
91                 "Usage:",
92                 format("%s [<option>...] <config.json> <files>", program),
93                 "<option>:",
94                 ].join("\n"),
95                 main_args.options);
96         return 0;
97     }
98 
99     if (!wallet_config_files.length == 2) {
100         writeln("Exactly 2 wallets should be provided");
101         return 1;
102     }
103 
104     if (wallet_pins.empty) {
105         foreach (i, _; wallet_config_files) {
106             wallet_pins ~= format("%04d", i + 1);
107         }
108     }
109 
110     check(wallet_pins.length == wallet_config_files.length, "wallet configs and wallet pins were not the same amount");
111 
112     alias ConfigAndPin = Tuple!(string, "config", string, "pin");
113     ConfigAndPin[] configs_and_pins
114         = wallet_config_files.zip(wallet_pins).map!(c => ConfigAndPin(c[0], c[1])).array;
115 
116     Duration max_runtime;
117     with (DurationUnit) final switch (duration_unit) {
118     case days:
119         max_runtime = duration.days;
120         break;
121     case hours:
122         max_runtime = duration.hours;
123         break;
124     case minutes:
125         max_runtime = duration.minutes;
126         break;
127     }
128 
129     void pickWallets(ref ConfigAndPin[] configs_and_pins, out ConfigAndPin sender, out ConfigAndPin receiver, uint run)
130     in (configs_and_pins.length >= 2)
131     out (; sender != receiver)
132     do {
133         sender = configs_and_pins[!(run & 1)];
134         receiver = configs_and_pins[(run & 1)];
135     }
136 
137     // Times of the monotomic clock
138     const start_clocktime = MonoTime.currTime;
139     const end_clocktime = start_clocktime + max_runtime;
140 
141     // Date for pretty reporting
142     const start_date = cast(DateTime) Clock.currTime;
143     const predicted_end_date = start_date + max_runtime;
144     sdt_t start_sdt_time = currentTime();
145 
146     int run_counter;
147     int failed_runs_counter;
148     scope (exit) {
149         const end_date = cast(DateTime)(Clock.currTime);
150         writefln("Made %s runs", run_counter);
151         writefln("Test ended on %s", end_date);
152         tx_stats.transactions = run_counter - failed_runs_counter;
153         tx_stats.failed_runs = failed_runs_counter;
154         tx_stats.start = start_sdt_time;
155         tx_stats.end = currentTime(); // sdt time
156 
157         const tx_file = buildPath(env.dlog, "tx_stats".setExtension(FileExtension.hibon));
158         mkdirRecurse(dirName(tx_file));
159         fwrite(tx_file, *tx_stats);
160     }
161 
162     writefln("Starting operational test now on\n\t%s\nand will end in %s, on\n\t%s",
163             start_date, max_runtime,
164             predicted_end_date);
165 
166     bool stop;
167     while (!stop) {
168         scope (failure) {
169             stop = true;
170         }
171         run_counter++;
172 
173         ConfigAndPin sender;
174         ConfigAndPin receiver;
175         pickWallets(configs_and_pins, sender, receiver, run_counter);
176 
177         auto sender_interface = createInterface(sender.config, sender.pin);
178         auto receiver_interface = createInterface(receiver.config, receiver.pin);
179 
180         writefln("Making transaction between sender %s and receiver %s", sender.config, receiver.config);
181 
182         auto operational_feature = automation!operational;
183         operational_feature.SendNContractsFromwallet1Towallet2(sender_interface, receiver_interface, sendkernel, tx_stats);
184         auto feat_group = operational_feature.run;
185 
186         auto runs_file = File(buildPath(env.dlog, "runs.txt"), "w");
187         runs_file.writeln(run_counter);
188         runs_file.close;
189 
190         if (feat_group.result.hasErrors) {
191             /// Never if max_failed_runs -1
192             if (failed_runs_counter == max_failed_runs) {
193                 stop = true;
194             }
195 
196             auto failed_run_file = buildPath(env.dlog, format("failed_%s", run_counter).setExtension(FileExtension
197                     .hibon));
198             fwrite(failed_run_file, *(feat_group.result));
199             failed_runs_counter++;
200             return 1;
201         }
202 
203         stop = (MonoTime.currTime >= end_clocktime);
204     }
205     return 0;
206 }
207 
208 enum feature = Feature(
209             "send multiple contracts through the network",
210             []);
211 
212 alias FeatureContext = Tuple!(
213         SendNContractsFromwallet1Towallet2, "SendNContractsFromwallet1Towallet2",
214         FeatureGroup*, "result"
215 );
216 
217 @safe @Scenario("send N contracts from `wallet1` to `wallet2`",
218         [])
219 class SendNContractsFromwallet1Towallet2 {
220     enum invoice_amount = 1000.TGN;
221     WalletInterface* sender;
222     WalletInterface* receiver;
223     bool sendkernel;
224     bool send;
225 
226     TagionCurrency receiver_amount;
227     TagionCurrency sender_amount;
228     TxStats* tx_stats;
229 
230     this(ref WalletInterface* sender, WalletInterface* receiver, bool sendkernel, TxStats* tx_stats) {
231         this.sender = sender;
232         this.receiver = receiver;
233         this.sendkernel = sendkernel;
234         this.send = !sendkernel;
235         this.tx_stats = tx_stats;
236     }
237 
238     @Given("i have a network")
239     Document network() @trusted {
240         writefln("sendkernel: %s, sendshell: %s", sendkernel, send);
241         // dfmt off
242         const wallet_switch = WalletInterface.Switch(
243                 update: true, 
244                 sendkernel: sendkernel,
245                 send: send);
246         // dfmt on
247 
248         with (receiver) {
249             check(secure_wallet.isLoggedin, "the wallet must be logged in!!!");
250             operate(wallet_switch, []);
251             receiver_amount = secure_wallet.available_balance;
252         }
253 
254         with (sender) {
255             check(secure_wallet.isLoggedin, "the wallet must be logged in!!!");
256             operate(wallet_switch, []);
257             sender_amount = secure_wallet.available_balance;
258         }
259 
260         return result_ok;
261     }
262 
263     Invoice invoice;
264     TagionCurrency fees;
265     @When("i send a valid contract from `wallet1` to `wallet2`")
266     Document wallet2() @trusted {
267         with (receiver.secure_wallet) {
268             invoice = createInvoice("Invoice", invoice_amount);
269             registerInvoice(invoice);
270         }
271 
272         SignedContract signed_contract;
273 
274         with (sender) {
275             check(secure_wallet.isLoggedin, "the wallet must be logged in!!!");
276             auto result = secure_wallet.payment([invoice], signed_contract, fees);
277 
278             const message = secure_wallet.net.calcHash(signed_contract);
279             const contract_net = secure_wallet.net.derive(message);
280             const hirpc = HiRPC(contract_net);
281             const hirpc_submit = hirpc.submit(signed_contract);
282 
283             if (sendkernel) {
284                 auto response = sendSubmitHiRPC(options.contract_address, hirpc_submit, contract_net);
285                 check(!response.isError, format("Error when sending kernel submit\n%s", response.toPretty));
286             }
287             else {
288                 auto response = sendShellSubmitHiRPC(options.addr ~ options.contract_shell_endpoint, hirpc_submit, contract_net);
289                 check(!response.isError, format("Error when sending shell submit\n%s", response.toPretty));
290             }
291 
292             result.get;
293         }
294 
295         return result_ok;
296     }
297 
298     @When("the contract has been executed")
299     Document executed() @trusted {
300         Thread.sleep(20.seconds);
301         return result_ok;
302     }
303 
304     @Then("wallet1 and wallet2 balances should be updated")
305     Document updated() @trusted {
306         //dfmt off
307         const wallet_switch = WalletInterface.Switch(
308             trt_update : true,
309             sendkernel: sendkernel,
310             send: send);
311 
312         enum update_retries = 20;
313         enum retry_delay = 5.seconds;
314 
315         void check_balance(WalletInterface* wallet, const TagionCurrency expected) {
316             with(wallet) {
317                 check(secure_wallet.isLoggedin, "the wallet must be logged in!!!");
318                 foreach(i; 0 .. update_retries) {
319                     writefln("wallet try update %s of %s", i+1, update_retries);
320                     try {
321                         operate(wallet_switch, []);
322                     }
323                     catch(TagionException e) {
324                         writeln(e);
325                     }
326                     if(secure_wallet.available_balance == expected) {
327                         return;
328                     }
329                     Thread.sleep(retry_delay);
330                 }
331                 check(secure_wallet.available_balance == expected, 
332                         format("wallet amount incorrect, expected %s got %s",
333                         expected, secure_wallet.available_balance));
334             }
335         }
336 
337         check_balance(receiver, (receiver_amount + invoice.amount));
338         check_balance(sender, (sender_amount - (invoice.amount + fees)));
339 
340         tx_stats.total_fees += fees;
341         tx_stats.total_sent += invoice.amount;
342 
343 
344         return result_ok;
345     }
346 
347 }