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 }