importasyncioimportjsonimportosimportrandomimporturllib.requestimportuuidimportstomperfromsymbolchain.CryptoTypesimportPrivateKey,PublicKeyfromsymbolchain.facade.NemFacadeimportNemFacadefromsymbolchain.ncimportAmountfromsymbolchain.nem.FeeCalculatorimportcalculate_transaction_feefromsymbolchain.nem.NetworkimportNetworkTimestampfromwebsocketsimportconnectNODE_URL=os.getenv('NODE_URL','http://libertalia.nemtest.net:7890')WS_URL=NODE_URL.replace(':7890',':7778')print(f'Using node {NODE_URL}')# SockJS has no Python client library.# These helpers wrap the raw WebSocket transport to mirror a STOMP client.defsockjs_url(endpoint_url):# SockJS raw WebSocket transport adds a random server and session idserver=random.randint(100,999)session=uuid.uuid4().hexws_base=endpoint_url.replace('http','ws',1)returnf'{ws_base}/{server}/{session}/websocket'asyncdefsend_frame(websocket,frame):# SockJS wraps each client payload as a JSON array of frame stringsawaitwebsocket.send(json.dumps([frame]))asyncdefstomp_connect(websocket):awaitwebsocket.recv()# consume the SockJS open frameawaitsend_frame(websocket,stomper.connect('','',NODE_URL,heartbeats=(0,0)))asyncdefstomp_subscribe(websocket,destination,sub_id):awaitsend_frame(websocket,stomper.subscribe(destination,sub_id))asyncdefstomp_send(websocket,destination,body):awaitsend_frame(websocket,stomper.send(destination,body))asyncdefstomp_unsubscribe(websocket,sub_id):awaitsend_frame(websocket,stomper.unsubscribe(sub_id))asyncdefstomp_disconnect(websocket):awaitsend_frame(websocket,stomper.disconnect())defstomp_messages(raw_frame):# Yield each STOMP MESSAGE frame in a SockJS data frameif'a'!=raw_frame[0]:# skip 'o' open, 'h' heartbeat, 'c' closereturnforpayloadinjson.loads(raw_frame[1:]):frame=stomper.unpack_frame(payload)if'MESSAGE'==frame['cmd']:yieldframeasyncdefstomp_frames(websocket):# Yield each STOMP MESSAGE frame as it arrivesasyncforraw_frameinwebsocket:forframeinstomp_messages(raw_frame):yieldframefacade=NemFacade('testnet')# Set up the multisig and cosignatory accountsMULTISIG_PUBLIC_KEY=os.getenv('MULTISIG_PUBLIC_KEY','D656155B48D4E71E4C59EC6FAEB5EB4F214DE8BC3C65D5BF6A3D9931B4E5ACF2')multisig_public_key=PublicKey(MULTISIG_PUBLIC_KEY)multisig_address=str(facade.network.public_key_to_address(multisig_public_key))print(f'Multisig address: {multisig_address}')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}')asyncdefmain():# [Cosignatory 0] Build and sign the multisig transactionwithurllib.request.urlopen(f'{NODE_URL}/time-sync/network-time')asresp:network_time=json.loads(resp.read().decode())['receiveTimeStamp']//1000timestamp=NetworkTimestamp(network_time)deadline=timestamp.add_hours(2)transfer_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))transaction=facade.transaction_factory.create({'type':'multisig_transaction_v1','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))signature=facade.sign_transaction(cosignatory0_key_pair,transaction)json_payload=facade.transaction_factory.attach_signature(transaction,signature)transaction_hash=str(facade.hash_transaction(transaction)).upper()print('[Cosignatory 0] Built multisig transaction 'f'{transaction_hash[:16]}...')# [Cosignatory 1] Connect to the WebSocketendpoint=f'{WS_URL}/w/messages'asyncwithconnect(sockjs_url(endpoint))aswebsocket:awaitstomp_connect(websocket)print(f'[Cosignatory 1] Connected to {WS_URL}')frames=stomp_frames(websocket)# [Cosignatory 1] Subscribe to the multisig account channelschannels={f'/account/{multisig_address}':'id-0',f'/unconfirmed/{multisig_address}':'id-1',f'/transactions/{multisig_address}':'id-2',}forchannel,sub_idinchannels.items():awaitstomp_subscribe(websocket,channel,sub_id)print(f'[Cosignatory 1] Subscribed to {channel} channel')# [Cosignatory 1] Register the multisig accountawaitstomp_send(websocket,'/w/api/account/get',json.dumps({'account':multisig_address}))asyncforframeinframes:if'/account/'inframe['headers']['destination']:balance=json.loads(frame['body'])['account']['balance']print(f'Account update: balance={balance}')breakprint('[Cosignatory 1] Multisig account registered')# [Cosignatory 0] Announce the multisig transactionprint('[Cosignatory 0] Announcing multisig transaction 'f'{transaction_hash[:16]}...')announce_request=urllib.request.Request(f'{NODE_URL}/transaction/announce',data=json_payload.encode(),headers={'Content-Type':'application/json'},method='POST')withurllib.request.urlopen(announce_request)asresp:result=json.loads(resp.read().decode())if'SUCCESS'!=result['message']:print(f'Transaction rejected: {result["message"]}')return# The transaction is now waiting for the second signature# [Cosignatory 1] Select the pending multisig transactioninner_transaction_hash=Noneasyncforframeinframes:destination=frame['headers']['destination']body=json.loads(frame['body'])if'/unconfirmed/'notindestination:continuesigner=body['transaction'].get('otherTrans',{}).get('signer','')ifsigner.upper()!=str(multisig_public_key):continueinner_transaction_hash=body['meta']['innerHash']['data']print('unconfirmed: innerHash='f'{inner_transaction_hash[:16]}...')# [Cosignatory 1] Cosign the pending transactioncosignature=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))cosignature_signature=facade.sign_transaction(cosignatory1_key_pair,cosignature)cosignature_payload=(facade.transaction_factory.attach_signature(cosignature,cosignature_signature))cosignature_request=urllib.request.Request(f'{NODE_URL}/transaction/announce',data=cosignature_payload.encode(),headers={'Content-Type':'application/json'},method='POST')withurllib.request.urlopen(cosignature_request)asresp:cosignature_result=json.loads(resp.read().decode())if'SUCCESS'!=cosignature_result['message']:print('Cosignature rejected: 'f'{cosignature_result["message"]}')returnprint('[Cosignatory 1] Announced cosignature')break# [Cosignatory 1] Wait for confirmationconfirmed=Falseasyncforframeinframes:destination=frame['headers']['destination']body=json.loads(frame['body'])if'/account/'indestination:balance=body['account']['balance']print(f'Account update: balance={balance}')ifconfirmed:breakelif'/transactions/'indestination:message_hash=body['meta']['innerHash']['data']print(f'confirmed: innerHash={message_hash[:16]}...')matched=message_hash==inner_transaction_hashifmatchedandnotconfirmed:print('Multisig transaction confirmed')confirmed=True# [Cosignatory 1] Unsubscribe before closingforsub_idinchannels.values():awaitstomp_unsubscribe(websocket,sub_id)print('[Cosignatory 1] Unsubscribed from all channels')awaitstomp_disconnect(websocket)try:asyncio.run(main())exceptExceptionaserror:print(error)
import{Client}from'@stomp/stompjs';importSockJSfrom'sockjs-client';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';constWS_URL=NODE_URL.replace(':7890',':7778');console.log(`Using node ${NODE_URL}`);constfacade=newNemFacade('testnet');// Set up the multisig and cosignatory accountsconstMULTISIG_PUBLIC_KEY=process.env.MULTISIG_PUBLIC_KEY||('D656155B48D4E71E4C59EC6FAEB5EB4F214DE8BC3C65D5BF6A3D9931B4E5ACF2');constmultisigPublicKey=newPublicKey(MULTISIG_PUBLIC_KEY);constmultisigAddress=facade.network.publicKeyToAddress(multisigPublicKey).toString();console.log(`Multisig address: ${multisigAddress}`);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{// [Cosignatory 0] Build and sign the multisig transactionconsttimeResponse=awaitfetch(`${NODE_URL}/time-sync/network-time`);constnetworkTime=Math.floor((awaittimeResponse.json()).receiveTimeStamp/1000);consttimestamp=newNetworkTimestamp(networkTime);constdeadline=timestamp.addHours(2);consttransferTransaction=facade.transactionFactory.create({type:'transfer_transaction_v2',signerPublicKey:multisigPublicKey.toString(),timestamp:timestamp.timestamp,deadline:deadline.timestamp,recipientAddress:multisigAddress,amount:1_000_000n// 1 XEM});transferTransaction.fee=newmodels.Amount(calculateTransactionFee(transferTransaction));consttransaction=facade.transactionFactory.create({type:'multisig_transaction_v1',signerPublicKey:cosignatory0KeyPair.publicKey.toString(),timestamp:timestamp.timestamp,deadline:deadline.timestamp,innerTransaction:facade.transactionFactory.static.toNonVerifiableTransaction(transferTransaction)});transaction.fee=newmodels.Amount(calculateTransactionFee(transaction));constsignature=facade.signTransaction(cosignatory0KeyPair,transaction);constjsonPayload=facade.transactionFactory.static.attachSignature(transaction,signature);consttransactionHash=facade.hashTransaction(transaction).toString().toUpperCase();constshortHash=transactionHash.substring(0,16);console.log(`[Cosignatory 0] Built multisig transaction ${shortHash}...`);// [Cosignatory 1] Connect to the WebSocketconstclient=newClient({webSocketFactory:()=>newSockJS(`${WS_URL}/w/messages`)});awaitnewPromise(resolve=>{client.onConnect=resolve;client.activate();});console.log(`[Cosignatory 1] Connected to ${WS_URL}`);// [Cosignatory 1] Select the pending multisig transactionletinnerTransactionHash=null;letresolveCosigned;constcosigned=newPromise(resolve=>{resolveCosigned=resolve;});constonUnconfirmed=asyncmessage=>{if(null!==innerTransactionHash)return;constbody=JSON.parse(message.body);constsigner=(body.transaction.otherTrans?.signer??'').toUpperCase();if(multisigPublicKey.toString()!==signer)return;innerTransactionHash=body.meta.innerHash.data;console.log('unconfirmed: innerHash='+`${innerTransactionHash.substring(0,16)}...`);// [Cosignatory 1] Cosign the pending transactionconstcosignature=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});cosignature.fee=newmodels.Amount(calculateTransactionFee(cosignature));constcosignatureSignature=facade.signTransaction(cosignatory1KeyPair,cosignature);constcosignaturePayload=facade.transactionFactory.static.attachSignature(cosignature,cosignatureSignature);constcosignatureResponse=awaitfetch(`${NODE_URL}/transaction/announce`,{method:'POST',headers:{'Content-Type':'application/json'},body:cosignaturePayload});constcosignatureResult=awaitcosignatureResponse.json();if('SUCCESS'!==cosignatureResult.message){console.log(`Cosignature rejected: ${cosignatureResult.message}`);resolveCosigned(false);return;}console.log('[Cosignatory 1] Announced cosignature');resolveCosigned(true);};// [Cosignatory 1] Wait for confirmationletconfirmed=false;letresolveRegistered;letresolveDone;constregistered=newPromise(resolve=>{resolveRegistered=resolve;});constdone=newPromise(resolve=>{resolveDone=resolve;});constonConfirmed=message=>{constmessageHash=JSON.parse(message.body).meta.innerHash.data;console.log(`confirmed: innerHash=${messageHash.substring(0,16)}...`);if(messageHash===innerTransactionHash&&!confirmed){console.log('Multisig transaction confirmed');confirmed=true;}};constonAccountUpdate=message=>{const{balance}=JSON.parse(message.body).account;console.log(`Account update: balance=${balance}`);resolveRegistered();if(confirmed)resolveDone();};// [Cosignatory 1] Subscribe to the multisig account channelsconstsubscriptions=[{channel:`/account/${multisigAddress}`,handler:onAccountUpdate,id:'id-0'},{channel:`/unconfirmed/${multisigAddress}`,handler:onUnconfirmed,id:'id-1'},{channel:`/transactions/${multisigAddress}`,handler:onConfirmed,id:'id-2'}];for(const{channel,handler,id}ofsubscriptions){client.subscribe(channel,handler,{id});console.log(`[Cosignatory 1] Subscribed to ${channel} channel`);}// [Cosignatory 1] Register the multisig accountclient.publish({destination:'/w/api/account/get',body:JSON.stringify({account:multisigAddress})});awaitregistered;console.log('[Cosignatory 1] Multisig account registered');// [Cosignatory 0] Announce the multisig transactionconsole.log('[Cosignatory 0] Announcing multisig transaction '+`${shortHash}...`);constresponse=awaitfetch(`${NODE_URL}/transaction/announce`,{method:'POST',headers:{'Content-Type':'application/json'},body:jsonPayload});constannounceResult=awaitresponse.json();if('SUCCESS'===announceResult.message){// The transaction is now waiting for the second signature// Wait for the cosignature to be announced and the// transaction to confirmif(awaitcosigned)awaitdone;}else{console.log(`Transaction rejected: ${announceResult.message}`);}// [Cosignatory 1] Unsubscribe before closingfor(const{id}ofsubscriptions)client.unsubscribe(id);console.log('[Cosignatory 1] Unsubscribed from all channels');client.deactivate();}catch(error){console.error(error);}
# Set up the multisig and cosignatory accountsMULTISIG_PUBLIC_KEY=os.getenv('MULTISIG_PUBLIC_KEY','D656155B48D4E71E4C59EC6FAEB5EB4F214DE8BC3C65D5BF6A3D9931B4E5ACF2')multisig_public_key=PublicKey(MULTISIG_PUBLIC_KEY)multisig_address=str(facade.network.public_key_to_address(multisig_public_key))print(f'Multisig address: {multisig_address}')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}')
// Set up the multisig and cosignatory accountsconstMULTISIG_PUBLIC_KEY=process.env.MULTISIG_PUBLIC_KEY||('D656155B48D4E71E4C59EC6FAEB5EB4F214DE8BC3C65D5BF6A3D9931B4E5ACF2');constmultisigPublicKey=newPublicKey(MULTISIG_PUBLIC_KEY);constmultisigAddress=facade.network.publicKeyToAddress(multisigPublicKey).toString();console.log(`Multisig address: ${multisigAddress}`);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}`);
# [Cosignatory 1] Connect to the WebSocketendpoint=f'{WS_URL}/w/messages'asyncwithconnect(sockjs_url(endpoint))aswebsocket:awaitstomp_connect(websocket)print(f'[Cosignatory 1] Connected to {WS_URL}')frames=stomp_frames(websocket)
// [Cosignatory 1] Connect to the WebSocketconstclient=newClient({webSocketFactory:()=>newSockJS(`${WS_URL}/w/messages`)});awaitnewPromise(resolve=>{client.onConnect=resolve;client.activate();});console.log(`[Cosignatory 1] Connected to ${WS_URL}`);
# [Cosignatory 1] Subscribe to the multisig account channelschannels={f'/account/{multisig_address}':'id-0',f'/unconfirmed/{multisig_address}':'id-1',f'/transactions/{multisig_address}':'id-2',}forchannel,sub_idinchannels.items():awaitstomp_subscribe(websocket,channel,sub_id)print(f'[Cosignatory 1] Subscribed to {channel} channel')
// [Cosignatory 1] Subscribe to the multisig account channelsconstsubscriptions=[{channel:`/account/${multisigAddress}`,handler:onAccountUpdate,id:'id-0'},{channel:`/unconfirmed/${multisigAddress}`,handler:onUnconfirmed,id:'id-1'},{channel:`/transactions/${multisigAddress}`,handler:onConfirmed,id:'id-2'}];for(const{channel,handler,id}ofsubscriptions){client.subscribe(channel,handler,{id});console.log(`[Cosignatory 1] Subscribed to ${channel} channel`);}
# [Cosignatory 0] Announce the multisig transactionprint('[Cosignatory 0] Announcing multisig transaction 'f'{transaction_hash[:16]}...')announce_request=urllib.request.Request(f'{NODE_URL}/transaction/announce',data=json_payload.encode(),headers={'Content-Type':'application/json'},method='POST')withurllib.request.urlopen(announce_request)asresp:result=json.loads(resp.read().decode())if'SUCCESS'!=result['message']:print(f'Transaction rejected: {result["message"]}')return# The transaction is now waiting for the second signature
// [Cosignatory 0] Announce the multisig transactionconsole.log('[Cosignatory 0] Announcing multisig transaction '+`${shortHash}...`);constresponse=awaitfetch(`${NODE_URL}/transaction/announce`,{method:'POST',headers:{'Content-Type':'application/json'},body:jsonPayload});constannounceResult=awaitresponse.json();if('SUCCESS'===announceResult.message){// The transaction is now waiting for the second signature
# [Cosignatory 1] Cosign the pending transactioncosignature=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))cosignature_signature=facade.sign_transaction(cosignatory1_key_pair,cosignature)cosignature_payload=(facade.transaction_factory.attach_signature(cosignature,cosignature_signature))cosignature_request=urllib.request.Request(f'{NODE_URL}/transaction/announce',data=cosignature_payload.encode(),headers={'Content-Type':'application/json'},method='POST')withurllib.request.urlopen(cosignature_request)asresp:cosignature_result=json.loads(resp.read().decode())if'SUCCESS'!=cosignature_result['message']:print('Cosignature rejected: 'f'{cosignature_result["message"]}')returnprint('[Cosignatory 1] Announced cosignature')break
// [Cosignatory 1] Cosign the pending transactionconstcosignature=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});cosignature.fee=newmodels.Amount(calculateTransactionFee(cosignature));constcosignatureSignature=facade.signTransaction(cosignatory1KeyPair,cosignature);constcosignaturePayload=facade.transactionFactory.static.attachSignature(cosignature,cosignatureSignature);constcosignatureResponse=awaitfetch(`${NODE_URL}/transaction/announce`,{method:'POST',headers:{'Content-Type':'application/json'},body:cosignaturePayload});constcosignatureResult=awaitcosignatureResponse.json();if('SUCCESS'!==cosignatureResult.message){console.log(`Cosignature rejected: ${cosignatureResult.message}`);resolveCosigned(false);return;}console.log('[Cosignatory 1] Announced cosignature');resolveCosigned(true);
# [Cosignatory 1] Unsubscribe before closingforsub_idinchannels.values():awaitstomp_unsubscribe(websocket,sub_id)print('[Cosignatory 1] Unsubscribed from all channels')awaitstomp_disconnect(websocket)
// [Cosignatory 1] Unsubscribe before closingfor(const{id}ofsubscriptions)client.unsubscribe(id);console.log('[Cosignatory 1] Unsubscribed from all channels');client.deactivate();