Sending a multisig transaction

This guide will show you how to issue transactions from a multisig account.

Use case

../../_images/multisig-transaction-1-of-2.png

Sending an AggregateCompleteTransaction

In this example, Alice and Bob are cosignatories of a 1-of-2 multisig account. This multisig configuration permits Alice and Bob to share funds in a separate account, requiring only the signature from one of them to transact.

Let’s send 10 bitxor from the shared account to a third address.

Prerequisites

Method #01: Using the Desktop Wallet

  1. Log in to one of the cosignatory accounts of the multisig.

  2. Click on the “transfer” tab.

  3. Enter the appropriate information for your transfer transaction. Select the multisig account from the dropdown menu of the “FROM” field. Enter the address of the recipient. Select the token you desire to send and the amount. Click “Send”. Review the information on the popup and enter your wallet password. Click “Confirm

resources/images/screenshots/multisig-transaction-1.gif
  1. If the multisig account has the “minimum approval” set to a number greater than 1, log in to another cosignatory account and cosign the transaction. Repeat this step until the minimum approval number is satisfied.

resources/images/screenshots/add-signer-2.gif
  1. Once the transaction is confirmed, you should see changes in the respective account balances.

Method #02: Using the SDK

  1. Define the private key of one of the multisig cosignatories in a new variable. Then, define the public key of the shared account.

// replace network type
const networkType = NetworkType.TEST_NET;
// replace with cosignatory private key
const cosignatoryPrivateKey =
  '0000000000000000000000000000000000000000000000000000000000000000';
const cosignatoryAccount = Account.createFromPrivateKey(
  cosignatoryPrivateKey,
  networkType,
);
// replace with multisig account public key
const multisigAccountPublicKey =
  '3A537D5A1AF51158C42F80A199BB58351DBF3253C4A6A1B7BD1014682FB595EA';
const multisigAccount = PublicAccount.createFromPublicKey(
  multisigAccountPublicKey,
  networkType,
);
// replace with recipient address
const recipientRawAddress = 'BXRYXK-VYBMO4-NBCUF3-AXKJMX-CGVSYQ-OS7ZG2-TLI';
const recipientAddress = Address.createFromRawAddress(recipientRawAddress);
// replace with bitxor id
const networkCurrencyTokenId = new TokenId('5E62990DCAC5BE8A');
// replace with network currency divisibility
const networkCurrencyDivisibility = 6;
// replace network type
const networkType = bitxor_sdk_1.NetworkType.TEST_NET;
// replace with cosignatory private key
const cosignatoryPrivateKey =
    '0000000000000000000000000000000000000000000000000000000000000000';
const cosignatoryAccount = bitxor_sdk_1.Account.createFromPrivateKey(
    cosignatoryPrivateKey,
    networkType,
);
// replace with multisig account public key
const multisigAccountPublicKey =
    '3A537D5A1AF51158C42F80A199BB58351DBF3253C4A6A1B7BD1014682FB595EA';
const multisigAccount = bitxor_sdk_1.PublicAccount.createFromPublicKey(
    multisigAccountPublicKey,
    networkType,
);
// replace with recipient address
const recipientRawAddress = 'BXRYXK-VYBMO4-NBCUF3-AXKJMX-CGVSYQ-OS7ZG2-TLI';
const recipientAddress = bitxor_sdk_1.Address.createFromRawAddress(
    recipientRawAddress,
);
// replace with bitxor id
const networkCurrencyTokenId = new bitxor_sdk_1.TokenId('5E62990DCAC5BE8A');
// replace with network currency divisibility
const networkCurrencyDivisibility = 6;
            NetworkType networkType = repositoryFactory.getNetworkType().toFuture().get();

            // replace with cosignatory private key
            String cosignatoryPrivateKey = "";
            Account cosignatoryAccount = Account.createFromPrivateKey(cosignatoryPrivateKey, networkType);

            // replace with cosignatory private key
            String multisigAccountPublicKey = "";
            Account multisigAccount = Account.createFromPrivateKey(multisigAccountPublicKey, networkType);

            // replace with recipient address
            String recipientRawAddress = "BXRYXK-VYBMO4-NBCUF3-AXKJMX-CGVSYQ-OS7ZG2-TLI";
            Address recipientAddress = Address.createFromRawAddress(recipientRawAddress);

            NetworkCurrency networkCurrency = repositoryFactory.getNetworkCurrency().toFuture().get();
  1. Define the a TransferTransaction as follows:

Property

Value

Type

TransferTransaction

Recipient

Address of the account that will receive the transaction

Tokens

[10 bitxor]

Message

sending 10 bitxor

const transferTransaction = TransferTransaction.create(
  Deadline.create(epochAdjustment),
  recipientAddress,
  [
    new Token(
      networkCurrencyTokenId,
      UInt64.fromUint(10 * Math.pow(10, networkCurrencyDivisibility)),
    ),
  ],
  PlainMessage.create('sending 10 bitxor'),
  networkType,
);
const transferTransaction = bitxor_sdk_1.TransferTransaction.create(
    bitxor_sdk_1.Deadline.create(epochAdjustment),
    recipientAddress, [
        new bitxor_sdk_1.Token(
            networkCurrencyTokenId,
            bitxor_sdk_1.UInt64.fromUint(
                10 * Math.pow(10, networkCurrencyDivisibility),
            ),
        ),
    ],
    bitxor_sdk_1.PlainMessage.create('sending 10 bitxor'),
    networkType,
);
            TransferTransaction transferTransaction = TransferTransactionFactory.create(networkType, recipientAddress,
                Collections.singletonList(networkCurrency.createRelative(BigInteger.valueOf(10))),
                PlainMessage.create("sending 10 bitxor")).build();
  1. Wrap the TransferTransaction in an Aggregate Transaction, attaching the multisig public key as the signer of the transaction.

const aggregateTransaction = AggregateTransaction.createComplete(
  Deadline.create(epochAdjustment),
  [transferTransaction.toAggregate(multisigAccount)],
  networkType,
  [],
  UInt64.fromUint(2000000),
);
const aggregateTransaction = bitxor_sdk_1.AggregateTransaction.createComplete(
    bitxor_sdk_1.Deadline.create(epochAdjustment), [transferTransaction.toAggregate(multisigAccount)],
    networkType, [],
    bitxor_sdk_1.UInt64.fromUint(2000000),
);
            AggregateTransaction aggregateTransaction = AggregateTransactionFactory.createComplete(networkType,
                Collections.singletonList(transferTransaction.toAggregate(multisigAccount.getPublicAccount())))
                .maxFee(BigInteger.valueOf(2000000)).build();
  1. Then, sign and announce the transaction with a cosignatory account.

// replace with meta.networkGenerationHash (nodeUrl + '/node/info')
const networkGenerationHash =
  '1DFB2FAA9E7F054168B0C5FCB84F4DEB62CC2B4D317D861F3168D161F54EA78B';
const signedTransaction = cosignatoryAccount.sign(
  aggregateTransaction,
  networkGenerationHash,
);
// replace with node endpoint
const nodeUrl = 'NODE_URL';
const repositoryFactory = new RepositoryFactoryHttp(nodeUrl);
const transactionHttp = repositoryFactory.createTransactionRepository();
transactionHttp.announce(signedTransaction).subscribe(
  (x) => console.log(x),
  (err) => console.error(err),
);
// replace with meta.networkGenerationHash (nodeUrl + '/node/info')
const networkGenerationHash =
    '1DFB2FAA9E7F054168B0C5FCB84F4DEB62CC2B4D317D861F3168D161F54EA78B';
const signedTransaction = cosignatoryAccount.sign(
    aggregateTransaction,
    networkGenerationHash,
);
// replace with node endpoint
const nodeUrl = 'NODE_URL';
const repositoryFactory = new bitxor_sdk_1.RepositoryFactoryHttp(nodeUrl);
const transactionHttp = repositoryFactory.createTransactionRepository();
transactionHttp.announce(signedTransaction).subscribe(
    (x) => console.log(x),
    (err) => console.error(err),
);
            String generationHash = repositoryFactory.getGenerationHash().toFuture().get();
            SignedTransaction signedTransaction = cosignatoryAccount.sign(aggregateTransaction, generationHash);

            try (Listener listener = repositoryFactory.createListener()) {
                listener.open().get();
                TransactionService transactionService = new TransactionServiceImpl(repositoryFactory);
                transactionService.announce(listener, signedTransaction).toFuture().get();
            }
  1. The recipient of the transction should receive the funds once the funds are confirmed.

This time, the TransferTransaction was wrapped in an AggregateCompleteTransaction because just one account was required to announce the transaction. If more than one cosignature is required to announce the transaction (e.g., the multisig is a 2-of-2 account), the transaction must be defined as aggregate bonded, and all other required multisig participants should cosign it in order to be confirmed.

  1. To issue a transaction from a 2-of-2 multisig, modify the previous code and define the transaction as bonded.

const aggregateTransaction = AggregateTransaction.createBonded(
  Deadline.create(epochAdjustment),
  [transferTransaction.toAggregate(multisigAccount)],
  networkType,
  [],
  UInt64.fromUint(2000000),
);

// replace with meta.networkGenerationHash (nodeUrl + '/node/info')
const networkGenerationHash =
  '1DFB2FAA9E7F054168B0C5FCB84F4DEB62CC2B4D317D861F3168D161F54EA78B';
const signedTransaction = cosignatoryAccount.sign(
  aggregateTransaction,
  networkGenerationHash,
);
console.log(signedTransaction.hash);
const aggregateTransaction = bitxor_sdk_1.AggregateTransaction.createBonded(
    bitxor_sdk_1.Deadline.create(epochAdjustment), [transferTransaction.toAggregate(multisigAccount)],
    networkType, [],
    bitxor_sdk_1.UInt64.fromUint(2000000),
);
// replace with meta.networkGenerationHash (nodeUrl + '/node/info')
const networkGenerationHash =
    '1DFB2FAA9E7F054168B0C5FCB84F4DEB62CC2B4D317D861F3168D161F54EA78B';
const signedTransaction = cosignatoryAccount.sign(
    aggregateTransaction,
    networkGenerationHash,
);
console.log(signedTransaction.hash);
            NetworkType networkType = repositoryFactory.getNetworkType().toFuture().get();

            // replace with cosignatory private key
            String cosignatoryPrivateKey = "";
            Account cosignatoryAccount = Account.createFromPrivateKey(cosignatoryPrivateKey, networkType);

            // replace with cosignatory private key
            String multisigAccountPublicKey = "";
            Account multisigAccount = Account.createFromPrivateKey(multisigAccountPublicKey, networkType);

            // replace with recipient address
            String recipientRawAddress = "BXRYXK-VYBMO4-NBCUF3-AXKJMX-CGVSYQ-OS7ZG2-TLI";
            Address recipientAddress = Address.createFromRawAddress(recipientRawAddress);

            NetworkCurrency networkCurrency = repositoryFactory.getNetworkCurrency().toFuture().get();

            TransferTransaction transferTransaction = TransferTransactionFactory.create(networkType, recipientAddress,
                Collections.singletonList(networkCurrency.createRelative(BigInteger.valueOf(10))),
                PlainMessage.create("sending 10 bitxor")).build();

            AggregateTransaction aggregateTransaction = AggregateTransactionFactory.createBonded(networkType,
                Collections.singletonList(transferTransaction.toAggregate(multisigAccount.getPublicAccount())))
                .maxFee(BigInteger.valueOf(2000000)).build();

            String generationHash = repositoryFactory.getGenerationHash().toFuture().get();
            SignedTransaction signedTransaction = cosignatoryAccount.sign(aggregateTransaction, generationHash);

2. When an AggregateTransaction is bonded, an account needs to lock at least 10 bitxor to prevent spamming the network. Once all cosigners sign the transaction, the amount of bitxor locked becomes available again in the account that has locked the funds. After HashLockTransaction has been confirmed, announce the AggregateBondedTransaction with a cosignatory. In our case, we will sign the transaction with Bob’s account.

const hashLockTransaction = HashLockTransaction.create(
  Deadline.create(epochAdjustment),
  new Token(
    networkCurrencyTokenId,
    UInt64.fromUint(10 * Math.pow(10, networkCurrencyDivisibility)),
  ),
  UInt64.fromUint(480),
  signedTransaction,
  networkType,
  UInt64.fromUint(2000000),
);

const signedHashLockTransaction = cosignatoryAccount.sign(
  hashLockTransaction,
  networkGenerationHash,
);

// replace with node endpoint
const nodeUrl = 'NODE_URL';
const repositoryFactory = new RepositoryFactoryHttp(nodeUrl);
const listener = repositoryFactory.createListener();
const receiptHttp = repositoryFactory.createReceiptRepository();
const transactionHttp = repositoryFactory.createTransactionRepository();
const transactionService = new TransactionService(transactionHttp, receiptHttp);

listener.open().then(() => {
  transactionService
    .announceHashLockAggregateBonded(
      signedHashLockTransaction,
      signedTransaction,
      listener,
    )
    .subscribe(
      (x) => console.log(x),
      (err) => console.log(err),
      () => listener.close(),
    );
});
const hashLockTransaction = bitxor_sdk_1.HashLockTransaction.create(
    bitxor_sdk_1.Deadline.create(epochAdjustment),
    new bitxor_sdk_1.Token(
        networkCurrencyTokenId,
        bitxor_sdk_1.UInt64.fromUint(
            10 * Math.pow(10, networkCurrencyDivisibility),
        ),
    ),
    bitxor_sdk_1.UInt64.fromUint(480),
    signedTransaction,
    networkType,
    bitxor_sdk_1.UInt64.fromUint(2000000),
);
const signedHashLockTransaction = cosignatoryAccount.sign(
    hashLockTransaction,
    networkGenerationHash,
);
// replace with node endpoint
const nodeUrl = 'NODE_URL';
const repositoryFactory = new bitxor_sdk_1.RepositoryFactoryHttp(nodeUrl);
const listener = repositoryFactory.createListener();
const receiptHttp = repositoryFactory.createReceiptRepository();
const transactionHttp = repositoryFactory.createTransactionRepository();
const transactionService = new bitxor_sdk_1.TransactionService(
    transactionHttp,
    receiptHttp,
);
listener.open().then(() => {
    transactionService
        .announceHashLockAggregateBonded(
            signedHashLockTransaction,
            signedTransaction,
            listener,
        )
        .subscribe(
            (x) => console.log(x),
            (err) => console.log(err),
            () => listener.close(),
        );
});
            HashLockTransaction hashLockTransaction = HashLockTransactionFactory
                .create(networkType, networkCurrency.createRelative(BigDecimal.valueOf(10)), BigInteger.valueOf(480),
                    signedTransaction).build();

            SignedTransaction signedHashLockTransaction = cosignatoryAccount.sign(hashLockTransaction, generationHash);

            try (Listener listener = repositoryFactory.createListener()) {
                listener.open().get();
                TransactionService transactionService = new TransactionServiceImpl(repositoryFactory);

                transactionService
                    .announceHashLockAggregateBonded(listener, signedHashLockTransaction, signedTransaction).toFuture()
                    .get();
            }
  1. Once the transaction reaches the network, every other multisig cosignatory required to reach quorum must cosign the transaction.

To cosign the transaction, you can use the CLI command transaction cosign, replacing the transaction hash from (2).

bitxor-cli transaction cosign --hash A6A374E66B32A3D5133018EFA9CD6E3169C8EEA339F7CCBE29C47D07086E068C --profile alice
  1. The recipient should receive the funds once the transaction is cosigned by at least minApproval cosignatories. Besides, the account that has locked the 10 bitxor should have received the locked funds back.