Modifying a multisig account
Modify an existing multisig account.
First, you are going to turn a 1-of-2 multisig account into a 2-of-2. Then, you will add a new cosignatory, becoming a 2-of-3. Finally, after removing a cosignatory, you will know how to perform all sort of modifications to multisig accounts.
Prerequisites
- Text editor or IDE
- XPX-Chain-SDK or XPX-Chain-CLI
- Finish converting an account to multisig guide
- Have on emultisignature account
Getting into some code
Editing minApproval
Alice and Bob are cosignatories of the 1-of-2 multisig account. This means that at least one of their account’s signatures is required to authorize multisig transactions. In other words, we can say that the minApproval
parameter of the multisig is currently set to 1
.
Multisig accounts are editable at the blockchain level. In this case, we want to make both cosignatories required, shifting to a 2-of-2 multisig instead. You can achieve this by increasing minApproval
parameter in one unit.
2-of-2 multisig account example
One of the accounts, for example Alice’s, will announce a modify multisig account transaction to increase minApprovalDelta.
- First, define Alice account as the cosignatory and the multisig account using its public key.
conf, err := sdk.NewConfig(context.Background(), []string{"http://localhost:3000"})
if err != nil {
panic(err)
}
// Use the default http client
client := sdk.NewClient(nil, conf)
multisig, err := client.NewAccountFromPublicKey(os.Getenv("MULTISIG_ACCOUNT_PUBLIC_KEY"))
if err != nil {
panic(err)
}
cosignatory, err := client.NewAccountFromPrivateKey(os.Getenv("COSIGNATORY_PRIVATE_KEY"))
if err != nil {
panic(err)
}
const transactionHttp = new TransactionHttp('http://localhost:3000');
const cosignatoryPrivateKey = "<cosignatoryPrivateKey>";
const cosignatoryAccount = Account.createFromPrivateKey(cosignatoryPrivateKey, NetworkType.TEST_NET);
const multisigAccountPublicKey = "<multisigPublicKey>";
const multisigAccount = PublicAccount.createFromPublicKey(multisigAccountPublicKey, NetworkType.TEST_NET);
const transactionHttp = new TransactionHttp('http://localhost:3000');
const cosignatoryPrivateKey = "<cosignatoryPrivateKey>";
const cosignatoryAccount = Account.createFromPrivateKey(cosignatoryPrivateKey, NetworkType.TEST_NET);
const multisigAccountPublicKey = "<multisigPublicKey>";
const multisigAccount = PublicAccount.createFromPublicKey(multisigAccountPublicKey, NetworkType.TEST_NET);
final TransactionHttp transactionHttp = new TransactionHttp("http://localhost:3000");
final String cosignatoryPrivateKey = "<cosignatoryPrivateKey>";
final String multisigAccountPublicKey = "<multisigPublicKey>";
final Account cosignatoryAccount = Account.createFromPrivateKey(cosignatoryPrivateKey, NetworkType.TEST_NET);
final PublicAccount multisigAccount = PublicAccount.createFromPublicKey(multisigAccountPublicKey, NetworkType.TEST_NET);
- Define a modify multisig account transaction to increase the
minAprovalDelta
in one unit.
modifyMultisigAccountTransaction, err := client.NewModifyMultisigAccountTransaction(
sdk.NewDeadline(time.Hour),
1,
0,
[]*sdk.MultisigCosignatoryModification{ },
)
if err != nil {
panic(err)
}
const modifyMultisigAccountTransaction = ModifyMultisigAccountTransaction.create(
Deadline.create(),
1, // min to Approve
0, // min to remove cosignatory
[],
NetworkType.TEST_NET);
const modifyMultisigAccountTransaction = ModifyMultisigAccountTransaction.create(
Deadline.create(),
1, // min to Approve
0, // min to remove cosignatory
[],
NetworkType.TEST_NET);
final ModifyMultisigAccountTransaction modifyMultisigAccountTransaction = ModifyMultisigAccountTransaction.create(
Deadline.create(2, HOURS),
1, // min to Approve
0, // min to remove cosignatory
Collections.emptyList(),
NetworkType.TEST_NET
);
- Wrap the modify multisig account transaction in an aggregate transaction, attaching the multisig public key as the signer.
An aggregate transaction is complete
if, before announcing it to the network, all required cosignatories have signed it. If valid, it will be included in a block.
As only one cosignature is required (1-of-2), Alice can sign the transaction and announce it to the network.
modifyMultisigAccountTransaction.ToAggregate(multisig)
aggregateTransaction, err := client.NewCompleteAggregateTransaction(sdk.NewDeadline(time.Hour), []sdk.Transaction{modifyMultisigAccountTransaction})
if err != nil {
panic(err)
}
signedTransaction, err := cosignatory.Sign(aggregateTransaction)
if err != nil {
panic(err)
}
_, err = client.Transaction.Announce(context.Background(), signedTransaction)
if err != nil {
panic(err)
}
const aggregateTransaction = AggregateTransaction.createComplete(
Deadline.create(),
[modifyMultisigAccountTransaction.toAggregate(multisigAccount)],
NetworkType.TEST_NET,
[]);
const signedTransaction = cosignatoryAccount.sign(aggregateTransaction, generationHash);
transactionHttp
.announce(signedTransaction)
.subscribe(x => console.log(x), err => console.error(err));
const aggregateTransaction = AggregateTransaction.createComplete(
Deadline.create(),
[modifyMultisigAccountTransaction.toAggregate(multisigAccount)],
NetworkType.TEST_NET,
[]);
const signedTransaction = cosignatoryAccount.sign(aggregateTransaction, generationHash);
transactionHttp
.announce(signedTransaction)
.subscribe(x => console.log(x), err => console.error(err));
final AggregateTransaction aggregateTransaction = new TransactionBuilderFactory().aggregateComplete()
.innerTransactions(Arrays.asList(
modifyMultisigAccountTransaction.toAggregate(multisigAccount)
)).deadline(new Deadline(2, ChronoUnit.HOURS)).networkType(NetworkType.TEST_NET);
final SignedTransaction signedTransaction = cosignatoryAccount.sign(aggregateTransaction, generationHash);
transactionHttp.announce(signedTransaction).toFuture().get();
Once confirmed, the minApproval
value of the multisig will be set to 2, having our 2-of-2 multisig.
Note:
If you want to decrease the minApproval
parameter, set minApprovalDelta
with a negative value. In this case -1
.
Adding a new cosignatory
Alice and Bob want to add Carol as a cosignatory of the multisig account to achieve 2-of-3 cosignatures required.
2-of-3 multisig account example
- Create a modify multisig account transaction adding Carol as a cosignatory. The multisig account will become a 2-of-3, since you are not increasing the
minApprovalDelta
.
conf, err := sdk.NewConfig(context.Background(), []string{"http://localhost:3000"})
if err != nil {
panic(err)
}
// Use the default http client
client := sdk.NewClient(nil, conf)
multisig, err := client.NewAccountFromPublicKey(os.Getenv("MULTISIG_ACCOUNT_PUBLIC_KEY"))
if err != nil {
panic(err)
}
cosignatory, err := client.NewAccountFromPrivateKey(os.Getenv("COSIGNATORY_PRIVATE_KEY"))
if err != nil {
panic(err)
}
newCosignatory, err := client.NewAccountFromPrivateKey(os.Getenv("NEW_COSIGNATORY_PRIVATE_KEY"))
if err != nil {
panic(err)
}
multisigCosignatoryModification := sdk.MultisigCosignatoryModification{sdk.Add, newCosignatory.PublicAccount}
const nodeUrl = 'http://localhost:3000';
const transactionHttp = new TransactionHttp(nodeUrl);
const listener = new Listener(nodeUrl);
const cosignatoryPrivateKey = "<cosignatoryPrivateKey>";
const cosignatoryAccount = Account.createFromPrivateKey(cosignatoryPrivateKey, NetworkType.TEST_NET);
const multisigAccountPublicKey = "<multisigPublicKey>";
const multisigAccount = PublicAccount.createFromPublicKey(multisigAccountPublicKey, NetworkType.TEST_NET);
const newCosignatoryPublicKey = "<newCosignatoryPublicKey>";
const newCosignatoryAccount = PublicAccount.createFromPublicKey(newCosignatoryPublicKey, NetworkType.TEST_NET);
const multisigCosignatoryModification = new MultisigCosignatoryModification(MultisigCosignatoryModificationType.Add,newCosignatoryAccount);
const nodeUrl = 'http://localhost:3000';
const transactionHttp = new TransactionHttp(nodeUrl);
const listener = new Listener(nodeUrl);
const cosignatoryPrivateKey = "<cosignatoryPrivateKey>";
const cosignatoryAccount = Account.createFromPrivateKey(cosignatoryPrivateKey, NetworkType.TEST_NET);
const multisigAccountPublicKey = "<multisigPublicKey>";
const multisigAccount = PublicAccount.createFromPublicKey(multisigAccountPublicKey, NetworkType.TEST_NET);
const newCosignatoryPublicKey = "<newCosignatoryPublicKey>";
const newCosignatoryAccount = PublicAccount.createFromPublicKey(newCosignatoryPublicKey, NetworkType.TEST_NET);
const multisigCosignatoryModification = new MultisigCosignatoryModification(MultisigCosignatoryModificationType.Add,newCosignatoryAccount);
// Replace with the multisig public key
final String cosignatoryPrivateKey = "<cosignatoryPrivateKey>";
final String multisigAccountPublicKey = "<multisigPublicKey>";
final String newCosignatoryPublicKey = "<newCosignatoryPublicKey>";
final Account cosignatoryAccount = Account.createFromPrivateKey(cosignatoryPrivateKey, NetworkType.TEST_NET);
final PublicAccount newCosignatoryAccount = PublicAccount.createFromPublicKey(newCosignatoryPublicKey, NetworkType.TEST_NET);
final PublicAccount multisigAccount = PublicAccount.createFromPublicKey(multisigAccountPublicKey, NetworkType.TEST_NET);
final MultisigCosignatoryModification multisigCosignatoryModification = new MultisigCosignatoryModification(
MultisigCosignatoryModificationType.ADD,
newCosignatoryAccount
);
- Create a modify multisig account transaction adding the previous modification.
modifyMultisigTransaction, err := client.NewModifyMultisigAccountTransaction(
sdk.NewDeadline(time.Hour),
0,
0,
[]*sdk.MultisigCosignatoryModification{
&multisigCosignatoryModification,
},
)
if err != nil {
panic(err)
}
const modifyMultisigAccountTransaction = ModifyMultisigAccountTransaction.create(
Deadline.create(),
0,
0,
[multisigCosignatoryModification],
NetworkType.TEST_NET);
const modifyMultisigAccountTransaction = ModifyMultisigAccountTransaction.create(
Deadline.create(),
0,
0,
[multisigCosignatoryModification],
NetworkType.TEST_NET);
final ModifyMultisigAccountTransaction modifyMultisigAccountTransaction = ModifyMultisigAccountTransaction.create(
Deadline.create(2, HOURS),
0,
0,
Collections.singletonList(multisigCosignatoryModification),
NetworkType.TEST_NET
);
- Wrap the modify multisig account transaction in an aggregate bonded transaction and sign it.
modifyMultisigTransaction.ToAggregate(multisig)
aggregateTransaction, err := client.NewBondedAggregateTransaction(sdk.NewDeadline(time.Hour), []sdk.Transaction{modifyMultisigTransaction})
if err != nil {
panic(err)
}
signedAggregateTransaction, err := cosignatory.Sign(aggregateTransaction)
if err != nil {
panic(err)
}
const aggregateTransaction = AggregateTransaction.createBonded(
Deadline.create(),
[modifyMultisigAccountTransaction.toAggregate(multisigAccount)],
NetworkType.TEST_NET);
const signedTransaction = cosignatoryAccount.sign(aggregateTransaction);
const aggregateTransaction = AggregateTransaction.createBonded(
Deadline.create(),
[modifyMultisigAccountTransaction.toAggregate(multisigAccount)],
NetworkType.TEST_NET);
const signedTransaction = cosignatoryAccount.sign(aggregateTransaction);
final AggregateTransaction aggregateTransaction = new TransactionBuilderFactory().aggregateBonded()
.innerTransactions(Arrays.asList(
modifyMultisigAccountTransaction.toAggregate(multisigAccount)
)).deadline(new Deadline(2, ChronoUnit.HOURS)).networkType(NetworkType.TEST_NET);
final SignedTransaction signedTransaction = cosignatoryAccount.sign(aggregateTransaction);
- Before sending an aggregate bonded transaction, Alice needs to lock at least
10 xpx
. This transaction is required to prevent network spamming and ensure that transactions are cosigned. After the hash lock transaction has been confirmed, announce the aggregate transaction.
lockFundsTransaction, err := client.NewLockFundsTransaction(
sdk.NewDeadline(time.Hour),
sdk.XpxRelative(10),
sdk.Duration(1000),
signedAggregateTransaction,
)
if err != nil {
panic(err)
}
signedLockFundsTransaction, err := cosignatory.Sign(lockFundsTransaction)
if err != nil {
panic(err)
}
_, err = client.Transaction.Announce(context.Background(), signedLockFundsTransaction)
if err != nil {
panic(err)
}
const lockFundsTransaction = LockFundsTransaction.create(
Deadline.create(),
NetworkCurrencyMosaic.createRelative(10),
UInt64.fromUint(1000),
signedTransaction,
NetworkType.TEST_NET);
const lockFundsTransactionSigned = cosignatoryAccount.sign(lockFundsTransaction);
listener.open().then(() => {
transactionHttp
.announce(lockFundsTransactionSigned)
.subscribe(x => console.log(x), err => console.error(err));
listener
.confirmed(cosignatoryAccount.address)
.pipe(
filter((transaction) => transaction.transactionInfo !== undefined
&& transaction.transactionInfo.hash === lockFundsTransactionSigned.hash),
mergeMap(ignored => transactionHttp.announceAggregateBonded(signedTransaction))
)
.subscribe(announcedAggregateBonded => console.log(announcedAggregateBonded),
err => console.error(err));
});
const lockFundsTransaction = LockFundsTransaction.create(
Deadline.create(),
NetworkCurrencyMosaic.createRelative(10),
UInt64.fromUint(1000),
signedTransaction,
NetworkType.TEST_NET);
const lockFundsTransactionSigned = cosignatoryAccount.sign(lockFundsTransaction);
listener.open().then(() => {
transactionHttp
.announce(lockFundsTransactionSigned)
.subscribe(x => console.log(x), err => console.error(err));
listener
.confirmed(cosignatoryAccount.address)
.pipe(
filter((transaction) => transaction.transactionInfo !== undefined
&& transaction.transactionInfo.hash === lockFundsTransactionSigned.hash),
mergeMap(ignored => transactionHttp.announceAggregateBonded(signedTransaction))
)
.subscribe(announcedAggregateBonded => console.log(announcedAggregateBonded),
err => console.error(err));
});
final LockFundsTransaction lockFundsTransaction = LockFundsTransaction.create(
Deadline.create(2, HOURS),
NetworkCurrencyMosaic.createRelative(BigInteger.valueOf(10)),
BigInteger.valueOf(1000),
signedTransaction,
NetworkType.TEST_NET
);
final SignedTransaction lockFundsTransactionSigned = cosignatoryAccount.sign(lockFundsTransaction);
final TransactionHttp transactionHttp = new TransactionHttp("http://localhost:3000");
transactionHttp.announce(lockFundsTransactionSigned).toFuture().get();
final Listener listener = new Listener("http://localhost:3000");
listener.open().get();
// listen to confirmed transaction which is the LockFundsTransaction in this case
final Transaction transaction = listener.confirmed(cosignatoryAccount.getAddress()).toFuture().get();
// announce signed transaction
transactionHttp.announceAggregateBonded(signedTransaction).toFuture().get();
- Cosign the aggregate transaction hash with Carols’s account. She has to opt-in to become a multisig cosignatory.
xpx2-cli transaction cosign --hash A6A374E66B32A3D5133018EFA9CD6E3169C8EEA339F7CCBE29C47D07086E068C --profile carol
- Cosign the aggregate transaction with Bob’s account. The amount of xpx locked becomes available again on Alice’s account and Carol is added to the multisig.
xpx2-cli transaction cosign --hash A6A374E66B32A3D5133018EFA9CD6E3169C8EEA339F7CCBE29C47D07086E068C --profile bob
Removing a cosignatory
Once you have finished this guide, delete a cosignatory from the multisig. Multisig accounts can be converted again into regular accounts by removing all cosignatories. Make sure you own the multisig private key!
The following code shows how to remove a cosignatory of a 2-of-3 multisig account with minRemoval
set to 1
. The multisig modification transaction is wrapped in an aggregate complete, as only one account is required to delete others from the multisig.
Note:
The minRemoval
parameter indicates the number of required signatures to delete an account from the multisig. You can increase or decrease it the same way you modify minApproval parameter.
conf, err := sdk.NewConfig(context.Background(), []string{"http://localhost:3000"})
if err != nil {
panic(err)
}
// Use the default http client
client := sdk.NewClient(nil, conf)
multisig, err := client.NewAccountFromPublicKey(os.Getenv("MULTISIG_ACCOUNT_PUBLIC_KEY"))
if err != nil {
panic(err)
}
cosignatory, err := client.NewAccountFromPrivateKey(os.Getenv("COSIGNATORY_PRIVATE_KEY"))
if err != nil {
panic(err)
}
cosignatoryToRemove, err := client.NewAccountFromPublicKey(os.Getenv("COSIGNATORY_PUBLIC_KEY"))
if err != nil {
panic(err)
}
multisigCosignatoryModification := sdk.MultisigCosignatoryModification{sdk.Remove, cosignatoryToRemove}
modifyMultisigAccountTransaction, err := client.NewModifyMultisigAccountTransaction(
sdk.NewDeadline(time.Hour),
0,
0,
[]*sdk.MultisigCosignatoryModification{ &multisigCosignatoryModification },
)
if err != nil {
panic(err)
}
modifyMultisigAccountTransaction.ToAggregate(multisig)
aggregateTransaction, err := client.NewCompleteAggregateTransaction(sdk.NewDeadline(time.Hour), []sdk.Transaction{modifyMultisigAccountTransaction})
if err != nil {
panic(err)
}
signedTransaction, err := cosignatory.Sign(aggregateTransaction)
if err != nil {
panic(err)
}
_, err = client.Transaction.Announce(context.Background(), signedTransaction)
if err != nil {
panic(err)
}
const transactionHttp = new TransactionHttp('http://localhost:3000');
const multisigAccountPublicKey = "<multisigPublicKey>";
const multisigAccount = PublicAccount.createFromPublicKey(multisigAccountPublicKey, NetworkType.TEST_NET);
const cosignatoryToRemovePublicKey = "<cosignatoryToRemovePublicKey>";
const cosignatoryToRemove = PublicAccount.createFromPublicKey(cosignatoryToRemovePublicKey, NetworkType.TEST_NET);
const cosignatoryPrivateKey = "<cosignatoryPrivateKey>";;
const cosignatoryAccount = Account.createFromPrivateKey(cosignatoryPrivateKey, NetworkType.TEST_NET);
const multisigCosignatoryModification = new MultisigCosignatoryModification(MultisigCosignatoryModificationType.Remove,cosignatoryToRemove);
const modifyMultisigAccountTransaction = ModifyMultisigAccountTransaction.create(
Deadline.create(),
0,
0,
[multisigCosignatoryModification],
NetworkType.TEST_NET);
const aggregateTransaction = AggregateTransaction.createComplete(
Deadline.create(),
[modifyMultisigAccountTransaction.toAggregate(multisigAccount)],
NetworkType.TEST_NET,
[]);
const signedTransaction = cosignatoryAccount.sign(aggregateTransaction, generationHash);
transactionHttp.announce(signedTransaction)
.subscribe(x => console.log(x), err => console.error(err));
const transactionHttp = new TransactionHttp('http://localhost:3000');
const multisigAccountPublicKey = "<multisigPublicKey>";
const multisigAccount = PublicAccount.createFromPublicKey(multisigAccountPublicKey, NetworkType.TEST_NET);
const cosignatoryToRemovePublicKey = "<cosignatoryToRemovePublicKey>";
const cosignatoryToRemove = PublicAccount.createFromPublicKey(cosignatoryToRemovePublicKey, NetworkType.TEST_NET);
const cosignatoryPrivateKey = "<cosignatoryPrivateKey>";
const cosignatoryAccount = Account.createFromPrivateKey(cosignatoryPrivateKey, NetworkType.TEST_NET);
const multisigCosignatoryModification = new MultisigCosignatoryModification(MultisigCosignatoryModificationType.Remove,cosignatoryToRemove);
const modifyMultisigAccountTransaction = ModifyMultisigAccountTransaction.create(
Deadline.create(),
0,
0,
[multisigCosignatoryModification],
NetworkType.TEST_NET);
const aggregateTransaction = AggregateTransaction.createComplete(
Deadline.create(),
[modifyMultisigAccountTransaction.toAggregate(multisigAccount)],
NetworkType.TEST_NET,
[]);
const signedTransaction = cosignatoryAccount.sign(aggregateTransaction, generationHash);
transactionHttp
.announce(signedTransaction)
.subscribe(x => console.log(x), err => console.error(err));
// Replace with the multisig public key
final String multisigAccountPublicKey = "<multisigPublicKey>";;
// Replace with the cosignatory private key
final String cosignatoryPrivateKey = "<cosignatoryPrivateKey>";
final Account cosignatoryAccount = Account.createFromPrivateKey(cosignatoryPrivateKey, NetworkType.TEST_NET);
final PublicAccount multisigPublicAccount = PublicAccount.createFromPublicKey(multisigAccountPublicKey, NetworkType.TEST_NET);
final MultisigCosignatoryModification multisigCosignatoryModification = new MultisigCosignatoryModification(
MultisigCosignatoryModificationType.REMOVE,
PublicAccount.createFromPublicKey("<cosignatoryToRemovePublicKey>", NetworkType.TEST_NET)
);
final ModifyMultisigAccountTransaction modifyMultisigAccountTransaction = ModifyMultisigAccountTransaction.create(
Deadline.create(2, HOURS),
0,
0,
Collections.singletonList(multisigCosignatoryModification),
NetworkType.TEST_NET
);
final AggregateTransaction aggregateTransaction = new TransactionBuilderFactory().aggregateComplete()
.innerTransactions(Arrays.asList(
modifyMultisigAccountTransaction.toAggregate(multisigPublicAccount)
)).deadline(new Deadline(2, ChronoUnit.HOURS)).networkType(NetworkType.TEST_NET);
final SignedTransaction signedTransaction = cosignatoryAccount.sign(aggregateTransaction, generationHash);
final TransactionHttp transactionHttp = new TransactionHttp("http://localhost:3000");
transactionHttp.announce(signedTransaction).toFuture().get();
What’s next?
Learn more about multi-level multisig accounts.