Cosigning aggregate bonded transactions automatically

Create a bot to cosign automatically transactions that require your account’s signature.

Prerequisites

Developing the bot

  1. Create a function to cosign any AggregateBondedTransaction.
const cosignAggregateBondedTransaction = (transaction: AggregateTransaction, account: Account): CosignatureSignedTransaction => {
    const cosignatureTransaction = CosignatureTransaction.create(transaction);
    return account.signCosignatureTransaction(cosignatureTransaction);
};
const cosignAggregateBondedTransaction = (transaction, account) => {
    const cosignatureTransaction = symbol_sdk_1.CosignatureTransaction.create(transaction);
    return account.signCosignatureTransaction(cosignatureTransaction);
};
            BiFunction<AggregateTransaction, Account, CosignatureSignedTransaction> cosignAggregateBondedTransaction = ((transaction, account) -> CosignatureTransaction
                .create(transaction).signWith(account));
  1. Create a new Listener to get notified every time a new AggregateBondedTransaction requires the signature of your account.
  2. Open the connection. You only need to open the connection once and then connect to all desired channels.
  3. Start listening for new transactions, subscribing to the aggregateBondedAdded channel using your account’s address.

Note

To automatically sign aggregate bonded transactions that must be signed by multisig cosignatories, refer to the multisig address instead.See how to get multisig accounts where an account is cosignatory.

5. For each received transaction, check if you have signed it. At this point, you might want to do some extra checks, like verifying the contents of the transaction. Cosign each pending AggregateBondedTransaction using the previously created function.

  1. Announce CosignatureSignedTransaction to the network using the TransactionHttp repository.
// replace with network type
const networkType = NetworkType.TEST_NET;
// replace with private key
const privateKey = '0000000000000000000000000000000000000000000000000000000000000000';
const account = Account.createFromPrivateKey(privateKey, networkType);
// replace with node endpoint
const nodeUrl = 'http://api-01.us-east-1.096x.symboldev.network:3000';
const repositoryFactory = new RepositoryFactoryHttp(nodeUrl);
const transactionHttp = repositoryFactory.createTransactionRepository();
const listener = repositoryFactory.createListener();

listener.open().then(() => {
    listener
        .aggregateBondedAdded(account.address)
        .pipe(
            filter((_) => !_.signedByAccount(account.publicAccount)),
            map((transaction) => cosignAggregateBondedTransaction(transaction, account)),
            mergeMap((signedCosignatureTransaction) => {
                listener.close();
                return transactionHttp.announceAggregateBondedCosignature(signedCosignatureTransaction);
            }),
        )
        .subscribe((announcedTransaction) => {
            console.log(announcedTransaction);
            listener.close();
        }, (err) => console.error(err));
});
// replace with network type
const networkType = symbol_sdk_1.NetworkType.TEST_NET;
// replace with private key
const privateKey = '0000000000000000000000000000000000000000000000000000000000000000';
const account = symbol_sdk_1.Account.createFromPrivateKey(privateKey, networkType);
// replace with node endpoint
const nodeUrl = 'http://api-01.us-east-1.096x.symboldev.network:3000';
const repositoryFactory = new symbol_sdk_1.RepositoryFactoryHttp(nodeUrl);
const transactionHttp = repositoryFactory.createTransactionRepository();
const listener = repositoryFactory.createListener();
listener.open().then(() => {
    listener
        .aggregateBondedAdded(account.address)
        .pipe(operators_1.filter((_) => !_.signedByAccount(account.publicAccount)), operators_1.map((transaction) => cosignAggregateBondedTransaction(transaction, account)), operators_1.mergeMap((signedCosignatureTransaction) => {
        listener.close();
        return transactionHttp.announceAggregateBondedCosignature(signedCosignatureTransaction);
    }))
        .subscribe((announcedTransaction) => {
        console.log(announcedTransaction);
        listener.close();
    }, (err) => console.error(err));
});
            NetworkType networkType = repositoryFactory.getNetworkType().toFuture().get();
            // replace with cosigner private key
            String privateKey = "";
            Account account = Account.createFromPrivateKey(privateKey, networkType);

            TransactionRepository transactionRepository = repositoryFactory.createTransactionRepository();

            try (Listener listener = repositoryFactory.createListener()) {
                listener.open().get();
                listener.aggregateBondedAdded(account.getAddress())
                    .filter(a -> a.signedByAccount(account.getPublicAccount()))
                    .map(a -> cosignAggregateBondedTransaction.apply(a, account))
                    .flatMap(transactionRepository::announceAggregateBondedCosignature).toFuture().get();
            }

What’s next?

Extend the previous function to cosign transactions if they follow some constraints. For example, adapt your bot to only cosign aggregate transactions matching the following conditions:

  • The aggregate has two inner transactions.
  • The inner transactions must be transfer transactions.
  • The transaction sending funds must have yourself as the signer.
  • The transaction sending funds should have only one mosaic, being this less than 100 symbol.xym.

Here you have a possible implementation:

const validTransaction = (transaction: Transaction, publicAccount: PublicAccount): boolean => {
    return transaction instanceof TransferTransaction &&
        transaction.signer!.equals(publicAccount) &&
        transaction.mosaics.length === 1 &&
        (transaction.mosaics[0].id.equals(new MosaicId('5E62990DCAC5BE8A') ||
            transaction.mosaics[0].id.equals(new NamespaceId('symbol.xym')))) &&
        transaction.mosaics[0].amount.compare(UInt64.fromUint(100 * Math.pow(10, 6))) < 0;
};

const cosignAggregateBondedTransaction = (transaction: AggregateTransaction, account: Account): CosignatureSignedTransaction => {
    const cosignatureTransaction = CosignatureTransaction.create(transaction);
    return account.signCosignatureTransaction(cosignatureTransaction);
};

// replace with network type
const networkType = NetworkType.TEST_NET;
// replace with private key
const privateKey = '0000000000000000000000000000000000000000000000000000000000000000';
const account = Account.createFromPrivateKey(privateKey, networkType);
// replace with node endpoint
const nodeUrl = 'http://api-01.us-east-1.096x.symboldev.network:3000';
const repositoryFactory = new RepositoryFactoryHttp(nodeUrl);
const transactionHttp = repositoryFactory.createTransactionRepository();
const listener = repositoryFactory.createListener();

listener.open().then(() => {
    listener
        .aggregateBondedAdded(account.address)
        .pipe(
            filter((_) => _.innerTransactions.length === 2),
            filter((_) => !_.signedByAccount(account.publicAccount)),
            filter((_) => validTransaction(_.innerTransactions[0], account.publicAccount)
                || validTransaction(_.innerTransactions[1], account.publicAccount)),
            map((transaction) => cosignAggregateBondedTransaction(transaction, account)),
            mergeMap((signedCosignatureTransaction) => transactionHttp.announceAggregateBondedCosignature(signedCosignatureTransaction)),
        )
        .subscribe((announcedTransaction) => {
                console.log(announcedTransaction);
                listener.close();
            },
            (err) => console.error(err));
});
const validTransaction = (transaction, publicAccount) => {
    return transaction instanceof symbol_sdk_1.TransferTransaction &&
        transaction.signer.equals(publicAccount) &&
        transaction.mosaics.length === 1 &&
        (transaction.mosaics[0].id.equals(new symbol_sdk_1.MosaicId('5E62990DCAC5BE8A') ||
            transaction.mosaics[0].id.equals(new symbol_sdk_1.NamespaceId('symbol.xym')))) &&
        transaction.mosaics[0].amount.compare(symbol_sdk_1.UInt64.fromUint(100 * Math.pow(10, 6))) < 0;
};
const cosignAggregateBondedTransaction = (transaction, account) => {
    const cosignatureTransaction = symbol_sdk_1.CosignatureTransaction.create(transaction);
    return account.signCosignatureTransaction(cosignatureTransaction);
};
// replace with network type
const networkType = symbol_sdk_1.NetworkType.TEST_NET;
// replace with private key
const privateKey = '0000000000000000000000000000000000000000000000000000000000000000';
const account = symbol_sdk_1.Account.createFromPrivateKey(privateKey, networkType);
// replace with node endpoint
const nodeUrl = 'http://api-01.us-east-1.096x.symboldev.network:3000';
const repositoryFactory = new symbol_sdk_1.RepositoryFactoryHttp(nodeUrl);
const transactionHttp = repositoryFactory.createTransactionRepository();
const listener = repositoryFactory.createListener();
listener.open().then(() => {
    listener
        .aggregateBondedAdded(account.address)
        .pipe(operators_1.filter((_) => _.innerTransactions.length === 2), operators_1.filter((_) => !_.signedByAccount(account.publicAccount)), operators_1.filter((_) => validTransaction(_.innerTransactions[0], account.publicAccount)
        || validTransaction(_.innerTransactions[1], account.publicAccount)), operators_1.map((transaction) => cosignAggregateBondedTransaction(transaction, account)), operators_1.mergeMap((signedCosignatureTransaction) => transactionHttp.announceAggregateBondedCosignature(signedCosignatureTransaction)))
        .subscribe((announcedTransaction) => {
        console.log(announcedTransaction);
        listener.close();
    }, (err) => console.error(err));
});
            NetworkCurrency networkCurrency = repositoryFactory.getNetworkCurrency().toFuture().get();

            BiFunction<Transaction, PublicAccount, Boolean> validTransaction = ((transaction, account) -> {
                if (transaction instanceof TransferTransaction) {
                    return false;
                }
                if (transaction.getSigner().map(s -> !s.equals(account)).orElse(true)) {
                    return false;
                }

                TransferTransaction transferTransaction = (TransferTransaction) transaction;
                return transferTransaction.getMosaics().size() == 1 && transferTransaction.getMosaics().stream()
                    .allMatch(m -> {
                        BigInteger maxBalance = networkCurrency.createAbsolute(BigInteger.valueOf(100)).getAmount();
                        if (m.getAmount().compareTo(maxBalance) > 0) {
                            return false;
                        }
                        return networkCurrency.getMosaicId().map(mosaicId -> mosaicId.equals(m.getId())).orElse(false)
                            || networkCurrency.getNamespaceId().map(namespaceId -> namespaceId.equals(m.getId()))
                            .orElse(false);
                    });

            });

            BiFunction<AggregateTransaction, Account, CosignatureSignedTransaction> cosignAggregateBondedTransaction = ((transaction, account) -> CosignatureTransaction
                .create(transaction).signWith(account));

            NetworkType networkType = repositoryFactory.getNetworkType().toFuture().get();
            // replace with cosigner private key
            String privateKey = "";
            Account account = Account.createFromPrivateKey(privateKey, networkType);

            TransactionRepository transactionRepository = repositoryFactory.createTransactionRepository();

            try (Listener listener = repositoryFactory.createListener()) {
                listener.open().get();
                listener.aggregateBondedAdded(account.getAddress())
                    .filter(a -> a.signedByAccount(account.getPublicAccount())).filter(
                    a -> a.getInnerTransactions().stream()
                        .anyMatch(t -> validTransaction.apply(t, account.getPublicAccount())))
                    .map(a -> cosignAggregateBondedTransaction.apply(a, account))
                    .flatMap(transactionRepository::announceAggregateBondedCosignature).toFuture().get();
            }