Signing a Transaction from a Multisignature Account⚓︎
INTERMEDIATE
This tutorial transfers 1 XEM from an account to itself, mirroring the
Transfer XEM tutorial.
However, in this case, the source account is a multisignature account, also called multisig,
and therefore it cannot initiate or sign transactions on its own.
Instead, it relies on its cosignatory accounts to create transactions and sign them on its behalf.
The multisig account used in this tutorial is configured as a 2-of-2 multisig.
It has two cosignatories, and both signatures are required to approve a transaction.
Cosignatory 0 initiates the transfer, and Cosignatory 1 provides the second required cosignature:
Alternative: WebSockets
In this tutorial, the cosignatory discovers the pending transaction by querying the node.
For a WebSocket-based approach, where the cosignatory is notified in real time, see the
Listening to Multisig Transaction Flow tutorial.
The multisig configured in that tutorial is a 1-of-2, where a single cosignatory signature is enough.
This tutorial instead requires the stricter 2-of-2 configuration described above.
After running this tutorial, remember that the configure tutorial's default disable path assumes a 1-of-2
multisig.
To disable the 2-of-2 multisig, remove cosignatory 1 first with min_approval_delta set to -1, and have
both cosignatories sign that removal.
Once confirmed, remove cosignatory 0 with min_approval_delta set to -1 as usual.
Additionally, review the Transfer XEM tutorial to understand how transactions are
announced and confirmed.
importjsonimportosimporttimeimporturllib.requestfromsymbolchain.CryptoTypesimportPrivateKey,PublicKeyfromsymbolchain.facade.NemFacadeimportNemFacadefromsymbolchain.ncimportAmountfromsymbolchain.nem.FeeCalculatorimportcalculate_transaction_feefromsymbolchain.nem.NetworkimportNetworkTimestampNODE_URL=os.getenv('NODE_URL','http://libertalia.nemtest.net:7890')print(f'Using node {NODE_URL}')# Helper function to announce a transactiondefannounce_transaction(payload,label):announce_path='/transaction/announce'print(f'Announcing {label} to {announce_path}')request=urllib.request.Request(f'{NODE_URL}{announce_path}',data=payload.encode(),headers={'Content-Type':'application/json'},method='POST')withurllib.request.urlopen(request)asannounce_response:result=json.loads(announce_response.read().decode())print(f' Result: {result["message"]}')returnresult['message']# Helper function to wait for transaction confirmationdefwait_for_confirmation(tx_hash,label):status_path=f'/transaction/get?hash={tx_hash}'print(f'Waiting for {label} confirmation from {status_path}')is_confirmed=Falsefor_inrange(120):try:withurllib.request.urlopen(f'{NODE_URL}{status_path}')asstatus_response:confirmed=json.loads(status_response.read().decode())height=confirmed['meta']['height']print(f'{label} confirmed in block {height}')is_confirmed=Truebreakexcepturllib.error.HTTPError:print(' Transaction status: pending')time.sleep(1)ifnotis_confirmed:print(f'{label} confirmation took too long.')facade=NemFacade('testnet')MULTISIG_PUBLIC_KEY=os.getenv('MULTISIG_PUBLIC_KEY','D656155B48D4E71E4C59EC6FAEB5EB4F214DE8BC3C65D5BF6A3D9931B4E5ACF2')multisig_public_key=PublicKey(MULTISIG_PUBLIC_KEY)multisig_address=facade.network.public_key_to_address(multisig_public_key)print(f'Multisig public key: {multisig_public_key}')COSIGNATORY0_PRIVATE_KEY=os.getenv('COSIGNATORY0_PRIVATE_KEY','0000000000000000000000000000000000000000000000000000000000000002')cosignatory0_key_pair=NemFacade.KeyPair(PrivateKey(COSIGNATORY0_PRIVATE_KEY))print(f'Cosignatory 0 public key: {cosignatory0_key_pair.public_key}')COSIGNATORY1_PRIVATE_KEY=os.getenv('COSIGNATORY1_PRIVATE_KEY','0000000000000000000000000000000000000000000000000000000000000003')cosignatory1_key_pair=NemFacade.KeyPair(PrivateKey(COSIGNATORY1_PRIVATE_KEY))print(f'Cosignatory 1 public key: {cosignatory1_key_pair.public_key}')try:# Fetch current network timetime_path='/time-sync/network-time'print(f'Fetching current network time from {time_path}')withurllib.request.urlopen(f'{NODE_URL}{time_path}')asresponse:response_json=json.loads(response.read().decode())network_time=response_json['receiveTimeStamp']//1000print(f' Network time: {network_time} s since the nemesis block')# Derived fields from network timetimestamp=NetworkTimestamp(network_time)deadline=timestamp.add_hours(2)# Build the inner transfer transactiontransfer_transaction=facade.transaction_factory.create({'type':'transfer_transaction_v2','signer_public_key':multisig_public_key,'timestamp':timestamp.timestamp,'deadline':deadline.timestamp,'recipient_address':multisig_address,'amount':1_000_000# 1 XEM})transfer_transaction.fee=Amount(calculate_transaction_fee(transfer_transaction))# Build the wrapper multisig transactiontransaction=facade.transaction_factory.create({'type':'multisig_transaction_v1',# This is the cosignatory that initiates the transfer'signer_public_key':cosignatory0_key_pair.public_key,'timestamp':timestamp.timestamp,'deadline':deadline.timestamp,'inner_transaction':facade.transaction_factory.to_non_verifiable_transaction(transfer_transaction)})transaction.fee=Amount(calculate_transaction_fee(transaction))# Sign and announce the multisig transactionsignature=facade.sign_transaction(cosignatory0_key_pair,transaction)json_payload=facade.transaction_factory.attach_signature(transaction,signature)print('Built multisig transaction:')print(json.dumps(transaction.to_json(),indent=2))announce_result=announce_transaction(json_payload,'multisig transaction')# The transaction is now waiting for the second signature# Retrieve the pending transaction from the networkif'SUCCESS'==announce_result:cosignatory1_address=facade.network.public_key_to_address(cosignatory1_key_pair.public_key)unconfirmed_path=('/account/unconfirmedTransactions'f'?address={cosignatory1_address}')print(f'Fetching pending transactions from {unconfirmed_path}')withurllib.request.urlopen(f'{NODE_URL}{unconfirmed_path}')asresponse:pending=json.loads(response.read().decode())['data']# Select the pending transaction issued by the multisig accountinner_transaction_hash=next(entry['meta']['data']forentryinpendingifentry['transaction'].get('otherTrans',{}).get('signer','').upper()==str(multisig_public_key))print(f' Inner transaction hash: {inner_transaction_hash}')# Build the cosignaturecosignature=facade.transaction_factory.create({'type':'cosignature_v1',# This is the cosignatory providing the second signature'signer_public_key':cosignatory1_key_pair.public_key,'timestamp':timestamp.timestamp,'deadline':deadline.timestamp,# Hash of the inner transfer transaction'other_transaction_hash':inner_transaction_hash,# Address of the multisig account'multisig_account_address':multisig_address})cosignature.fee=Amount(calculate_transaction_fee(cosignature))# Sign and announce the cosignaturecosignature_signature=facade.sign_transaction(cosignatory1_key_pair,cosignature)cosignature_payload=facade.transaction_factory.attach_signature(cosignature,cosignature_signature)print('Built cosignature:')print(json.dumps(cosignature.to_json(),indent=2))cosignature_result=announce_transaction(cosignature_payload,'cosignature')# Wait for the multisig transaction to be confirmedif'SUCCESS'==cosignature_result:wait_for_confirmation(facade.hash_transaction(transaction),'multisig transaction')else:print(f'Transaction rejected: {cosignature_result}')else:print(f'Transaction rejected: {announce_result}')excepturllib.error.URLErrorase:print(e.reason)
import{PrivateKey,PublicKey}from'symbol-sdk';import{NemFacade,NetworkTimestamp,calculateTransactionFee,models}from'symbol-sdk/nem';constNODE_URL=process.env.NODE_URL||'http://libertalia.nemtest.net:7890';console.log('Using node',NODE_URL);// Helper function to announce a transactionasyncfunctionannounceTransaction(payload,label){constannouncePath='/transaction/announce';console.log(`Announcing ${label} to ${announcePath}`);constannounceResponse=awaitfetch(`${NODE_URL}${announcePath}`,{method:'POST',headers:{'Content-Type':'application/json'},body:payload});constresult=awaitannounceResponse.json();console.log(' Result:',result.message);returnresult.message;}// Helper function to wait for transaction confirmationasyncfunctionwaitForConfirmation(transactionHash,label){conststatusPath=`/transaction/get?hash=${transactionHash}`;console.log(`Waiting for ${label} confirmation from`,statusPath);letisConfirmed=false;for(letattempt=1;120>=attempt;++attempt){constresponse=awaitfetch(`${NODE_URL}${statusPath}`);if(response.ok){constconfirmed=awaitresponse.json();console.log(`${label} confirmed in block`,confirmed.meta.height);isConfirmed=true;break;}console.log(' Transaction status: pending');awaitnewPromise(resolve=>{setTimeout(resolve,1000);});}if(!isConfirmed)console.warn(`${label} confirmation took too long.`);}constfacade=newNemFacade('testnet');constMULTISIG_PUBLIC_KEY=process.env.MULTISIG_PUBLIC_KEY||('D656155B48D4E71E4C59EC6FAEB5EB4F214DE8BC3C65D5BF6A3D9931B4E5ACF2');constmultisigPublicKey=newPublicKey(MULTISIG_PUBLIC_KEY);constmultisigAddress=facade.network.publicKeyToAddress(multisigPublicKey);console.log(`Multisig public key: ${multisigPublicKey}`);constCOSIGNATORY0_PRIVATE_KEY=process.env.COSIGNATORY0_PRIVATE_KEY||('0000000000000000000000000000000000000000000000000000000000000002');constcosignatory0KeyPair=newNemFacade.KeyPair(newPrivateKey(COSIGNATORY0_PRIVATE_KEY));console.log(`Cosignatory 0 public key: ${cosignatory0KeyPair.publicKey}`);constCOSIGNATORY1_PRIVATE_KEY=process.env.COSIGNATORY1_PRIVATE_KEY||('0000000000000000000000000000000000000000000000000000000000000003');constcosignatory1KeyPair=newNemFacade.KeyPair(newPrivateKey(COSIGNATORY1_PRIVATE_KEY));console.log(`Cosignatory 1 public key: ${cosignatory1KeyPair.publicKey}`);try{// Fetch current network timeconsttimePath='/time-sync/network-time';console.log('Fetching current network time from',timePath);consttimeResponse=awaitfetch(`${NODE_URL}${timePath}`);consttimeJSON=awaittimeResponse.json();constnetworkTime=Math.floor(timeJSON.receiveTimeStamp/1000);console.log(' Network time:',networkTime,'s since the nemesis block');// Derived fields from network timeconsttimestamp=newNetworkTimestamp(networkTime);constdeadline=timestamp.addHours(2);// Build the inner transfer transactionconsttransferTransaction=facade.transactionFactory.create({type:'transfer_transaction_v2',signerPublicKey:multisigPublicKey.toString(),timestamp:timestamp.timestamp,deadline:deadline.timestamp,recipientAddress:multisigAddress.toString(),amount:1_000_000n// 1 XEM});transferTransaction.fee=newmodels.Amount(calculateTransactionFee(transferTransaction));// Build the wrapper multisig transactionconsttransaction=facade.transactionFactory.create({type:'multisig_transaction_v1',// This is the cosignatory that initiates the transfersignerPublicKey:cosignatory0KeyPair.publicKey.toString(),timestamp:timestamp.timestamp,deadline:deadline.timestamp,innerTransaction:facade.transactionFactory.static.toNonVerifiableTransaction(transferTransaction)});transaction.fee=newmodels.Amount(calculateTransactionFee(transaction));// Sign and announce the multisig transactionconstsignature=facade.signTransaction(cosignatory0KeyPair,transaction);constjsonPayload=facade.transactionFactory.static.attachSignature(transaction,signature);console.log('Built multisig transaction:');console.log(JSON.stringify(transaction.toJson(),null,2));constannounceResult=awaitannounceTransaction(jsonPayload,'multisig transaction');// The transaction is now waiting for the second signature// Retrieve the pending transaction from the networkif('SUCCESS'===announceResult){constcosignatory1Address=facade.network.publicKeyToAddress(cosignatory1KeyPair.publicKey);constunconfirmedPath='/account/unconfirmedTransactions'+`?address=${cosignatory1Address}`;console.log('Fetching pending transactions from',unconfirmedPath);constunconfirmedResponse=awaitfetch(`${NODE_URL}${unconfirmedPath}`);constpending=(awaitunconfirmedResponse.json()).data;// Select the pending transaction issued by the multisig accountconstpendingEntry=pending.find(entry=>multisigPublicKey.toString()===(entry.transaction.otherTrans?.signer??'').toUpperCase());constinnerTransactionHash=pendingEntry.meta.data;console.log(' Inner transaction hash:',innerTransactionHash);// Build the cosignatureconstcosignature=facade.transactionFactory.create({type:'cosignature_v1',// This is the cosignatory providing the second signaturesignerPublicKey:cosignatory1KeyPair.publicKey.toString(),timestamp:timestamp.timestamp,deadline:deadline.timestamp,// Hash of the inner transfer transactionotherTransactionHash:innerTransactionHash,// Address of the multisig accountmultisigAccountAddress:multisigAddress.toString()});cosignature.fee=newmodels.Amount(calculateTransactionFee(cosignature));// Sign and announce the cosignatureconstcosignatureSignature=facade.signTransaction(cosignatory1KeyPair,cosignature);constcosignaturePayload=facade.transactionFactory.static.attachSignature(cosignature,cosignatureSignature);console.log('Built cosignature:');console.log(JSON.stringify(cosignature.toJson(),null,2));constcosignatureResult=awaitannounceTransaction(cosignaturePayload,'cosignature');// Wait for the multisig transaction to be confirmedif('SUCCESS'===cosignatureResult){awaitwaitForConfirmation(facade.hashTransaction(transaction).toString(),'multisig transaction');}else{console.log('Transaction rejected:',cosignatureResult);}}else{console.log('Transaction rejected:',announceResult);}}catch(e){console.error(e.message,'| Cause:',e.cause?.code??'unknown');}
Signing a transaction on behalf of a multisig account involves wrapping it in a MultisigTransactionV1 and
collecting the required cosignatures.
In this tutorial, the wrapped transaction is a transfer, with the multisig account as its signer,
since this is the origin of the funds.
Cosignatory 0 signs and announces the wrapper, and the transaction remains pending until Cosignatory 1 provides the
second required cosignature.
In practice, each cosignatory would run its own part on a different machine, holding only its own private key.
This tutorial combines both roles in a single program for simplicity.
The code defines two helper functions for announcing a transaction and waiting for its confirmation.
For details on how these work, see the Transfer XEM tutorial.
MULTISIG_PUBLIC_KEY=os.getenv('MULTISIG_PUBLIC_KEY','D656155B48D4E71E4C59EC6FAEB5EB4F214DE8BC3C65D5BF6A3D9931B4E5ACF2')multisig_public_key=PublicKey(MULTISIG_PUBLIC_KEY)multisig_address=facade.network.public_key_to_address(multisig_public_key)print(f'Multisig public key: {multisig_public_key}')COSIGNATORY0_PRIVATE_KEY=os.getenv('COSIGNATORY0_PRIVATE_KEY','0000000000000000000000000000000000000000000000000000000000000002')cosignatory0_key_pair=NemFacade.KeyPair(PrivateKey(COSIGNATORY0_PRIVATE_KEY))print(f'Cosignatory 0 public key: {cosignatory0_key_pair.public_key}')COSIGNATORY1_PRIVATE_KEY=os.getenv('COSIGNATORY1_PRIVATE_KEY','0000000000000000000000000000000000000000000000000000000000000003')cosignatory1_key_pair=NemFacade.KeyPair(PrivateKey(COSIGNATORY1_PRIVATE_KEY))print(f'Cosignatory 1 public key: {cosignatory1_key_pair.public_key}')
constMULTISIG_PUBLIC_KEY=process.env.MULTISIG_PUBLIC_KEY||('D656155B48D4E71E4C59EC6FAEB5EB4F214DE8BC3C65D5BF6A3D9931B4E5ACF2');constmultisigPublicKey=newPublicKey(MULTISIG_PUBLIC_KEY);constmultisigAddress=facade.network.publicKeyToAddress(multisigPublicKey);console.log(`Multisig public key: ${multisigPublicKey}`);constCOSIGNATORY0_PRIVATE_KEY=process.env.COSIGNATORY0_PRIVATE_KEY||('0000000000000000000000000000000000000000000000000000000000000002');constcosignatory0KeyPair=newNemFacade.KeyPair(newPrivateKey(COSIGNATORY0_PRIVATE_KEY));console.log(`Cosignatory 0 public key: ${cosignatory0KeyPair.publicKey}`);constCOSIGNATORY1_PRIVATE_KEY=process.env.COSIGNATORY1_PRIVATE_KEY||('0000000000000000000000000000000000000000000000000000000000000003');constcosignatory1KeyPair=newNemFacade.KeyPair(newPrivateKey(COSIGNATORY1_PRIVATE_KEY));console.log(`Cosignatory 1 public key: ${cosignatory1KeyPair.publicKey}`);
The tutorial requires three separate accounts, configured through environment variables.
If not set, default values are used:
Environment Variable
Default value
Purpose
MULTISIG_PUBLIC_KEY
D656..ACF2
2-of-2 multisig account
COSIGNATORY0_PRIVATE_KEY
0000..0002
First cosignatory account, the initiator
COSIGNATORY1_PRIVATE_KEY
0000..0003
Second cosignatory account
Each key is a 64-character hexadecimal string.
Unlike a regular account, the multisig account cannot initiate transactions itself.
Instead, its cosignatories sign on its behalf.
Its private key is therefore never needed, and its public key is enough to identify the account.
The multisig account must hold enough funds to pay the transaction fees.
If the default values are used, this account may already be funded.
The snippet above derives and stores the key pair of each cosignatory, and the multisig account's address,
for later use.
# Fetch current network timetime_path='/time-sync/network-time'print(f'Fetching current network time from {time_path}')withurllib.request.urlopen(f'{NODE_URL}{time_path}')asresponse:response_json=json.loads(response.read().decode())network_time=response_json['receiveTimeStamp']//1000print(f' Network time: {network_time} s since the nemesis block')# Derived fields from network timetimestamp=NetworkTimestamp(network_time)deadline=timestamp.add_hours(2)
// Fetch current network timeconsttimePath='/time-sync/network-time';console.log('Fetching current network time from',timePath);consttimeResponse=awaitfetch(`${NODE_URL}${timePath}`);consttimeJSON=awaittimeResponse.json();constnetworkTime=Math.floor(timeJSON.receiveTimeStamp/1000);console.log(' Network time:',networkTime,'s since the nemesis block');// Derived fields from network timeconsttimestamp=newNetworkTimestamp(networkTime);constdeadline=timestamp.addHours(2);
Network time is fetched from /time-sync/network-timeGET, and the transactions' timestamp and deadline fields
are derived from it, following the process described in the Transfer XEM tutorial.
# Build the inner transfer transactiontransfer_transaction=facade.transaction_factory.create({'type':'transfer_transaction_v2','signer_public_key':multisig_public_key,'timestamp':timestamp.timestamp,'deadline':deadline.timestamp,'recipient_address':multisig_address,'amount':1_000_000# 1 XEM})transfer_transaction.fee=Amount(calculate_transaction_fee(transfer_transaction))
// Build the inner transfer transactionconsttransferTransaction=facade.transactionFactory.create({type:'transfer_transaction_v2',signerPublicKey:multisigPublicKey.toString(),timestamp:timestamp.timestamp,deadline:deadline.timestamp,recipientAddress:multisigAddress.toString(),amount:1_000_000n// 1 XEM});transferTransaction.fee=newmodels.Amount(calculateTransactionFee(transferTransaction));
: public key of the account whose funds are being transferred, that is,
the multisignature account.
: in this particular example, the funds are sent back to the sender, so the
recipient is also the multisig account.
: 1'000'000 atomic units, corresponding to 1 XEM,
as explained in the Transfer XEM tutorial.
The inner transaction has its own transaction fee, calculated with .
For the 1 XEM sent here, the fee is 0.05 XEM, as shown in the
transfer fee schedule.
# Build the wrapper multisig transactiontransaction=facade.transaction_factory.create({'type':'multisig_transaction_v1',# This is the cosignatory that initiates the transfer'signer_public_key':cosignatory0_key_pair.public_key,'timestamp':timestamp.timestamp,'deadline':deadline.timestamp,'inner_transaction':facade.transaction_factory.to_non_verifiable_transaction(transfer_transaction)})transaction.fee=Amount(calculate_transaction_fee(transaction))
// Build the wrapper multisig transactionconsttransaction=facade.transactionFactory.create({type:'multisig_transaction_v1',// This is the cosignatory that initiates the transfersignerPublicKey:cosignatory0KeyPair.publicKey.toString(),timestamp:timestamp.timestamp,deadline:deadline.timestamp,innerTransaction:facade.transactionFactory.static.toNonVerifiableTransaction(transferTransaction)});transaction.fee=newmodels.Amount(calculateTransactionFee(transaction));
The transfer transaction is then wrapped in a MultisigTransactionV1. Its most relevant fields are:
: this time, it is the public key of the cosignatory that initiates the
transaction.
: the wrapped transfer transaction, converted with
so it can be embedded without a signature of its own.
The multisig wrapper also has its own transaction fee of 0.15 XEM, as shown in the
fee schedule.
All fees, and the transferred amount, are deducted from the multisig account once the transaction is confirmed.
Initiator: Announcing the Multisig Transaction⚓︎
# Sign and announce the multisig transactionsignature=facade.sign_transaction(cosignatory0_key_pair,transaction)json_payload=facade.transaction_factory.attach_signature(transaction,signature)print('Built multisig transaction:')print(json.dumps(transaction.to_json(),indent=2))announce_result=announce_transaction(json_payload,'multisig transaction')# The transaction is now waiting for the second signature
// Sign and announce the multisig transactionconstsignature=facade.signTransaction(cosignatory0KeyPair,transaction);constjsonPayload=facade.transactionFactory.static.attachSignature(transaction,signature);console.log('Built multisig transaction:');console.log(JSON.stringify(transaction.toJson(),null,2));constannounceResult=awaitannounceTransaction(jsonPayload,'multisig transaction');// The transaction is now waiting for the second signature
In this case, Cosignatory 0 is the initiator of the multisig transaction.
It signs the transaction and announces it to the network.
If valid, the network accepts the transaction, but it is not confirmed yet.
Since the multisig account requires two cosignatures and only one has been provided,
the transaction waits in the unconfirmed pool until the missing cosignature arrives.
Simpler configurations
In a multisig that requires only one cosignature, such as the 1-of-2 configuration created in the
Configuring a Multisignature Account tutorial, the initiating
cosignatory's signature is enough.
If valid, the transaction is confirmed without any further steps.
Cosignatory: Retrieving the Pending Transaction⚓︎
# Retrieve the pending transaction from the networkif'SUCCESS'==announce_result:cosignatory1_address=facade.network.public_key_to_address(cosignatory1_key_pair.public_key)unconfirmed_path=('/account/unconfirmedTransactions'f'?address={cosignatory1_address}')print(f'Fetching pending transactions from {unconfirmed_path}')withurllib.request.urlopen(f'{NODE_URL}{unconfirmed_path}')asresponse:pending=json.loads(response.read().decode())['data']# Select the pending transaction issued by the multisig accountinner_transaction_hash=next(entry['meta']['data']forentryinpendingifentry['transaction'].get('otherTrans',{}).get('signer','').upper()==str(multisig_public_key))print(f' Inner transaction hash: {inner_transaction_hash}')
// Retrieve the pending transaction from the networkif('SUCCESS'===announceResult){constcosignatory1Address=facade.network.publicKeyToAddress(cosignatory1KeyPair.publicKey);constunconfirmedPath='/account/unconfirmedTransactions'+`?address=${cosignatory1Address}`;console.log('Fetching pending transactions from',unconfirmedPath);constunconfirmedResponse=awaitfetch(`${NODE_URL}${unconfirmedPath}`);constpending=(awaitunconfirmedResponse.json()).data;// Select the pending transaction issued by the multisig accountconstpendingEntry=pending.find(entry=>multisigPublicKey.toString()===(entry.transaction.otherTrans?.signer??'').toUpperCase());constinnerTransactionHash=pendingEntry.meta.data;console.log(' Inner transaction hash:',innerTransactionHash);
At this point, Cosignatory 1 takes over.
Cosignatories can use the /account/unconfirmedTransactionsGET endpoint to discover pending multisig transactions
awaiting their signature.
The metadata of each pending multisig transaction contains the hash of its inner transaction, which is the value
that a cosignature must reference.
A cosignatory can have multiple pending multisig transactions awaiting approval.
In this example, the code selects the transaction issued by the multisig account.
This is sufficient for the tutorial because only one pending transaction is expected from that account.
In real applications, however, this filter is not enough if the multisig account has multiple pending transactions.
Instead, inspect the content of each pending transaction, such as its type, recipient, and amount, before selecting the
one to cosign.
Verify before cosigning
Always verify the contents of a transaction before cosigning it.
Cosignatures are binding and cannot be undone.
# Build the cosignaturecosignature=facade.transaction_factory.create({'type':'cosignature_v1',# This is the cosignatory providing the second signature'signer_public_key':cosignatory1_key_pair.public_key,'timestamp':timestamp.timestamp,'deadline':deadline.timestamp,# Hash of the inner transfer transaction'other_transaction_hash':inner_transaction_hash,# Address of the multisig account'multisig_account_address':multisig_address})cosignature.fee=Amount(calculate_transaction_fee(cosignature))
// Build the cosignatureconstcosignature=facade.transactionFactory.create({type:'cosignature_v1',// This is the cosignatory providing the second signaturesignerPublicKey:cosignatory1KeyPair.publicKey.toString(),timestamp:timestamp.timestamp,deadline:deadline.timestamp,// Hash of the inner transfer transactionotherTransactionHash:innerTransactionHash,// Address of the multisig accountmultisigAccountAddress:multisigAddress.toString()});cosignature.fee=newmodels.Amount(calculateTransactionFee(cosignature));
Cosignatory 1 provides the missing signature by announcing a CosignatureV1.
The cosignature specifies:
: public key of the cosignatory providing the signature.
: hash of the inner transfer transaction retrieved in the previous step.
: address of the multisig account the signature refers to.
The cosignature has a 0.15 XEM fee.
The fee is also deducted from the multisig account once the multisig transaction is confirmed.
# Sign and announce the cosignaturecosignature_signature=facade.sign_transaction(cosignatory1_key_pair,cosignature)cosignature_payload=facade.transaction_factory.attach_signature(cosignature,cosignature_signature)print('Built cosignature:')print(json.dumps(cosignature.to_json(),indent=2))cosignature_result=announce_transaction(cosignature_payload,'cosignature')
// Sign and announce the cosignatureconstcosignatureSignature=facade.signTransaction(cosignatory1KeyPair,cosignature);constcosignaturePayload=facade.transactionFactory.static.attachSignature(cosignature,cosignatureSignature);console.log('Built cosignature:');console.log(JSON.stringify(cosignature.toJson(),null,2));constcosignatureResult=awaitannounceTransaction(cosignaturePayload,'cosignature');
Cosignatory 1 then signs the cosignature and announces it to the network.
The announced cosignature does not appear in the unconfirmed pool as a separate transaction.
Instead, the network attaches it to the pending multisig transaction.
In configurations that require additional cosignatures, the transaction remains pending.
The collected signatures can be inspected in the transaction's signatures field by querying
/account/unconfirmedTransactionsGET again.
In this tutorial, however, the second cosignature completes the transaction, which leaves the pool and is confirmed
in the next block.
# Wait for the multisig transaction to be confirmedif'SUCCESS'==cosignature_result:wait_for_confirmation(facade.hash_transaction(transaction),'multisig transaction')else:print(f'Transaction rejected: {cosignature_result}')
// Wait for the multisig transaction to be confirmedif('SUCCESS'===cosignatureResult){awaitwaitForConfirmation(facade.hashTransaction(transaction).toString(),'multisig transaction');}else{console.log('Transaction rejected:',cosignatureResult);}
Once all required cosignatures have been collected, the multisig transaction is confirmed as a single unit.
Multisig transactions are rejected if they violate protocol constraints.
The following table summarizes the most common error sources:
Error message
Probable cause
FAILURE_TRANSACTION_NOT_ALLOWED_FOR_MULTISIG
The multisig account tried to announce the transfer itself.
FAILURE_MULTISIG_NOT_A_COSIGNER
The signer of the multisig transaction is not in the cosignatories list.
FAILURE_MULTISIG_NO_MATCHING_MULTISIG
The cosignature does not match a pending multisig transaction, or its signer is not a cosignatory.
FAILURE_SIGNATURE_NOT_VERIFIABLE
The signature attached to a transaction does not match its .