Skip to content

Commit

Permalink
issue #133 - helper with common functionality
Browse files Browse the repository at this point in the history
helper supports several operations + enhanced multisig builder
  • Loading branch information
tonowie committed Nov 19, 2019
1 parent 2417a75 commit 2e84280
Show file tree
Hide file tree
Showing 3 changed files with 276 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import io.proximax.sdk.model.account.PublicAccount;
import io.proximax.sdk.model.transaction.EntityType;
import io.proximax.sdk.model.transaction.EntityVersion;
import io.proximax.sdk.model.transaction.ModifyMultisigAccountTransaction;
Expand Down Expand Up @@ -124,4 +126,25 @@ public ModifyMultisigAccountTransactionBuilder modifications(MultisigCosignatory
return modifications(Arrays.asList(modifications));
}

/**
* <p>
* change account to mutisig account by adding specified cosigners and specifying minimum approvals and minimum
* removals
* </p>
* <p>
* NOTE that if account has already been multisig then cosigners will be added and min approvals and min removals
* will be changed by specified deltas
* </p>
*
* @param cosigners list of cosigners to be added to the account
* @param minApprovals change in min approvals
* @param minRemovals change in min removals
*
* @return self
*/
public ModifyMultisigAccountTransactionBuilder changeToMultisig(List<PublicAccount> cosigners, int minApprovals,
int minRemovals) {
return minApprovalDelta(minApprovals).minRemovalDelta(minRemovals)
.modifications(cosigners.stream().map(MultisigCosignatoryModification::add).collect(Collectors.toList()));
}
}
193 changes: 193 additions & 0 deletions src/main/java/io/proximax/sdk/utils/BlockchainHelper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/*
* Copyright 2019 ProximaX Limited. All rights reserved.
* Use of this source code is governed by the Apache 2.0
* license that can be found in the LICENSE file.
*/
package io.proximax.sdk.utils;

import java.math.BigInteger;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import io.proximax.sdk.BlockchainApi;
import io.proximax.sdk.FeeCalculationStrategy;
import io.proximax.sdk.ListenerRepository;
import io.proximax.sdk.TransactionRepository;
import io.proximax.sdk.model.account.Account;
import io.proximax.sdk.model.account.PublicAccount;
import io.proximax.sdk.model.blockchain.NetworkType;
import io.proximax.sdk.model.transaction.AggregateTransaction;
import io.proximax.sdk.model.transaction.LockFundsTransaction;
import io.proximax.sdk.model.transaction.ModifyMultisigAccountTransaction;
import io.proximax.sdk.model.transaction.SignedTransaction;
import io.proximax.sdk.model.transaction.Transaction;
import io.proximax.sdk.model.transaction.TransactionInfo;
import io.proximax.sdk.model.transaction.builder.TransactionBuilderFactory;

/**
* helper class for common operations
*/
public class BlockchainHelper {
/** milliseconds in an hour 60 minutes of 60 seconds of 1000 milliseconds */
private static final long HOUR_MILLIS = 3_600_000;

private final BlockchainApi api;
private final TransactionBuilderFactory transact;

/**
* </p>
* create new helper instance
* </p>
* <p>
* note this might require connection to the network if network type is not known to the provided api
* </p>
*
* @param api
*/
public BlockchainHelper(BlockchainApi api) {
this.api = api;
this.transact = api.transact();
// initialize defaults for the transactions - 2 hour deadline and medium fees
this.transact.setDeadlineMillis(BigInteger.valueOf(2 * HOUR_MILLIS));
this.transact.setFeeCalculationStrategy(FeeCalculationStrategy.MEDIUM);
}

/**
* get infinite source of new, random accounts
*
* @return stream of random accounts
*/
public Stream<Account> accountRandom() {
final NetworkType network = api.getNetworkType();
return Stream.generate(() -> Account.generateNewAccount(network));
}

/**
* generate specified number of account instances
*
* @param count the count of items in the returned list of accounts
* @return list of accounts
*/
public List<Account> accountRandom(int count) {
// generate specified number of random accounts
return accountRandom().limit(count).collect(Collectors.toList());
}

public void accountToMultisig(Account account, List<PublicAccount> cosigners, int minApprovals, int minRemovals,
int optinBlocks, int confirmationTimeoutSeconds) {
// prepare transaction repo
TransactionRepository transactions = api.createTransactionRepository();
// prepare listener
ListenerRepository listener = createListener();
// first prepare the actual transaction to change account to multisig account
ModifyMultisigAccountTransaction multisigChangeTrans = transact.multisigModification()
.changeToMultisig(cosigners, minApprovals, minRemovals).build();
// aggregate bonded transaction is required for cosigner opt-in so create that
AggregateTransaction aggregateTrans = transact.aggregateBonded()
.innerTransactions(multisigChangeTrans.toAggregate(account.getPublicAccount())).build();
// aggregate bonded transaction requires lock funds
LockFundsTransaction lockTrans = transact.lockFunds()
.forAggregate(BigInteger.valueOf(optinBlocks), api.sign(aggregateTrans, account)).build();
// announce lock funds and wait for confirmation
transactionConfirmed(transactions, listener, lockTrans, account, confirmationTimeoutSeconds);
// !!! wait a bit for server to get into consistent state !!!
sleepForAWhile();
// now announce the aggregate transaction
transactionBondedAdded(transactions, listener, aggregateTrans, account, confirmationTimeoutSeconds);
}

/**
* announce transaction and wait for it to be added to confirmed transactions
*
* @param transRepo
* @param listener
* @param transaction
* @param initiatorAccount
* @param confirmationTimeoutSeconds
* @return
*/
public Transaction transactionConfirmed(TransactionRepository transRepo, ListenerRepository listener,
Transaction transaction, Account initiatorAccount, int confirmationTimeoutSeconds) {
// sign the transaction
SignedTransaction signedTrans = api.sign(transaction, initiatorAccount);
// announce the signed transaction
transRepo.announce(signedTrans).blockingFirst();
// wait for confirmation of the transaction
return listener.confirmed(initiatorAccount.getAddress()).filter(trans -> equalHashes(trans, signedTrans))
.timeout(confirmationTimeoutSeconds, TimeUnit.SECONDS).blockingFirst();
}

/**
* announce transaction and wait for it to be added to confirmed transactions
*
* @param transRepo
* @param listener
* @param transaction
* @param initiatorAccount
* @param confirmationTimeoutSeconds
* @return
*/
public Transaction transactionBondedAdded(TransactionRepository transRepo, ListenerRepository listener,
AggregateTransaction transaction, Account initiatorAccount, int confirmationTimeoutSeconds) {
// sign the transaction
SignedTransaction signedTrans = api.sign(transaction, initiatorAccount);
// announce the signed transaction
transRepo.announceAggregateBonded(signedTrans).blockingFirst();
// wait for confirmation of the transaction
return listener.aggregateBondedAdded(initiatorAccount.getAddress())
.filter(trans -> equalHashes(trans, signedTrans)).timeout(confirmationTimeoutSeconds, TimeUnit.SECONDS)
.blockingFirst();
}

/**
* blocking! create and initialize listener
*
* @return listener that can immediately be used
*/
public ListenerRepository createListener() {
ListenerRepository listener = api.createListener();
try {
listener.open().get();
return listener;
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException("Failed to open listener", e);
}
}

/**
* wait for specified number of blocks. This method blocks or throws timeout exception
*
* @param count number of blocks to wait for
* @param timeoutSeconds timeout after which the wait will be aborted
*
* @return height of the last block
*/
public BigInteger blockConfirmations(int count, long timeoutSeconds) {
return api.createBlockchainRepository().getBlockchainHeight().timeout(timeoutSeconds, TimeUnit.SECONDS)
.take(count).blockingLast();
}

private static boolean equalHashes(Transaction trans, SignedTransaction signed) {
System.out.println(trans);
Optional<TransactionInfo> info = trans.getTransactionInfo();
if (info.isPresent()) {
return info.get().getHash().equals(Optional.of(signed.getHash()));
}
return false;
}

/**
* convenience sleep needed to work around server listener synchronization issues
*/
public void sleepForAWhile() {
try {
Thread.sleep(1000l);
} catch (InterruptedException e) {
// do nothing
}
}
}
60 changes: 60 additions & 0 deletions src/test/java/io/proximax/sdk/utils/BlockchainHelperTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright 2019 ProximaX Limited. All rights reserved.
* Use of this source code is governed by the Apache 2.0
* license that can be found in the LICENSE file.
*/
package io.proximax.sdk.utils;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.Optional;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

import io.proximax.sdk.BlockchainApi;
import io.proximax.sdk.model.account.Account;
import io.proximax.sdk.model.blockchain.NetworkType;

/**
* TODO add proper description
*/
class BlockchainHelperTest {
private BlockchainApi api;
private BlockchainHelper helper;

@BeforeEach
void init() throws MalformedURLException {
// specify both URL and network type so the network does not need to be accessed unless really needed
api = new BlockchainApi(new URL("http://localhost:3000"), NetworkType.MAIN_NET);
helper = new BlockchainHelper(api);
}

@AfterEach
void cleanup() {
this.api = null;
this.helper = null;
}

@Test
void testAccounts() {
// make sure that when asking for 5 account 5 accounts are returned
assertEquals(5, helper.accountRandom(5).size());
}

@Test
@Disabled("not proper test - but cool anyway!")
void findAddress() {
// note this expects MAIN_NET as addresses there start by X
// find account with address starting by XCHG (1M attempts should not take too long)
Optional<Account> account = helper.accountRandom().parallel().limit(1_000_000).filter(acc -> acc.getAddress().plain().startsWith("XCHG")).findFirst();
assertTrue(account.isPresent(), "Account was not found :(");
System.out.println(account.orElseThrow(() -> new IllegalStateException("account not found :(")));
}

}

0 comments on commit 2e84280

Please sign in to comment.