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 the transfer.
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.
Create a 2-of-2 multisig account.
To create it, run the configure multisig tutorial,
changing from 1 to 2.
If the account is already a multisig with a different configuration,
disable it first.
Additionally, review the Transfer XEM tutorial to understand how transactions are
announced and confirmed.
importjsonimportosimporttimeimporturllib.requestfromsymbolchain.CryptoTypesimportPrivateKey,PublicKeyfromsymbolchain.facade.NemFacadeimportNemFacadefromsymbolchain.ncimportAmountfromsymbolchain.nem.FeeCalculatorimportcalculate_transaction_feeNODE_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:# Build the inner transfer transactiontransfer_transaction=facade.create_transaction_from_descriptor({'type':'transfer_transaction_v2','recipient_address':multisig_address,'amount':1_000_000# 1 XEM},multisig_public_key,0,2*60*60)transfer_transaction.fee=Amount(calculate_transaction_fee(transfer_transaction))# Build the wrapper multisig transactiontransaction=facade.create_transaction_from_descriptor({'type':'multisig_transaction_v1','inner_transaction':facade.transaction_factory.to_non_verifiable_transaction(transfer_transaction)},# This is the cosignatory that initiates the transfercosignatory0_key_pair.public_key,0,2*60*60)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))transaction_hash=facade.hash_transaction(transaction)print(f'Transaction hash: {transaction_hash}')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.create_transaction_from_descriptor({'type':'cosignature_v1',# Hash of the inner transfer transaction'other_transaction_hash':inner_transaction_hash,# Address of the multisig account'multisig_account_address':multisig_address},# This is the cosignatory providing the second signaturecosignatory1_key_pair.public_key,0,2*60*60)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(transaction_hash,'multisig transaction')else:print(f'Transaction rejected: {cosignature_result}')else:print(f'Transaction rejected: {announce_result}')exceptExceptionaserror:print(error)
import{Hash256,PrivateKey,PublicKey}from'symbol-sdk';import{Address,NemFacade,calculateTransactionFee,descriptors,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}`);constresponse=awaitfetch(`${NODE_URL}${announcePath}`,{method:'POST',headers:{'Content-Type':'application/json'},body:payload});if(!response.ok)thrownewError(`HTTP ${response.status}`);constresult=awaitresponse.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){console.log(' Transaction status: pending');awaitnewPromise(resolve=>{setTimeout(resolve,1000);});}else{constconfirmed=awaitresponse.json();console.log(`${label} confirmed in block`,confirmed.meta.height);isConfirmed=true;break;}}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{// Build the inner transfer transactionconsttransferTransaction=facade.createTransactionFromTypedDescriptor(newdescriptors.TransferTransactionV2Descriptor(newAddress(multisigAddress.toString()),newmodels.Amount(1_000_000n)),// 1 XEMmultisigPublicKey,0n,2*60*60);transferTransaction.fee=newmodels.Amount(calculateTransactionFee(transferTransaction));// Build the wrapper multisig transactionconsttransaction=facade.createTransactionFromTypedDescriptor(newdescriptors.MultisigTransactionV1Descriptor(facade.transactionFactory.static.toNonVerifiableTransaction(transferTransaction)),// This is the cosignatory that initiates the transfercosignatory0KeyPair.publicKey,0n,2*60*60);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));consttransactionHash=facade.hashTransaction(transaction).toString();console.log('Transaction hash:',transactionHash);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.createTransactionFromTypedDescriptor(newdescriptors.CosignatureV1Descriptor(// Hash of the inner transfer transactionnewHash256(innerTransactionHash),// Address of the multisig accountnewAddress(multisigAddress.toString())),// This is the cosignatory providing the second signaturecosignatory1KeyPair.publicKey,0n,2*60*60);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(transactionHash,'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');}
//JAVA 21+//DEPS org.symbol:symbol-sdk:3.3.3importjava.io.IOException;importjava.net.URI;importjava.net.http.HttpClient;importjava.net.http.HttpRequest;importjava.net.http.HttpResponse;importjava.net.http.HttpResponse.BodyHandlers;importjava.util.List;importcom.fasterxml.jackson.databind.JsonNode;importcom.fasterxml.jackson.databind.ObjectMapper;importorg.symbol.sdk.CryptoTypes;importorg.symbol.sdk.facade.NemFacade;importorg.symbol.sdk.nem.Address;importorg.symbol.sdk.nem.FeeCalculator;importorg.symbol.sdk.nem.KeyPair;importorg.symbol.sdk.nem.NemTransactionFactory;importorg.symbol.sdk.nem.descriptors.CosignatureV1Descriptor;importorg.symbol.sdk.nem.descriptors.MultisigTransactionV1Descriptor;importorg.symbol.sdk.nem.descriptors.TransferTransactionV2Descriptor;importorg.symbol.sdk.nem.models.Amount;importorg.symbol.sdk.nem.models.Transaction;publicfinalclassSignMultisig{privatestaticfinalObjectMapperJSON_MAPPER=newObjectMapper();privatestaticfinalHttpClientHTTP_CLIENT=HttpClient.newHttpClient();privatefinalStringnodeUrl=System.getenv().getOrDefault("NODE_URL","http://libertalia.nemtest.net:7890");privatefinalNemFacadefacade=newNemFacade("testnet");// Helper method to announce a transactionprivateStringannounceTransaction(finalStringpayload,finalStringlabel)throwsIOException,InterruptedException{finalStringannouncePath="/transaction/announce";System.out.printf("Announcing %s to %s%n",label,announcePath);finalHttpRequestrequest=HttpRequest.newBuilder(URI.create(nodeUrl+announcePath)).header("Content-Type","application/json").POST(HttpRequest.BodyPublishers.ofString(payload)).build();finalHttpResponse<String>response=HTTP_CLIENT.send(request,BodyHandlers.ofString());if(2!=response.statusCode()/100)thrownewIOException("HTTP "+response.statusCode());finalStringresult=JSON_MAPPER.readTree(response.body()).get("message").asText();System.out.printf(" Result: %s%n",result);returnresult;}// Helper method to wait for transaction confirmationprivatevoidwaitForConfirmation(finalStringtransactionHash,finalStringlabel)throwsIOException,InterruptedException{finalStringstatusPath=String.format("/transaction/get?hash=%s",transactionHash);System.out.printf("Waiting for %s confirmation from %s%n",label,statusPath);booleanisConfirmed=false;for(intattempt=1;120>=attempt;++attempt){finalHttpRequestrequest=HttpRequest.newBuilder(URI.create(nodeUrl+statusPath)).GET().build();finalHttpResponse<String>response=HTTP_CLIENT.send(request,BodyHandlers.ofString());if(2!=response.statusCode()/100){System.out.println(" Transaction status: pending");Thread.sleep(1000);}else{finalJsonNodeconfirmed=JSON_MAPPER.readTree(response.body());System.out.printf("%s confirmed in block %s%n",label,confirmed.get("meta").get("height").asText());isConfirmed=true;break;}}if(!isConfirmed)System.out.printf("%s confirmation took too long.%n",label);}publicstaticvoidmain(finalString[]args){try{newSignMultisig().run();}catch(finalExceptionex){System.out.println(null==ex.getMessage()?ex.toString():ex.getMessage());}}privatevoidrun()throwsIOException,InterruptedException{System.out.printf("Using node %s%n",nodeUrl);finalStringmultisigPublicKeyString=System.getenv().getOrDefault("MULTISIG_PUBLIC_KEY","D656155B48D4E71E4C59EC6FAEB5EB4F214DE8BC3C65D5BF6A3D9931B4E5"+"ACF2");finalCryptoTypes.PublicKeymultisigPublicKey=newCryptoTypes.PublicKey(multisigPublicKeyString);finalAddressmultisigAddress=facade.network.publicKeyToAddress(multisigPublicKey);System.out.printf("Multisig public key: %s%n",multisigPublicKey);finalKeyPaircosignatory0KeyPair=newKeyPair(newCryptoTypes.PrivateKey(System.getenv().getOrDefault("COSIGNATORY0_PRIVATE_KEY","0".repeat(63)+"2")));System.out.printf("Cosignatory 0 public key: %s%n",cosignatory0KeyPair.getPublicKey());finalKeyPaircosignatory1KeyPair=newKeyPair(newCryptoTypes.PrivateKey(System.getenv().getOrDefault("COSIGNATORY1_PRIVATE_KEY","0".repeat(63)+"3")));System.out.printf("Cosignatory 1 public key: %s%n",cosignatory1KeyPair.getPublicKey());// Build the inner transfer transactionfinalTransactiontransferTransaction=facade.createTransactionFromTypedDescriptor(newTransferTransactionV2Descriptor(newAddress(multisigAddress.toString()),newAmount(1_000_000),null,List.of()),// 1 XEMmultisigPublicKey,0,2*60*60);transferTransaction.setFee(newAmount(FeeCalculator.calculateTransactionFee(transferTransaction)));// Build the wrapper multisig transactionfinalTransactiontransaction=facade.createTransactionFromTypedDescriptor(newMultisigTransactionV1Descriptor(NemTransactionFactory.toNonVerifiableTransaction(transferTransaction),List.of()),// This is the cosignatory that initiates the transfercosignatory0KeyPair.getPublicKey(),0,2*60*60);transaction.setFee(newAmount(FeeCalculator.calculateTransactionFee(transaction)));// Sign and announce the multisig transactionfinalCryptoTypes.Signaturesignature=facade.signTransaction(cosignatory0KeyPair,transaction);finalStringpayload=NemTransactionFactory.attachSignature(transaction,signature);System.out.println("Built multisig transaction:");System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(transaction.toJson()));finalStringtransactionHash=facade.hashTransaction(transaction).toString();System.out.printf("Transaction hash: %s%n",transactionHash);finalStringannounceResult=announceTransaction(payload,"multisig transaction");// The transaction is now waiting for the second signature// Retrieve the pending transaction from the networkif("SUCCESS".equals(announceResult)){finalAddresscosignatory1Address=facade.network.publicKeyToAddress(cosignatory1KeyPair.getPublicKey());finalStringpath=String.format("/account/unconfirmedTransactions?address=%s",cosignatory1Address);System.out.printf("Fetching pending transactions from %s%n",path);finalHttpRequestrequest=HttpRequest.newBuilder(URI.create(nodeUrl+path)).GET().build();finalJsonNodepending=JSON_MAPPER.readTree(HTTP_CLIENT.send(request,BodyHandlers.ofString()).body()).get("data");// Select the pending transaction issued by the multisigJsonNodependingEntry=null;for(finalJsonNodeentry:pending){finalJsonNodeotherTransaction=entry.get("transaction").get("otherTrans");if(null!=otherTransaction&&multisigPublicKeyString.equalsIgnoreCase(otherTransaction.get("signer").asText())){pendingEntry=entry;break;}}if(null==pendingEntry)thrownewIOException("Pending transaction not found");finalStringinnerTransactionHash=pendingEntry.get("meta").get("data").asText();System.out.printf(" Inner transaction hash: %s%n",innerTransactionHash);// Build the cosignaturefinalTransactioncosignature=facade.createTransactionFromTypedDescriptor(newCosignatureV1Descriptor(// Hash of the inner transfer transactionnewCryptoTypes.Hash256(innerTransactionHash),// Address of the multisig accountnewAddress(multisigAddress.toString())),// Cosignatory providing the second signaturecosignatory1KeyPair.getPublicKey(),0,2*60*60);cosignature.setFee(newAmount(FeeCalculator.calculateTransactionFee(cosignature)));// Sign and announce the cosignaturefinalCryptoTypes.SignaturecosignatureSignature=facade.signTransaction(cosignatory1KeyPair,cosignature);finalStringcosignaturePayload=NemTransactionFactory.attachSignature(cosignature,cosignatureSignature);System.out.println("Built cosignature:");System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(cosignature.toJson()));finalStringcosignatureResult=announceTransaction(cosignaturePayload,"cosignature");// Wait for the multisig transaction to be confirmedif("SUCCESS".equals(cosignatureResult))waitForConfirmation(transactionHash,"multisig transaction");elseSystem.out.printf("Transaction rejected: %s%n",cosignatureResult);}else{System.out.printf("Transaction rejected: %s%n",announceResult);}}}
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}`);
finalStringmultisigPublicKeyString=System.getenv().getOrDefault("MULTISIG_PUBLIC_KEY","D656155B48D4E71E4C59EC6FAEB5EB4F214DE8BC3C65D5BF6A3D9931B4E5"+"ACF2");finalCryptoTypes.PublicKeymultisigPublicKey=newCryptoTypes.PublicKey(multisigPublicKeyString);finalAddressmultisigAddress=facade.network.publicKeyToAddress(multisigPublicKey);System.out.printf("Multisig public key: %s%n",multisigPublicKey);finalKeyPaircosignatory0KeyPair=newKeyPair(newCryptoTypes.PrivateKey(System.getenv().getOrDefault("COSIGNATORY0_PRIVATE_KEY","0".repeat(63)+"2")));System.out.printf("Cosignatory 0 public key: %s%n",cosignatory0KeyPair.getPublicKey());finalKeyPaircosignatory1KeyPair=newKeyPair(newCryptoTypes.PrivateKey(System.getenv().getOrDefault("COSIGNATORY1_PRIVATE_KEY","0".repeat(63)+"3")));System.out.printf("Cosignatory 1 public key: %s%n",cosignatory1KeyPair.getPublicKey());
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.
# Build the inner transfer transactiontransfer_transaction=facade.create_transaction_from_descriptor({'type':'transfer_transaction_v2','recipient_address':multisig_address,'amount':1_000_000# 1 XEM},multisig_public_key,0,2*60*60)transfer_transaction.fee=Amount(calculate_transaction_fee(transfer_transaction))
// Build the inner transfer transactionconsttransferTransaction=facade.createTransactionFromTypedDescriptor(newdescriptors.TransferTransactionV2Descriptor(newAddress(multisigAddress.toString()),newmodels.Amount(1_000_000n)),// 1 XEMmultisigPublicKey,0n,2*60*60);transferTransaction.fee=newmodels.Amount(calculateTransactionFee(transferTransaction));
// Build the inner transfer transactionfinalTransactiontransferTransaction=facade.createTransactionFromTypedDescriptor(newTransferTransactionV2Descriptor(newAddress(multisigAddress.toString()),newAmount(1_000_000),null,List.of()),// 1 XEMmultisigPublicKey,0,2*60*60);transferTransaction.setFee(newAmount(FeeCalculator.calculateTransactionFee(transferTransaction)));
: 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.
The multisig account's public key is passed separately to the facade as the signer.
# Build the wrapper multisig transactiontransaction=facade.create_transaction_from_descriptor({'type':'multisig_transaction_v1','inner_transaction':facade.transaction_factory.to_non_verifiable_transaction(transfer_transaction)},# This is the cosignatory that initiates the transfercosignatory0_key_pair.public_key,0,2*60*60)transaction.fee=Amount(calculate_transaction_fee(transaction))
// Build the wrapper multisig transactionconsttransaction=facade.createTransactionFromTypedDescriptor(newdescriptors.MultisigTransactionV1Descriptor(facade.transactionFactory.static.toNonVerifiableTransaction(transferTransaction)),// This is the cosignatory that initiates the transfercosignatory0KeyPair.publicKey,0n,2*60*60);transaction.fee=newmodels.Amount(calculateTransactionFee(transaction));
// Build the wrapper multisig transactionfinalTransactiontransaction=facade.createTransactionFromTypedDescriptor(newMultisigTransactionV1Descriptor(NemTransactionFactory.toNonVerifiableTransaction(transferTransaction),List.of()),// This is the cosignatory that initiates the transfercosignatory0KeyPair.getPublicKey(),0,2*60*60);transaction.setFee(newAmount(FeeCalculator.calculateTransactionFee(transaction)));
The transfer transaction is then wrapped in a MultisigTransactionV1. Its most relevant fields are:
: 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.
The initiating cosignatory's public key is passed separately to the facade as the wrapper's signer.
For both transactions, the facade derives the timestamp and deadline from the two-hour deadline duration.
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))transaction_hash=facade.hash_transaction(transaction)print(f'Transaction hash: {transaction_hash}')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));consttransactionHash=facade.hashTransaction(transaction).toString();console.log('Transaction hash:',transactionHash);constannounceResult=awaitannounceTransaction(jsonPayload,'multisig transaction');// The transaction is now waiting for the second signature
// Sign and announce the multisig transactionfinalCryptoTypes.Signaturesignature=facade.signTransaction(cosignatory0KeyPair,transaction);finalStringpayload=NemTransactionFactory.attachSignature(transaction,signature);System.out.println("Built multisig transaction:");System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(transaction.toJson()));finalStringtransactionHash=facade.hashTransaction(transaction).toString();System.out.printf("Transaction hash: %s%n",transactionHash);finalStringannounceResult=announceTransaction(payload,"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);
// Retrieve the pending transaction from the networkif("SUCCESS".equals(announceResult)){finalAddresscosignatory1Address=facade.network.publicKeyToAddress(cosignatory1KeyPair.getPublicKey());finalStringpath=String.format("/account/unconfirmedTransactions?address=%s",cosignatory1Address);System.out.printf("Fetching pending transactions from %s%n",path);finalHttpRequestrequest=HttpRequest.newBuilder(URI.create(nodeUrl+path)).GET().build();finalJsonNodepending=JSON_MAPPER.readTree(HTTP_CLIENT.send(request,BodyHandlers.ofString()).body()).get("data");// Select the pending transaction issued by the multisigJsonNodependingEntry=null;for(finalJsonNodeentry:pending){finalJsonNodeotherTransaction=entry.get("transaction").get("otherTrans");if(null!=otherTransaction&&multisigPublicKeyString.equalsIgnoreCase(otherTransaction.get("signer").asText())){pendingEntry=entry;break;}}if(null==pendingEntry)thrownewIOException("Pending transaction not found");finalStringinnerTransactionHash=pendingEntry.get("meta").get("data").asText();System.out.printf(" Inner transaction hash: %s%n",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 approach is sufficient for the tutorial because only one pending transaction is expected from that account, and
Cosignatory 1 has no prior knowledge of the transaction to match more precisely.
Alternatively, the initiator can share the inner transaction hash with the other cosignatories through an off-chain
channel.
The cosignatories can then match this hash to select the exact transaction.
In real applications, however, filtering by the issuing account is not enough.
Nothing guarantees that a pending transaction is the expected one, so 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.create_transaction_from_descriptor({'type':'cosignature_v1',# Hash of the inner transfer transaction'other_transaction_hash':inner_transaction_hash,# Address of the multisig account'multisig_account_address':multisig_address},# This is the cosignatory providing the second signaturecosignatory1_key_pair.public_key,0,2*60*60)cosignature.fee=Amount(calculate_transaction_fee(cosignature))
// Build the cosignatureconstcosignature=facade.createTransactionFromTypedDescriptor(newdescriptors.CosignatureV1Descriptor(// Hash of the inner transfer transactionnewHash256(innerTransactionHash),// Address of the multisig accountnewAddress(multisigAddress.toString())),// This is the cosignatory providing the second signaturecosignatory1KeyPair.publicKey,0n,2*60*60);cosignature.fee=newmodels.Amount(calculateTransactionFee(cosignature));
// Build the cosignaturefinalTransactioncosignature=facade.createTransactionFromTypedDescriptor(newCosignatureV1Descriptor(// Hash of the inner transfer transactionnewCryptoTypes.Hash256(innerTransactionHash),// Address of the multisig accountnewAddress(multisigAddress.toString())),// Cosignatory providing the second signaturecosignatory1KeyPair.getPublicKey(),0,2*60*60);cosignature.setFee(newAmount(FeeCalculator.calculateTransactionFee(cosignature)));
Cosignatory 1 provides the missing signature by announcing a CosignatureV1.
The cosignature specifies:
: 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.
The cosignatory's public key is passed separately to the facade as the signer.
# 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');
// Sign and announce the cosignaturefinalCryptoTypes.SignaturecosignatureSignature=facade.signTransaction(cosignatory1KeyPair,cosignature);finalStringcosignaturePayload=NemTransactionFactory.attachSignature(cosignature,cosignatureSignature);System.out.println("Built cosignature:");System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(cosignature.toJson()));finalStringcosignatureResult=announceTransaction(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(transaction_hash,'multisig transaction')else:print(f'Transaction rejected: {cosignature_result}')
// Wait for the multisig transaction to be confirmedif('SUCCESS'===cosignatureResult){awaitwaitForConfirmation(transactionHash,'multisig transaction');}else{console.log('Transaction rejected:',cosignatureResult);}
// Wait for the multisig transaction to be confirmedif("SUCCESS".equals(cosignatureResult))waitForConfirmation(transactionHash,"multisig transaction");elseSystem.out.printf("Transaction rejected: %s%n",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.