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 one of its cosignatory accounts to create transactions and sign them on its behalf.
This tutorial uses the multisig configuration created in the
Configuring a Multisignature Account tutorial,
with Cosignatory 0 initiating and signing the transaction:
importjsonimportosimporttimeimporturllib.requestfromsymbolchain.CryptoTypesimportPrivateKeyfromsymbolchain.facade.SymbolFacadeimportSymbolFacadefromsymbolchain.symbol.IdGeneratorimportgenerate_mosaic_alias_idNODE_URL=os.getenv('NODE_URL','https://reference.symboltest.net:3001')print(f'Using node {NODE_URL}')# Helper function to announce a transactiondefannounce_transaction(payload,label):print(f'Announcing {label} to /transactions')request=urllib.request.Request(f'{NODE_URL}/transactions',data=payload.encode(),headers={'Content-Type':'application/json'},method='PUT')withurllib.request.urlopen(request)asannounce_response:print(f' Response: {announce_response.read().decode()}')# Helper function to wait for transaction confirmationdefwait_for_confirmation(tx_hash,label):print(f'Waiting for {label} confirmation...')forattemptinrange(60):time.sleep(1)try:url=f'{NODE_URL}/transactionStatus/{tx_hash}'withurllib.request.urlopen(url)asconfirm_response:status=json.loads(confirm_response.read().decode())print(f' Transaction status: {status["group"]}')ifstatus['group']=='confirmed':print(f'{label} confirmed in {attempt} seconds')returnifstatus['group']=='failed':raiseRuntimeError(f'{label} failed: {status["code"]}')excepturllib.error.HTTPError:print(' Transaction status: unknown')raiseTimeoutError(f'{label} not confirmed after 60 seconds')MULTISIG_PRIVATE_KEY=os.getenv('MULTISIG_PRIVATE_KEY','0000000000000000000000000000000000000000000000000000000000000001')multisig_key_pair=SymbolFacade.KeyPair(PrivateKey(MULTISIG_PRIVATE_KEY))print(f'Multisig public key: {multisig_key_pair.public_key}')COSIGNATORY0_PRIVATE_KEY=os.getenv('COSIGNATORY0_PRIVATE_KEY','0000000000000000000000000000000000000000000000000000000000000002')cosignatory_key_pair=SymbolFacade.KeyPair(PrivateKey(COSIGNATORY0_PRIVATE_KEY))print(f'Cosignatory public key: {cosignatory_key_pair.public_key}')facade=SymbolFacade('testnet')try:# Fetch recommended feesfee_path='/network/fees/transaction'print(f'Fetching recommended fees from {fee_path}')withurllib.request.urlopen(f'{NODE_URL}{fee_path}')asresponse:response_json=json.loads(response.read().decode())median_multiplier=response_json['medianFeeMultiplier']minimum_multiplier=response_json['minFeeMultiplier']fee_multiplier=max(median_multiplier,minimum_multiplier)print(f' Fee multiplier: {fee_multiplier}')# Build the embedded transfer transactiontransfer_transaction=(facade.create_embedded_transaction_from_descriptor({'type':'transfer_transaction_v1','recipient_address':facade.network.public_key_to_address(multisig_key_pair.public_key),'mosaics':[{'mosaic_id':generate_mosaic_alias_id('symbol.xym'),'amount':1_000_000# 1 XYM}]},multisig_key_pair.public_key))# Build the wrapper aggregate transactiontransaction=facade.create_transaction_from_descriptor({'type':'aggregate_complete_transaction_v3','transactions_hash':facade.hash_embedded_transactions([transfer_transaction]),'transactions':[transfer_transaction]},cosignatory_key_pair.public_key,fee_multiplier,2*60*60)# Sign the aggregate transaction using the cosignatory's signaturejson_payload=facade.transaction_factory.attach_signature(transaction,facade.sign_transaction(cosignatory_key_pair,transaction))print('Built transaction:')print(json.dumps(transaction.to_json(),indent=2))# Announce the transactiontransaction_hash=facade.hash_transaction(transaction)print(f'Transaction hash: {transaction_hash}')announce_transaction(json_payload,'transaction')# Wait for confirmationwait_for_confirmation(transaction_hash,'transaction')excepturllib.error.URLErrorase:print(e.reason)
import{PrivateKey}from'symbol-sdk';import{SymbolFacade,descriptors,generateMosaicAliasId,models}from'symbol-sdk/symbol';constNODE_URL='https://reference.symboltest.net:3001';console.log('Using node',NODE_URL);// Helper function to announce a transactionasyncfunctionannounceTransaction(payload,label){console.log(`Announcing ${label} to /transactions`);constresponse=awaitfetch(`${NODE_URL}/transactions`,{method:'PUT',headers:{'Content-Type':'application/json'},body:payload});console.log(' Response:',awaitresponse.text());}// Helper function to wait for transaction confirmationasyncfunctionwaitForConfirmation(transactionHash,label){console.log(`Waiting for ${label} confirmation...`);for(letattempt=0;60>attempt;attempt++){awaitnewPromise(resolve=>{setTimeout(resolve,1000);});constresponse=awaitfetch(`${NODE_URL}/transactionStatus/${transactionHash}`);if(!response.ok){if(404===response.status){console.log(' Transaction status: unknown');continue;}thrownewError(`HTTP ${response.status}`);}conststatus=awaitresponse.json();console.log(' Transaction status:',status.group);if('confirmed'===status.group){console.log(`${label} confirmed in`,attempt,'seconds');return;}if('failed'===status.group)thrownewError(`${label} failed: ${status.code}`);}thrownewError(`${label} not confirmed after 60 seconds`);}constMULTISIG_PRIVATE_KEY=process.env.MULTISIG_PRIVATE_KEY||('0000000000000000000000000000000000000000000000000000000000000001');constmultisigKeyPair=newSymbolFacade.KeyPair(newPrivateKey(MULTISIG_PRIVATE_KEY));console.log(`Multisig public key: ${multisigKeyPair.publicKey}`);constCOSIGNATORY0_PRIVATE_KEY=process.env.COSIGNATORY0_PRIVATE_KEY||'0000000000000000000000000000000000000000000000000000000000000002';constcosignatoryKeyPair=newSymbolFacade.KeyPair(newPrivateKey(COSIGNATORY0_PRIVATE_KEY));console.log(`Cosignatory public key: ${cosignatoryKeyPair.publicKey}`);constfacade=newSymbolFacade('testnet');try{// Fetch recommended feesconstfeePath='/network/fees/transaction';console.log('Fetching recommended fees from',feePath);constfeeResponse=awaitfetch(`${NODE_URL}${feePath}`);constfeeJSON=awaitfeeResponse.json();constmedianMultiplier=feeJSON.medianFeeMultiplier;constminimumMultiplier=feeJSON.minFeeMultiplier;constfeeMultiplier=Math.max(medianMultiplier,minimumMultiplier);console.log(' Fee multiplier:',feeMultiplier);// Build the embedded transfer transactionconsttransferTransaction=facade.createEmbeddedTransactionFromTypedDescriptor(newdescriptors.TransferTransactionV1Descriptor(facade.network.publicKeyToAddress(multisigKeyPair.publicKey),[newdescriptors.UnresolvedMosaicDescriptor(generateMosaicAliasId('symbol.xym'),newmodels.Amount(1_000_000n))// 1 XYM],undefined),multisigKeyPair.publicKey);// Build the wrapper aggregate transactionconsttransaction=facade.createTransactionFromTypedDescriptor(newdescriptors.AggregateCompleteTransactionV3Descriptor(facade.static.hashEmbeddedTransactions([transferTransaction]),[transferTransaction],undefined),cosignatoryKeyPair.publicKey,feeMultiplier,2*60*60);// Sign the aggregate transaction using the cosignatory's signatureconstjsonPayload=facade.transactionFactory.static.attachSignature(transaction,facade.signTransaction(cosignatoryKeyPair,transaction));console.log('Built transaction:');console.dir(transaction.toJson(),{colors:true});// Announce the transactionconsttransactionHash=facade.hashTransaction(transaction).toString();console.log('Transaction hash:',transactionHash);awaitannounceTransaction(jsonPayload,'transaction');// Wait for confirmationawaitwaitForConfirmation(transactionHash,'transaction');}catch(e){console.error(e.message,'| Cause:',e.cause?.code??'unknown');}
//JAVA 21+//DEPS org.symbol:symbol-sdk:3.3.1importjava.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.SymbolFacade;importorg.symbol.sdk.symbol.IdGenerator;importorg.symbol.sdk.symbol.KeyPair;importorg.symbol.sdk.symbol.SymbolTransactionFactory;importorg.symbol.sdk.symbol.descriptors.*;importorg.symbol.sdk.symbol.models.*;publicfinalclassSignMultisig{privatestaticfinalObjectMapperJSON_MAPPER=newObjectMapper();privatestaticfinalHttpClientHTTP_CLIENT=HttpClient.newHttpClient();privatefinalStringnodeUrl="https://reference.symboltest.net:3001";privatefinalSymbolFacadefacade=newSymbolFacade("testnet");privatevoidannounceTransaction(finalStringpayload,finalStringlabel)throwsIOException,InterruptedException{System.out.printf("Announcing %s to /transactions%n",label);finalHttpRequestrequest=HttpRequest.newBuilder(URI.create(nodeUrl+"/transactions")).header("Content-Type","application/json").PUT(HttpRequest.BodyPublishers.ofString(payload)).build();finalHttpResponse<String>response=HTTP_CLIENT.send(request,BodyHandlers.ofString());System.out.printf(" Response: %s%n",response.body());}privatevoidwaitForConfirmation(finalStringtransactionHash,finalStringlabel)throwsIOException,InterruptedException{System.out.printf("Waiting for %s confirmation...%n",label);for(intattempt=0;60>attempt;++attempt){Thread.sleep(1000);finalStringstatusPath="/transactionStatus/"+transactionHash;finalHttpRequeststatusRequest=HttpRequest.newBuilder(URI.create(nodeUrl+statusPath)).GET().build();finalHttpResponse<String>statusResponse=HTTP_CLIENT.send(statusRequest,BodyHandlers.ofString());if(404==statusResponse.statusCode()){System.out.println(" Transaction status: unknown");continue;}if(2!=statusResponse.statusCode()/100)thrownewIOException("HTTP "+statusResponse.statusCode());finalJsonNodestatus=JSON_MAPPER.readTree(statusResponse.body());finalStringgroup=status.get("group").asText();System.out.printf(" Transaction status: %s%n",group);if("confirmed".equals(group)){System.out.printf("%s confirmed in %d seconds%n",label,attempt);return;}if("failed".equals(group))thrownewIOException(String.format("%s failed: %s",label,status.get("code").asText()));}thrownewIOException(String.format("%s not confirmed after 60 seconds",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);finalStringmultisigPrivateKey=System.getenv().getOrDefault("MULTISIG_PRIVATE_KEY","%064X".formatted(1));finalKeyPairmultisigKeyPair=newKeyPair(newCryptoTypes.PrivateKey(multisigPrivateKey));System.out.printf("Multisig public key: %s%n",multisigKeyPair.getPublicKey());finalStringcosignatory0PrivateKey=System.getenv().getOrDefault("COSIGNATORY0_PRIVATE_KEY","%064X".formatted(2));finalKeyPaircosignatoryKeyPair=newKeyPair(newCryptoTypes.PrivateKey(cosignatory0PrivateKey));System.out.printf("Cosignatory public key: %s%n",cosignatoryKeyPair.getPublicKey());// Fetch recommended feesfinalStringfeePath="/network/fees/transaction";System.out.printf("Fetching recommended fees from %s%n",feePath);finalHttpRequestfeeRequest=HttpRequest.newBuilder(URI.create(nodeUrl+feePath)).GET().build();finalHttpResponse<String>feeResponse=HTTP_CLIENT.send(feeRequest,BodyHandlers.ofString());finalJsonNodefeeJSON=JSON_MAPPER.readTree(feeResponse.body());finallongmedianMultiplier=feeJSON.get("medianFeeMultiplier").asLong();finallongminimumMultiplier=feeJSON.get("minFeeMultiplier").asLong();finallongfeeMultiplier=Math.max(medianMultiplier,minimumMultiplier);System.out.printf(" Fee multiplier: %d%n",feeMultiplier);// Build the embedded transfer transactionfinalEmbeddedTransactiontransferTransaction=facade.createEmbeddedTransactionFromTypedDescriptor(newTransferTransactionV1Descriptor(facade.network.publicKeyToAddress(multisigKeyPair.getPublicKey()),List.of(newUnresolvedMosaicDescriptor(newUnresolvedMosaicId(IdGenerator.generateMosaicAliasId("symbol.xym")),newAmount(1_000_000))),// 1 XYMnull),multisigKeyPair.getPublicKey());// Build the wrapper aggregate transactionfinalList<EmbeddedTransaction>embeddedTransactions=List.of(transferTransaction);finalTransactiontransaction=facade.createTransactionFromTypedDescriptor(newAggregateCompleteTransactionV3Descriptor(SymbolFacade.hashEmbeddedTransactions(embeddedTransactions),embeddedTransactions,null),cosignatoryKeyPair.getPublicKey(),feeMultiplier,2*60*60);// Sign the aggregate using the cosignatory's signaturefinalStringjsonPayload=SymbolTransactionFactory.attachSignature(transaction,facade.signTransaction(cosignatoryKeyPair,transaction));System.out.println("Built transaction:");System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(transaction.toJson()));// Announce the transactionfinalStringtransactionHash=facade.hashTransaction(transaction).toString();System.out.printf("Transaction hash: %s%n",transactionHash);announceTransaction(jsonPayload,"transaction");// Wait for confirmationwaitForConfirmation(transactionHash,"transaction");}}
In general, signing a transaction on behalf of a multisig account only requires wrapping it in an
aggregate transaction that provides the required cosignatures.
This tutorial builds an embedded transaction containing the transfer, using the multisig account as the signer,
since this is the origin of the transfer.
A complete aggregate transaction then wraps the transfer transaction, signed by the cosignatory,
since this is the account that can authorize the transaction.
MULTISIG_PRIVATE_KEY=os.getenv('MULTISIG_PRIVATE_KEY','0000000000000000000000000000000000000000000000000000000000000001')multisig_key_pair=SymbolFacade.KeyPair(PrivateKey(MULTISIG_PRIVATE_KEY))print(f'Multisig public key: {multisig_key_pair.public_key}')COSIGNATORY0_PRIVATE_KEY=os.getenv('COSIGNATORY0_PRIVATE_KEY','0000000000000000000000000000000000000000000000000000000000000002')cosignatory_key_pair=SymbolFacade.KeyPair(PrivateKey(COSIGNATORY0_PRIVATE_KEY))print(f'Cosignatory public key: {cosignatory_key_pair.public_key}')
constMULTISIG_PRIVATE_KEY=process.env.MULTISIG_PRIVATE_KEY||('0000000000000000000000000000000000000000000000000000000000000001');constmultisigKeyPair=newSymbolFacade.KeyPair(newPrivateKey(MULTISIG_PRIVATE_KEY));console.log(`Multisig public key: ${multisigKeyPair.publicKey}`);constCOSIGNATORY0_PRIVATE_KEY=process.env.COSIGNATORY0_PRIVATE_KEY||'0000000000000000000000000000000000000000000000000000000000000002';constcosignatoryKeyPair=newSymbolFacade.KeyPair(newPrivateKey(COSIGNATORY0_PRIVATE_KEY));console.log(`Cosignatory public key: ${cosignatoryKeyPair.publicKey}`);
finalStringmultisigPrivateKey=System.getenv().getOrDefault("MULTISIG_PRIVATE_KEY","%064X".formatted(1));finalKeyPairmultisigKeyPair=newKeyPair(newCryptoTypes.PrivateKey(multisigPrivateKey));System.out.printf("Multisig public key: %s%n",multisigKeyPair.getPublicKey());finalStringcosignatory0PrivateKey=System.getenv().getOrDefault("COSIGNATORY0_PRIVATE_KEY","%064X".formatted(2));finalKeyPaircosignatoryKeyPair=newKeyPair(newCryptoTypes.PrivateKey(cosignatory0PrivateKey));System.out.printf("Cosignatory public key: %s%n",cosignatoryKeyPair.getPublicKey());
The tutorial requires two separate accounts.
Their private keys can be provided through environment variables.
If not set, default values are used:
Environment Variable
Default value
Purpose
MULTISIG_PRIVATE_KEY
0000..0001
Multisig account
COSIGNATORY0_PRIVATE_KEY
0000..0002
Cosignatory account
Each private key is a 64-character hexadecimal string.
The cosignatory account must hold enough funds to pay the transaction fee.
If the default values are used, these accounts may already be funded.
The snippet above derives and stores the key pair of each account for later use.
# Build the embedded transfer transactiontransfer_transaction=(facade.create_embedded_transaction_from_descriptor({'type':'transfer_transaction_v1','recipient_address':facade.network.public_key_to_address(multisig_key_pair.public_key),'mosaics':[{'mosaic_id':generate_mosaic_alias_id('symbol.xym'),'amount':1_000_000# 1 XYM}]},multisig_key_pair.public_key))
// Build the embedded transfer transactionconsttransferTransaction=facade.createEmbeddedTransactionFromTypedDescriptor(newdescriptors.TransferTransactionV1Descriptor(facade.network.publicKeyToAddress(multisigKeyPair.publicKey),[newdescriptors.UnresolvedMosaicDescriptor(generateMosaicAliasId('symbol.xym'),newmodels.Amount(1_000_000n))// 1 XYM],undefined),multisigKeyPair.publicKey);
// Build the embedded transfer transactionfinalEmbeddedTransactiontransferTransaction=facade.createEmbeddedTransactionFromTypedDescriptor(newTransferTransactionV1Descriptor(facade.network.publicKeyToAddress(multisigKeyPair.getPublicKey()),List.of(newUnresolvedMosaicDescriptor(newUnresolvedMosaicId(IdGenerator.generateMosaicAliasId("symbol.xym")),newAmount(1_000_000))),// 1 XYMnull),multisigKeyPair.getPublicKey());
The embedded transfer transaction is created from the transaction's descriptor and the signer public key.
The signer public key belongs to the account whose funds are being transferred, that is, the multisignature account.
The descriptor includes:
: in this particular example, the funds are sent back to the sender,
so the recipient is also the multisig account.
mosaics: 1'000'000 atomic units of the symbol.xym mosaic, corresponding to 1 XYM,
as explained in the Transfer Transaction tutorial.
The embedded transaction is then wrapped in an aggregate transaction, even though it is the only inner transaction:
// Build the wrapper aggregate transactionfinalList<EmbeddedTransaction>embeddedTransactions=List.of(transferTransaction);finalTransactiontransaction=facade.createTransactionFromTypedDescriptor(newAggregateCompleteTransactionV3Descriptor(SymbolFacade.hashEmbeddedTransactions(embeddedTransactions),embeddedTransactions,null),cosignatoryKeyPair.getPublicKey(),feeMultiplier,2*60*60);
Its descriptor contains the embedded transactions.
The aggregate is then created with the cosignatory's public key as signer, because the cosignatory authorizes the
transaction and pays its fees.
transactions: the list of embedded transactions.
This example has only one, but there could be any number of them.
# Sign the aggregate transaction using the cosignatory's signaturejson_payload=facade.transaction_factory.attach_signature(transaction,facade.sign_transaction(cosignatory_key_pair,transaction))print('Built transaction:')print(json.dumps(transaction.to_json(),indent=2))
// Sign the aggregate transaction using the cosignatory's signatureconstjsonPayload=facade.transactionFactory.static.attachSignature(transaction,facade.signTransaction(cosignatoryKeyPair,transaction));console.log('Built transaction:');console.dir(transaction.toJson(),{colors:true});
// Sign the aggregate using the cosignatory's signaturefinalStringjsonPayload=SymbolTransactionFactory.attachSignature(transaction,facade.signTransaction(cosignatoryKeyPair,transaction));System.out.println("Built transaction:");System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(transaction.toJson()));
Multiple cosignatories
In other multisig configurations, more signatures might be required.
In that case, they are attached using instead of
.
# Announce the transactiontransaction_hash=facade.hash_transaction(transaction)print(f'Transaction hash: {transaction_hash}')announce_transaction(json_payload,'transaction')# Wait for confirmationwait_for_confirmation(transaction_hash,'transaction')
// Announce the transactionconsttransactionHash=facade.hashTransaction(transaction).toString();console.log('Transaction hash:',transactionHash);awaitannounceTransaction(jsonPayload,'transaction');// Wait for confirmationawaitwaitForConfirmation(transactionHash,'transaction');
// Announce the transactionfinalStringtransactionHash=facade.hashTransaction(transaction).toString();System.out.printf("Transaction hash: %s%n",transactionHash);announceTransaction(jsonPayload,"transaction");// Wait for confirmationwaitForConfirmation(transactionHash,"transaction");
Transactions are rejected if they violate protocol constraints.
The following table summarizes the most common error sources:
Error message
Probable cause
Multisig Operation Prohibited By Account
The multisig account tried to sign the aggregate transaction itself.
Aggregate Ineligible Cosignatories
The signer is not in the cosignatories list.
Consumer Batch Signature Not Verifiable
The signature attached to the aggregate transaction does not match its