This guide will show you how to issue transactions from a multisig account.
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.
Complete sending tokens and messages between two accounts guide.
Complete converting an account to multisig guide.
Load the 1-of-2 multisig account with 10 bitxor
units.
Load Bob’s account with enough bitxor
units to pay for the transactions fees.
Log in to one of the cosignatory accounts of the multisig.
Click on the “transfer” tab.
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”
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.
Once the transaction is confirmed, you should see changes in the respective account balances.
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();
Define the a TransferTransaction as follows:
Property |
Value |
---|---|
Type |
TransferTransaction |
Recipient |
Address of the account that will receive the transaction |
Tokens |
[10 |
Message |
sending 10 |
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();
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();
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();
}
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.
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();
}
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
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.