|
42 | 42 | import java.util.Collections; |
43 | 43 | import java.util.List; |
44 | 44 | import java.util.Set; |
| 45 | +import java.util.concurrent.ThreadLocalRandom; |
45 | 46 |
|
46 | 47 | import static com.algorand.algosdk.util.ResourceUtils.loadResource; |
47 | 48 | import static org.assertj.core.api.Assertions.assertThat; |
@@ -118,6 +119,31 @@ public class Stepdefs { |
118 | 119 | Response<CompileResponse> compileResponse; |
119 | 120 | Response<DryrunResponse> dryrunResponse; |
120 | 121 |
|
| 122 | + private static class DevModeState { |
| 123 | + static final long ACCOUNT_FUNDING_MICROALGOS = 100_000_000; |
| 124 | + private Account advanceRounds; |
| 125 | + |
| 126 | + /** |
| 127 | + * randomAmount minimizes the chance `advanceRounds` issues duplicate transactions by randomizing the payment amount. |
| 128 | + */ |
| 129 | + private long randomAmount() { |
| 130 | + return ThreadLocalRandom.current().nextLong(1, (long) (ACCOUNT_FUNDING_MICROALGOS * .01)); |
| 131 | + } |
| 132 | + |
| 133 | + public SignedTransaction selfPay(TransactionParams tp) throws Exception { |
| 134 | + Transaction tx = |
| 135 | + Transaction.PaymentTransactionBuilder() |
| 136 | + .sender(advanceRounds.getAddress()) |
| 137 | + .suggestedParams(tp) |
| 138 | + .amount(randomAmount()) |
| 139 | + .receiver(advanceRounds.getAddress()) |
| 140 | + .build(); |
| 141 | + return advanceRounds.signTransaction(tx); |
| 142 | + } |
| 143 | + } |
| 144 | + |
| 145 | + private final DevModeState dms = new DevModeState(); |
| 146 | + |
121 | 147 | protected Address getAddress(int i) { |
122 | 148 | if (addresses == null) { |
123 | 149 | throw new RuntimeException("Addresses not initialized, must use given 'wallet information'"); |
@@ -158,6 +184,48 @@ public SignedTransaction signWithAddress(Transaction tx, Address addr) throws co |
158 | 184 | return acct.signTransaction(tx); |
159 | 185 | } |
160 | 186 |
|
| 187 | + /** |
| 188 | + * advanceRound is a convenience method intended for testing with DevMode networks. |
| 189 | + * <p> |
| 190 | + * Since DevMode block generation requires a transaction rather than time passing, test assertions may require advancing rounds. advanceRound issues advanceCount payments to advance rounds. |
| 191 | + */ |
| 192 | + private void advanceRoundsV1(int advanceCount) { |
| 193 | + initializeDevModeAccount(); |
| 194 | + for (int i = 0; i < advanceCount; i++) { |
| 195 | + try { |
| 196 | + acl.rawTransaction(Encoder.encodeToMsgPack(dms.selfPay(acl.transactionParams()))); |
| 197 | + } catch (Exception e) { |
| 198 | + throw new RuntimeException(e); |
| 199 | + } |
| 200 | + } |
| 201 | + } |
| 202 | + |
| 203 | + /** |
| 204 | + * initializeDevModeAccount performs a one-time account initialization per inclusion in a scenario outline. No attempt is made to delete the account. |
| 205 | + */ |
| 206 | + public void initializeDevModeAccount() { |
| 207 | + if (dms.advanceRounds != null) { |
| 208 | + return; |
| 209 | + } |
| 210 | + |
| 211 | + try { |
| 212 | + getParams(); |
| 213 | + dms.advanceRounds = new Account(); |
| 214 | + Address sender = getAddress(0); |
| 215 | + Transaction tx = |
| 216 | + Transaction.PaymentTransactionBuilder() |
| 217 | + .sender(sender) |
| 218 | + .suggestedParams(acl.transactionParams()) |
| 219 | + .amount(DevModeState.ACCOUNT_FUNDING_MICROALGOS) |
| 220 | + .receiver(dms.advanceRounds.getAddress()) |
| 221 | + .build(); |
| 222 | + SignedTransaction st = signWithAddress(tx, sender); |
| 223 | + acl.rawTransaction(Encoder.encodeToMsgPack(st)); |
| 224 | + } catch (Exception e) { |
| 225 | + throw new RuntimeException(e); |
| 226 | + } |
| 227 | + } |
| 228 | + |
161 | 229 | /** |
162 | 230 | * Convenience method to export a key and initialize an account to use for signing. |
163 | 231 | */ |
@@ -447,8 +515,8 @@ public void status() throws ApiException{ |
447 | 515 | } |
448 | 516 |
|
449 | 517 | @When("I get status after this block") |
450 | | - public void statusBlock() throws ApiException, InterruptedException { |
451 | | - Thread.sleep(4000); |
| 518 | + public void statusBlock() throws Exception { |
| 519 | + advanceRoundsV1(1); |
452 | 520 | statusAfter = acl.waitForBlock(status.getLastRound()); |
453 | 521 | } |
454 | 522 |
|
@@ -526,21 +594,48 @@ public void msigNotInWallet()throws com.algorand.algosdk.kmd.client.ApiException |
526 | 594 | } |
527 | 595 |
|
528 | 596 | @When("I generate a key using kmd") |
529 | | - public void genKeyKmd() throws com.algorand.algosdk.kmd.client.ApiException, NoSuchAlgorithmException{ |
| 597 | + public void genKeyKmd() throws com.algorand.algosdk.kmd.client.ApiException, NoSuchAlgorithmException { |
530 | 598 | GenerateKeyRequest req = new GenerateKeyRequest(); |
531 | 599 | req.setDisplayMnemonic(false); |
532 | 600 | req.setWalletHandleToken(handle); |
533 | 601 | pk = new Address(kcl.generateKey(req).getAddress()); |
534 | 602 | } |
535 | 603 |
|
| 604 | + @When("I generate a key using kmd for rekeying and fund it") |
| 605 | + public void genKeyKmdRekey() throws com.algorand.algosdk.kmd.client.ApiException, NoSuchAlgorithmException { |
| 606 | + GenerateKeyRequest req = new GenerateKeyRequest(); |
| 607 | + req.setDisplayMnemonic(false); |
| 608 | + req.setWalletHandleToken(handle); |
| 609 | + rekey = new Address(kcl.generateKey(req).getAddress()); |
| 610 | + |
| 611 | + // Fund rekey address |
| 612 | + try { |
| 613 | + getParams(); |
| 614 | + Address sender = getAddress(0); |
| 615 | + Transaction tx = |
| 616 | + Transaction.PaymentTransactionBuilder() |
| 617 | + .sender(sender) |
| 618 | + .suggestedParams(acl.transactionParams()) |
| 619 | + .amount(100_000_000) |
| 620 | + .receiver(rekey) |
| 621 | + .build(); |
| 622 | + SignedTransaction st = signWithAddress(tx, sender); |
| 623 | + acl.rawTransaction(Encoder.encodeToMsgPack(st)); |
| 624 | + } catch (Exception e) { |
| 625 | + throw new RuntimeException(e); |
| 626 | + } |
| 627 | + } |
| 628 | + |
| 629 | + Address rekey; |
| 630 | + |
536 | 631 | @Then("the key should be in the wallet") |
537 | 632 | public void keyInWallet() throws com.algorand.algosdk.kmd.client.ApiException { |
538 | 633 | ListKeysRequest req = new ListKeysRequest(); |
539 | 634 | req.setWalletHandleToken(handle); |
540 | 635 | List<String> keys = kcl.listKeysInWallet(req).getAddresses(); |
541 | 636 | boolean exists = false; |
542 | | - for (String k : keys){ |
543 | | - if (k.equals(pk.toString())){ |
| 637 | + for (String k : keys) { |
| 638 | + if (k.equals(pk.toString())) { |
544 | 639 | exists = true; |
545 | 640 | } |
546 | 641 | } |
@@ -632,6 +727,7 @@ public void aClient() throws FileNotFoundException, IOException{ |
632 | 727 | algodClient.setBasePath("http://localhost:" + algodPort); |
633 | 728 | acl = new AlgodApi(algodClient); |
634 | 729 | } |
| 730 | + |
635 | 731 | @Given("an algod v2 client") |
636 | 732 | public void aClientv2() throws FileNotFoundException, IOException{ |
637 | 733 | aclv2 = new com.algorand.algosdk.v2.client.common.AlgodClient( |
@@ -660,21 +756,30 @@ public void walletInfo() throws com.algorand.algosdk.kmd.client.ApiException, No |
660 | 756 | } |
661 | 757 |
|
662 | 758 | @Given("default transaction with parameters {int} {string}") |
663 | | - public void defaultTxn(int amt, String note) throws ApiException, NoSuchAlgorithmException{ |
| 759 | + public void defaultTxn(int amt, String note) throws ApiException, NoSuchAlgorithmException { |
| 760 | + defaultTxnWithAddress(amt, note, getAddress(0)); |
| 761 | + } |
| 762 | + |
| 763 | + @Given("default transaction with parameters {int} {string} and rekeying key") |
| 764 | + public void defaultTxnForRekeying(int amt, String note) { |
| 765 | + defaultTxnWithAddress(amt, note, rekey); |
| 766 | + } |
| 767 | + |
| 768 | + private void defaultTxnWithAddress(int amt, String note, Address sender) { |
664 | 769 | getParams(); |
665 | | - if (note.equals("none")){ |
| 770 | + if (note.equals("none")) { |
666 | 771 | this.note = null; |
667 | | - } else{ |
| 772 | + } else { |
668 | 773 | this.note = Encoder.decodeFromBase64(note); |
669 | 774 | } |
670 | 775 | txnBuilder = Transaction.PaymentTransactionBuilder() |
671 | | - .sender(getAddress(0)) |
| 776 | + .sender(sender) |
672 | 777 | .suggestedParams(params) |
673 | 778 | .note(this.note) |
674 | 779 | .amount(amt) |
675 | 780 | .receiver(getAddress(1)); |
676 | 781 | txn = txnBuilder.build(); |
677 | | - pk = getAddress(0); |
| 782 | + pk = sender; |
678 | 783 | } |
679 | 784 |
|
680 | 785 | @Given("default multisig transaction with parameters {int} {string}") |
@@ -715,23 +820,32 @@ public void sendMsigTxn() throws JsonProcessingException, ApiException{ |
715 | 820 | } |
716 | 821 |
|
717 | 822 | @Then("the transaction should go through") |
718 | | - public void checkTxn() throws ApiException, InterruptedException{ |
| 823 | + public void checkTxn() throws Exception { |
719 | 824 | String ans = acl.pendingTransactionInformation(txid).getFrom(); |
720 | 825 | assertThat(this.txn.sender.toString()).isEqualTo(ans); |
721 | | - acl.waitForBlock(lastRound.add(BigInteger.valueOf(2))); |
| 826 | + waitForAlgodTransactionProcessingToComplete(); |
722 | 827 | String senderFromResponse = acl.transactionInformation(txn.sender.toString(), txid).getFrom(); |
723 | 828 | assertThat(senderFromResponse).isEqualTo(txn.sender.toString()); |
724 | 829 | assertThat(acl.transaction(txid).getFrom()).isEqualTo(senderFromResponse); |
725 | 830 | } |
726 | 831 |
|
| 832 | + /** |
| 833 | + * waitForAlgodTransactionProcessingToComplete is a Dev mode helper method that's a rough analog to `acl.waitForBlock(lastRound.add(BigInteger.valueOf(2)));`. |
| 834 | + * <p> |
| 835 | + * Since Dev mode produces blocks on a per transaction basis, it's possible algod generates a block _before_ the corresponding SDK call to wait for a block. Without _any_ wait, it's possible the SDK looks for the transaction before algod completes processing. So, the method performs a local sleep to simulate waiting for a block. |
| 836 | + */ |
| 837 | + private static void waitForAlgodTransactionProcessingToComplete() throws Exception { |
| 838 | + Thread.sleep(500); |
| 839 | + } |
| 840 | + |
727 | 841 | @Then("I can get the transaction by ID") |
728 | | - public void txnbyID() throws ApiException, InterruptedException{ |
729 | | - acl.waitForBlock(lastRound.add(BigInteger.valueOf(2))); |
| 842 | + public void txnByID() throws Exception { |
| 843 | + waitForAlgodTransactionProcessingToComplete(); |
730 | 844 | assertThat(acl.transaction(txid).getFrom()).isEqualTo(pk.toString()); |
731 | 845 | } |
732 | 846 |
|
733 | 847 | @Then("the transaction should not go through") |
734 | | - public void txnFail(){ |
| 848 | + public void txnFail() { |
735 | 849 | assertThat(err).isTrue(); |
736 | 850 | } |
737 | 851 |
|
|
0 commit comments