Symbol Python SDK: A Beginners Guide to Transactions

Introduction

This document provides examples of common Symbol transaction types written using the Python SDK. The Symbol Python SDK is a powerful tool that enables developers to interact with the Symbol blockchain and build applications with ease. This guide is aimed at beginners and assumes no prior knowledge of the Symbol Python SDK. The examples provided here demonstrate how to create and send various types of transactions on the Symbol blockchain. Each example is accompanied by a simple explanation of how the transactions function and the code is commented (perhaps excessively!) to provide additional context for readers who are new to the SDK. All code is written for testnet hence I am not worried about sharing Alice’s private key 😂

This article only covers Symbol but NEM transactions will be covered in a second article coming soon.

Note: I wrote this article some time ago and have not retested the code that I wrote so if something doesn’t work then let me know. As always I will give the disclaimer that I am not a real programmer so please overlook any inelegance in my code 😳

Simple Transfer

The code below shows an example of how to transfer mosaics and send messages between two accounts. In the example, Alice sends Bob 5 XYM with a message that reads “message goes here”. Both message and mosaic transfer are optional and multiple different mosaics can be included in a single transaction. The fee charged is dependent on the size of the transaction so in the example we include a fee of 0.00015 XYM per byte of data sent rather than setting a fixed fee.

Note: encrypted messages can also be sent. These can only be read by the message recipient when decrypted using their private key.

Transaction

Code

from symbolchain.symbol.KeyPair import KeyPair
from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.CryptoTypes import PrivateKey
from binascii import unhexlify, hexlify
from symbolchain.sc import Amount
import http.client
import datetime

facade = SymbolFacade('testnet')

# Replace with your private key
alicePrivateKey = PrivateKey(unhexlify('4D76AA199BE4EE93E217E303174C289EA49B380B7960EA0D75E430C38A67629E'))

# Replace with the recipient address
bobAddress = 'TDRJXPACFDXROTUCLUEO2QZUEQ4H5AOZIEUWGIY'

# Replace with node URL and connection port
node_url = 'mikun-testnet.tk'
node_port = 3000

# Set testnet network generation time and then add deadline for the
# transaction (2 hours)
netStart = 1667250467
deadline = (int((datetime.datetime.today() + datetime.timedelta(hours=2)).timestamp()) - netStart) * 1000

# Calculate public key and address from private key
aliceKeypair = SymbolFacade.KeyPair(alicePrivateKey)
alicePubkey = aliceKeypair.public_key
aliceAddress = facade.network.public_key_to_address(alicePubkey)

# Create a transfer transaction
transfer = facade.transaction_factory.create({
    'type': 'transfer_transaction_v1',
    'signer_public_key': alicePubkey, # add signer (sender)
		'recipient_address': bobAddress, # add recipient
    'deadline': deadline, # 2 hour deadline
		# Message to send (add preceeding '00' byte for compatibility with
		# explorer (otherwise message is displayed as a hex value)
    'message': bytes(1) + 'message goes here'.encode('utf8'),
    'mosaics': [
		# Multiple mosaics can be added in a single transaction
		# Because the XYM mosaic has a divisibility of 6 we need to
		# multiply the amount to send my 1,000,000 so a transfer of
		# 5,000,000 units would equate to 5 XYM
    {'mosaic_id': 0x72C0212E67A08BCE, 'amount': 5000000}
    ]
})

# Dynamically set the fee based on the transfer size
# 0.00015 XYM per byte
transfer.fee.value = transfer.size * 150

# Sign the transaction
signature = facade.sign_transaction(aliceKeypair, transfer)

# Attach the signature to the transaction
tx = facade.transaction_factory.attach_signature(transfer, signature)

# Announce the transaction to the network

headers = {'Content-type': 'application/json'}
conn = http.client.HTTPConnection(node_url, node_port)
conn.request("PUT", "/transactions", tx, headers)

response = conn.getresponse()
print(response.status, response.reason)

Aggregate complete transactions

It is possible to aggregate transactions, sending funds and/or messages to multiple recipients in a single transaction. Aggregate complete transactions require only the signature of the initiating account. They are constructed by defining all embedded transactions and then wrapping these inside an aggregate that is then signed and submitted to a node. Note: all inner transactions must be created with facade.transaction_factory.create_embedded.

In this example, Alice sends 5 XYM to Bob and 2 XYM to Carol with messages in a single transaction.

Transaction

Code

from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.CryptoTypes import PrivateKey
from binascii import unhexlify, hexlify
import http.client
import datetime

facade = SymbolFacade('testnet')

# Replace with your private key
alicePrivateKey = PrivateKey(unhexlify('4D76AA199BE4EE93E217E303174C289EA49B380B7960EA0D75E430C38A67629E'))

# Replace with the recipient address
bobAddress = 'TDRJXPACFDXROTUCLUEO2QZUEQ4H5AOZIEUWGIY'
carolAddress = 'TACHTT5XDU55QYFML2XAEC3WD6BJ5LH56TUFXHA'

# Replace with node URL and connection port

node_url = 'mikun-testnet.tk'
node_port = 3000

# Set testnet network generation time and then add deadline
netStart = 1667250467
deadline = (int((datetime.datetime.today() + datetime.timedelta(hours=2)).timestamp()) - netStart) * 1000

aliceKeypair = SymbolFacade.KeyPair(alicePrivateKey)
alicePubkey = aliceKeypair.public_key
aliceAddress = facade.network.public_key_to_address(alicePubkey)

# Set up the transactions

# A list which will contain each of the inner transactions
embeddedTxs = []

# Transaction 1 - sends 5 XYM to Bob
sendToBob = facade.transaction_factory.create_embedded({
			'type': 'transfer_transaction_v1',
			'signer_public_key': alicePubkey,
			'recipient_address': bobAddress,
			'message': bytes(1) + 'Alice sent you 5 XYM'.encode('utf8'),
    	'mosaics': [
    			{'mosaic_id': 0x72C0212E67A08BCE, 'amount': 5000000}
    			]
		})
# Add the embedded transaction to the list
embeddedTxs.append(sendToBob)

# Transaction 2 - sends 2 XYM to Carol
sendToCarol = facade.transaction_factory.create_embedded({
			'type': 'transfer_transaction_v1',
			'signer_public_key': alicePubkey,
			'recipient_address': carolAddress,
			'message': bytes(1) + 'Alice sent you 2 XYM'.encode('utf8'),
    	'mosaics': [
    		{'mosaic_id': 0x72C0212E67A08BCE, 'amount': 2000000}
    		]
		})
# Add the embedded transaction to the list
embeddedTxs.append(sendToCarol)

# Calculate the Merkle hash of the embedded transactions
merkleHash = facade.hash_embedded_transactions(embeddedTxs)

# Wrap the transactions into a single aggregate
aggregate_transaction = facade.transaction_factory.create({
		'type': 'aggregate_complete_transaction_v2',
		'signer_public_key': alicePubkey,
		'deadline': deadline,
		'transactions_hash': merkleHash,
		'transactions': embeddedTxs
	})

# Set the fee
aggregate_transaction.fee.value = aggregate_transaction.size * 150

# Sign
signature = facade.sign_transaction(aliceKeypair, aggregate_transaction)

# Attach Alice's signature
tx = facade.transaction_factory.attach_signature(aggregate_transaction, signature)
print(tx)

# Announce the transaction to the network

headers = {'Content-type': 'application/json'}
conn = http.client.HTTPConnection(node_url, node_port)
conn.request("PUT", "/transactions", tx, headers)

response = conn.getresponse()
print(response.status, response.reason)

Aggregate bonded transactions

Aggregate bonded transactions require two or more signatures and can be used to create complex transactions where assets are swapped between multiple accounts. If one part of the aggregate does not complete then the entire transaction will fail. Aggregate bonded transactions can be used to remove the requirement for a trusted third party to provide and escrow service and allow trustless swaps between multiple accounts.

In this example, Alice wants to buy a mosaic (0x3C9A67E6DF91E5C7) from Bob, she doesn’t know Bob and therefore does not want to send funds without knowing that she will receive the mosaic in return. She can set up an aggregate bonded transaction where she sends 5 XYM to Bob and requests the mosaic in return. If both parties sign, then the transaction will complete with Alice being sent the mosaic and Bob receiving 5 XYM.

Before sending an aggregate bonded transaction a hash lock transaction must be confirmed. These transactions act as a deposit and the transaction initiator is charged a fix fee of 10 XYM. If the transaction completes then Alice’s deposit will be refunded.

Note: We need to confirm that the hash lock transaction is confirmed before submitting the aggregate as otherwise, the transaction will fail. I have added some code is_transaction_confirmed to check this. It will wait for confirmation before submitting the second transaction. You will notice that the aggregate bonded transaction is sent to the partial transactions endpoint where it will await Bob’s signature which must be provided before the transaction expires. Note that the hashlock duration is in blocks so the transaction will fail after 480 blocks (~4 hours) if it is not signed by all parties.

Transaction

Code

from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.CryptoTypes import PrivateKey
from binascii import unhexlify, hexlify
import http.client
import datetime
import requests 
import time


facade = SymbolFacade('testnet')

# Replace with your private key
alicePrivateKey = PrivateKey(unhexlify('4D76AA199BE4EE93E217E303174C289EA49B380B7960EA0D75E430C38A67629E'))

# Replace with the recipient address
bobAddress = 'TDRJXPACFDXROTUCLUEO2QZUEQ4H5AOZIEUWGIY'

# Replace with node URL and connection port

node_url = 'mikun-testnet.tk'
node_port = 3000

# Set testnet network generation time and then add deadline
netStart = 1667250467
deadline = (int((datetime.datetime.today() + datetime.timedelta(hours=2)).timestamp()) - netStart) * 1000

aliceKeypair = SymbolFacade.KeyPair(alicePrivateKey)
alicePubkey = aliceKeypair.public_key
aliceAddress = facade.network.public_key_to_address(alicePubkey)
bobPubkey = '8E1207F079E7912A6A2B8B2166741C449FFC8CDF4ECED0E7D954A418A6DD1AAE'

# Set up the aggregate transactions

embeddedTxs = []

aliceToBob = facade.transaction_factory.create_embedded({
			'type': 'transfer_transaction_v1',
			'signer_public_key': alicePubkey,
			'recipient_address': bobAddress,
			'message': bytes(1) + 'Here is 5 XYM to swap for your mosaic'.encode('utf8'),
    		'mosaics': [
    			{'mosaic_id': 0x72C0212E67A08BCE, 'amount': 5000000}
    			]
		})
embeddedTxs.append(aliceToBob)
		
bobToAlice = facade.transaction_factory.create_embedded({
			'type': 'transfer_transaction_v1',
			'signer_public_key': bobPubkey,
			'recipient_address': aliceAddress,
			'message': bytes(1) + 'Sending the mosaic'.encode('utf8'),
    		'mosaics': [
    			{'mosaic_id': 0x3C9A67E6DF91E5C7, 'amount': 1}
    			]
		})
embeddedTxs.append(bobToAlice)

merkleHash = facade.hash_embedded_transactions(embeddedTxs)

aggregate_transaction = facade.transaction_factory.create({
		'type': 'aggregate_bonded_transaction_v2',
		'signer_public_key': alicePubkey,
		'deadline': deadline,
		'transactions_hash': merkleHash,
		'transactions': embeddedTxs
	})
	
aggregate_transaction.fee.value = aggregate_transaction.size * 150

signature = facade.sign_transaction(aliceKeypair, aggregate_transaction)
tx = facade.transaction_factory.attach_signature(aggregate_transaction, signature)

# Set up the hashlock transaction
txHash = facade.hash_transaction(aggregate_transaction)

hashLockTx = facade.transaction_factory.create({
'type': 'hash_lock_transaction_v1',
'signer_public_key': alicePubkey,
'hash': txHash,
'deadline': deadline,
'mosaic': {'mosaic_id': 0x72C0212E67A08BCE, 'amount': 10000000},
'duration': 480
})

hashLockTx.fee.value = hashLockTx.size * 150

hashLockSignature = facade.sign_transaction(aliceKeypair, hashLockTx)
facade.transaction_factory.attach_signature(hashLockTx, hashLockSignature)
hashLock = facade.hash_transaction(hashLockTx)
hashLockTx = facade.transaction_factory.attach_signature(hashLockTx, hashLockSignature)

# Code to check the hashlock transaction is confirmed before sending
# The aggregate (otherwise the second transaction would fail)
def is_transaction_confirmed(lock_tx_hash, node_url, node_port):
    endpoint = f"http://{node_url}:{node_port}/transactions/confirmed/{lock_tx_hash}"
    response = requests.get(endpoint)
    return response.status_code == 200

# Announce the transactions to the network

headers = {'Content-type': 'application/json'}
conn = http.client.HTTPConnection(node_url, node_port)
conn.request("PUT", "/transactions", hashLockTx, headers)

response = conn.getresponse()
print(response.status, response.reason)

# Hash lock needs to be confirmed before sending the aggregate

confirmed = False
while not confirmed:
    print("Waiting for hash lock transaction to be confirmed...")
    time.sleep(10)  # Adjust the sleep time as needed
    confirmed = is_transaction_confirmed(hashLock, node_url, node_port)

print("Hash lock transaction confirmed. Announcing aggregate transaction...")


conn = http.client.HTTPConnection(node_url, node_port)
conn.request("PUT", "/transactions/partial", tx, headers)

response = conn.getresponse()
print(response.status, response.reason)

Secret lock transactions

Secret lock transactions allow funds to be sent to an account and locked until a proof is provided. These are more widely known as Hashed Timelock Contracts (HTLCs). In this example, Alice creates a secret by computing the SHA3-256 hash of the plaintext string ‘test12345’, in reality this would likely be random data. She selects the duration of the transaction (20160 blocks) meaning that the proof needs to be provided within this timeframe, otherwise it will expire. She will send 5 XYM to Bob which will be unlocked once the correct proof is announced in a secret proof transaction.

Transaction

Code

from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.CryptoTypes import PrivateKey
from binascii import unhexlify, hexlify
import http.client
import datetime
import sha3

facade = SymbolFacade('testnet')

# Replace with your private key
alicePrivateKey = PrivateKey(unhexlify('4D76AA199BE4EE93E217E303174C289EA49B380B7960EA0D75E430C38A67629E'))

# Replace with the recipient address
bobAddress = 'TDRJXPACFDXROTUCLUEO2QZUEQ4H5AOZIEUWGIY'

# Replace with node URL and connection port

node_url = 'mikun-testnet.tk'
node_port = 3000

# Set testnet network generation time and then add deadline
netStart = 1667250467
deadline = (int((datetime.datetime.today() + datetime.timedelta(hours=2)).timestamp()) - netStart) * 1000

aliceKeypair = SymbolFacade.KeyPair(alicePrivateKey)
alicePubkey = aliceKeypair.public_key
aliceAddress = facade.network.public_key_to_address(alicePubkey)

# Create SHA3-256 hash of a secret. In this example my secret is 'test12345'
# but in a real world example you would probably want to feed in a randomly
# generated string
secret = sha3.sha3_256('test12345'.encode('utf8')).digest()

# Create a secret lock transaction.
transfer = facade.transaction_factory.create({
    'type': 'secret_lock_transaction_v1',
    'signer_public_key': alicePubkey,
		'recipient_address': bobAddress,
    'deadline': deadline,
		# This is the duration of the secret lock in blocks. 20160 blocks is around
		# 168 hours or 7 days
    'duration': 20160, 
    'secret': secret, # Our encrypted secret
    'mosaic': {'mosaic_id': 0x72C0212E67A08BCE, 'amount': 5000000},
    'hash_algorithm': 'sha3_256' # Specify the hash algorithm
})

# Dynamically set the fee based on the transfer size
transfer.fee.value = transfer.size * 150

# Alice signs the transaction
signature = facade.sign_transaction(aliceKeypair, transfer)

# Attach Alice's signature
tx = facade.transaction_factory.attach_signature(transfer, signature)

# Announce the transaction to the network

headers = {'Content-type': 'application/json'}
conn = http.client.HTTPConnection(node_url, node_port)
conn.request("PUT", "/transactions", tx, headers)

response = conn.getresponse()
print(response.status, response.reason)

Secret proof transactions

Secret proof transactions are used to unlock funds sent as secret locks. In this example, Alice has revealed the proof to Bob and he will then announce this to the network so the funds that Alice sent above (5 XYM) are credited to his account.

Bob provides both the secret and the proof along with the hashing algorithm used and once announced, if the proof is correct then the unlock transaction will complete successfully.

Transaction

Code

from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.CryptoTypes import PrivateKey
from binascii import unhexlify, hexlify
import http.client
import datetime

facade = SymbolFacade('testnet')

# Replace with your private key
bobPrivateKey = PrivateKey(unhexlify('43FE5378007766CDD002CAA40682E989EEF52F42CD8DF81CA82909BD87859D2C'))

# Replace with node URL and connection port

node_url = 'mikun-testnet.tk'
node_port = 3000

# Set testnet network generation time and then add deadline
netStart = 1667250467
deadline = (int((datetime.datetime.today() + datetime.timedelta(hours=2)).timestamp()) - netStart) * 1000

bobKeypair = SymbolFacade.KeyPair(bobPrivateKey)
bobPubkey = bobKeypair.public_key
bobAddress = facade.network.public_key_to_address(bobPubkey)

# Create a secret proof transaction
unlock = facade.transaction_factory.create({
    'type': 'secret_proof_transaction_v1',
    'signer_public_key': bobPubkey,
    'recipient_address': bobAddress,
    'deadline': deadline,
		# This is the SHA3-256 hash of 'test12345' that Alice announced above
    'secret': '4A9D2310D24A54F034112E54270D4CD043DE5E958B718F782D66104B1161C88F',
    'hash_algorithm': 'sha3_256', # Set the hashing algorithm
		# This is the original plain text string used to create the secret proving
		# that Bob knows it and that the funds should be released to him
    'proof': 'test12345' 
})

# Dynamically set the fee based on the transfer size
unlock.fee.value = unlock.size * 150

# Bob signs the transaction
signature = facade.sign_transaction(bobKeypair, unlock)

# Attach Bob's signature
tx = facade.transaction_factory.attach_signature(unlock, signature)

# Announce the transaction to the network

headers = {'Content-type': 'application/json'}
conn = http.client.HTTPConnection(node_url, node_port)
conn.request("PUT", "/transactions", tx, headers)

response = conn.getresponse()
print(response.status, response.reason)

Mosaic definition

Tokens, known as mosaics can be created using the Python SDK. When creating a new mosaic there are several different flags that can be used to define their properties. Firstly a divisibility value is set the minimum being 0 meaning that the token is not divisible and the maximum being 6 meaning that the lowest denomination is 0.000001. Mosaic properties can be set to determine whether the token should be transferrable, mutable, revokable and restrictable.

Mosaic flags

Supply mutable: The supply of the mosaic can be changed at any point by the mosaic issuer if set to true, else the supply is fixed once set by the mosaic issuer.

Transferrable: The mosaic can be transferred to another party if set to true, else the mosaic can only be sent by the mosaic issuer but not the recipient.

Restrictable: If set to true then restrictions can be set on the mosaic by the mosaic creator

Revokable: The mosaic can be revoked from any account holding the token at any point by the issuer if set to true.

In the example below, Alice creates a new mosaic that is transferable, mutable and revokable with a divisibility of zero. A nonce value needs to be supplied which is used in conjunction with the address to create a unique mosaic ID. In the example, the nonce is set to 123456 but in reality, it is likely that a random integer would be assigned.

Code

from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.CryptoTypes import PrivateKey
from binascii import unhexlify, hexlify
import http.client
import datetime

facade = SymbolFacade('testnet')

# Replace with your private key
alicePrivateKey = PrivateKey(unhexlify('4D76AA199BE4EE93E217E303174C289EA49B380B7960EA0D75E430C38A67629E'))

# Replace with node URL and connection port

node_url = 'mikun-testnet.tk'
node_port = 3000

# Set testnet network generation time and then add deadline
netStart = 1667250467
deadline = (int((datetime.datetime.today() + datetime.timedelta(hours=2)).timestamp()) - netStart) * 1000

aliceKeypair = SymbolFacade.KeyPair(alicePrivateKey)
alicePubkey = aliceKeypair.public_key
aliceAddress = facade.network.public_key_to_address(alicePubkey)


# Create a mosaic definition transaction
mosaic_def = facade.transaction_factory.create({
    'type': 'mosaic_definition_transaction_v1',
    'signer_public_key': alicePubkey,
    'deadline': deadline,
    'duration': 0,
		# The mosaic flags are set to allow transfers, allow the mosaic creator
		# to modify the supply and to revoke mosaics from other accounts
		'flags': 'transferable supply_mutable revokable',
		'nonce': 123456,
		'divisibility': 0
})

# Set fee
mosaic_def.fee.value = 150 * mosaic_def.size

# Sign the transaction
signature = facade.sign_transaction(aliceKeypair, mosaic_def)

# Attach the transaction
tx = facade.transaction_factory.attach_signature(mosaic_def, signature)

# Announce the transaction to the network

headers = {'Content-type': 'application/json'}
conn = http.client.HTTPConnection(node_url, node_port)
conn.request("PUT", "/transactions", tx, headers)

response = conn.getresponse()
print(response.status, response.reason)

Mosaic supply change

Once the mosaic has been created it is possible to change the maximum supply. This is possible even if the supply is not mutable as long as the account that created the mosaic has not yet transferred any of the tokens to other accounts. For mutable mosaics, the maximum supply can be adjusted at any time up to a maximum of 8,999,999,999 units. The supply change transaction can also be wrapped in an aggregate with the mosaic definition to perform both operations in a single transaction (the transactions must be ordered in the transaction so that it is first defined and then the supply is changed).

In the example below, Alice specifies the mosaic ID that she wants to alter the supply of and increases the number of tokens by 1,000. The delta value can be changed to 0x0 which would instead decrease the supply.

Note: Be careful with the units in the amount field, this mosaic is not divisible so increasing supply by 1,000 will create 1,000 additional mosaics. However, if the mosaic has a divisibility value of 6 then increasing supply by 1,000 would lead to an increased supply of 0.001 units.

from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.CryptoTypes import PrivateKey
from binascii import unhexlify, hexlify
import http.client
import datetime

facade = SymbolFacade('testnet')

# Replace with your private key
alicePrivateKey = PrivateKey(unhexlify('4D76AA199BE4EE93E217E303174C289EA49B380B7960EA0D75E430C38A67629E'))

# Replace with node URL and connection port

node_url = 'mikun-testnet.tk'
node_port = 3000

# Set testnet network generation time and then add deadline
netStart = 1667250467
deadline = (int((datetime.datetime.today() + datetime.timedelta(hours=2)).timestamp()) - netStart) * 1000

aliceKeypair = SymbolFacade.KeyPair(alicePrivateKey)
alicePubkey = aliceKeypair.public_key
aliceAddress = facade.network.public_key_to_address(alicePubkey)

# Create a mosaic supply change transaction
mosaic_supply = facade.transaction_factory.create({
    'type': 'mosaic_supply_change_transaction_v1',
    'signer_public_key': alicePubkey,
    'deadline': deadline,
		'delta': 1000, # number of mosaics to add/remove
		'action': 0x1, # 0x1 to add or 0x0 to remove 
		'mosaic_id': 0x53CD7D13A450B11C # mosaic to change supply of
})

# Add the fee
mosaic_supply.fee.value = 150 * mosaic_supply.size

# Sign the transaction
signature = facade.sign_transaction(aliceKeypair, mosaic_supply)

# Attach the signature
tx = facade.transaction_factory.attach_signature(mosaic_supply, signature)

# Announce the transaction to the network

headers = {'Content-type': 'application/json'}
conn = http.client.HTTPConnection(node_url, node_port)
conn.request("PUT", "/transactions", tx, headers)

response = conn.getresponse()
print(response.status, response.reason)

Mosaic revocation

Mosaics with a revokable flag can be reclaimed by the issuing account. In the example below Alice revokes 15 0x53CD7D13A450B11C mosaics from Bob’s account. These are then credited back to Alice’s address.

Note: This is only possible for mosaics with a revokable flag and no signature is required from the account containing the mosaic to be revoked meaning that they cannot refuse to send the mosaic back to the issuer.

Transaction

Code

from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.CryptoTypes import PrivateKey
from binascii import unhexlify, hexlify
import http.client
import datetime

facade = SymbolFacade('testnet')

# Replace with your private key
alicePrivateKey = PrivateKey(unhexlify('4D76AA199BE4EE93E217E303174C289EA49B380B7960EA0D75E430C38A67629E'))

# Replace with the recipient address
bobAddress = 'TDRJXPACFDXROTUCLUEO2QZUEQ4H5AOZIEUWGIY'

# Replace with node URL and connection port

node_url = 'mikun-testnet.tk'
node_port = 3000

# Set testnet network generation time and then add deadline
netStart = 1667250467
deadline = (int((datetime.datetime.today() + datetime.timedelta(hours=2)).timestamp()) - netStart) * 1000

aliceKeypair = SymbolFacade.KeyPair(alicePrivateKey)
alicePubkey = aliceKeypair.public_key
aliceAddress = facade.network.public_key_to_address(alicePubkey)


# Create a revocation transaction
mosaic_rev = facade.transaction_factory.create({
    'type': 'mosaic_supply_revocation_transaction_v1',
    'signer_public_key': alicePubkey,
    'source_address': bobAddress, # address to revoke mosaics from
    'deadline': deadline,
		# Revoke 15 mosaics (divisibility of this mosaic is 0)
		'mosaic': {'mosaic_id': 0x53CD7D13A450B11C, 'amount': 15}
})

# Add fee
mosaic_rev.fee.value = 150 * mosaic_rev.size

# Sign the transaction
signature = facade.sign_transaction(aliceKeypair, mosaic_rev)

# Attach transaction
tx = facade.transaction_factory.attach_signature(mosaic_rev, signature)

# Announce the transaction to the network

headers = {'Content-type': 'application/json'}
conn = http.client.HTTPConnection(node_url, node_port)
conn.request("PUT", "/transactions", tx, headers)

response = conn.getresponse()
print(response.status, response.reason)

Namespace rental

Namespaces are human-readable addresses that can be assigned to either accounts or mosaics. They are rented for a set number of blocks with the fee dynamically calculated based on the median fee multiplier over the previous 60 blocks and the rental duration.

The minimum rental period is 86,400 blocks which equates to 30 days given an average block time of 30 seconds. Namespace rentals can be extended during this time and if not renewed then they will expire after a 30-day grace period.

In the example below, Alice registers a namespace testnamespace123 for a period of 86,400 blocks (around 30 days).

Code

from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.CryptoTypes import PrivateKey
from binascii import unhexlify, hexlify
import http.client
import datetime

facade = SymbolFacade('testnet')

# Replace with your private key
alicePrivateKey = PrivateKey(unhexlify('4D76AA199BE4EE93E217E303174C289EA49B380B7960EA0D75E430C38A67629E'))

# Replace with node URL and connection port

node_url = 'mikun-testnet.tk'
node_port = 3000

# Set testnet network generation time and then add deadline
netStart = 1667250467
deadline = (int((datetime.datetime.today() + datetime.timedelta(hours=2)).timestamp()) - netStart) * 1000

aliceKeypair = SymbolFacade.KeyPair(alicePrivateKey)
alicePubkey = aliceKeypair.public_key
aliceAddress = facade.network.public_key_to_address(alicePubkey)


# Create a namespace registration transaction
namespace_def = facade.transaction_factory.create({
    'type': 'namespace_registration_transaction_v1',
    'signer_public_key': alicePubkey,
    'deadline': deadline,
		'duration': 86400, # Rental period (minimum duration is 86400 blocks)
		'name': 'testnamespace123', # Name of the namespace to register
		'registration_type': 'root' # This is a top-level namespace
})

# Rental fee is dependent on the duration (in blocks)
namespace_def.fee.value = 150 * namespace_def.size

# Sign the transaction
signature = facade.sign_transaction(aliceKeypair, namespace_def)

# Attach the signature
tx = facade.transaction_factory.attach_signature(namespace_def, signature)

# Announce the transaction to the network

headers = {'Content-type': 'application/json'}
conn = http.client.HTTPConnection(node_url, node_port)
conn.request("PUT", "/transactions", tx, headers)

response = conn.getresponse()
print(response.status, response.reason)

Sub namespace registration

Sub namespaces are child namespaces that can be assigned to a parent. Sub namespaces can be registered at a flat fee of $0.1 XYM * averageFeeMultiplier. They do not expire until the parent namespace does and therefore we do not need to set a rental period.

In the example below, Alice registers the sub namespace itsasubnamespace and links this to the parent namespace 0xE5F00DB6D7E239B8 the mosaic ID of the parent testnamespace123 defined above. This results in the subnamespace testnamespace123.itsasubnamespace.

Code

from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.CryptoTypes import PrivateKey
from binascii import unhexlify, hexlify
import http.client
import datetime

facade = SymbolFacade('testnet')

# Replace with your private key
alicePrivateKey = PrivateKey(unhexlify('4D76AA199BE4EE93E217E303174C289EA49B380B7960EA0D75E430C38A67629E'))

# Replace with node URL and connection port

node_url = 'mikun-testnet.tk'
node_port = 3000

# Set testnet network generation time and then add deadline
netStart = 1667250467
deadline = (int((datetime.datetime.today() + datetime.timedelta(hours=2)).timestamp()) - netStart) * 1000

aliceKeypair = SymbolFacade.KeyPair(alicePrivateKey)
alicePubkey = aliceKeypair.public_key
aliceAddress = facade.network.public_key_to_address(alicePubkey)


# Create a subnamespace registration transaction
namespace_def = facade.transaction_factory.create({
    'type': 'namespace_registration_transaction_v1',
    'signer_public_key': alicePubkey,
    'deadline': deadline,
		# Set the parent namespace by the namespace ID. This is the ID of the
		# namespace "testnamespace123" registered above. 
		'parent_id': 0xE5F00DB6D7E239B8,
		'registration_type': 'child', # this is a child namespace
		'fee': 10000000, # Fee for registering the child namespace
		# Name of the child namespace to register in this case "itsasubnamespace".
		# This will be assigned to the parent "testnamespace123" to become
		# "testnamespace123.itsasubnamespace"
		'name': 'itsasubnamespace'
})

# Sign the transaction
signature = facade.sign_transaction(aliceKeypair, namespace_def)

# Attach the signature
tx = facade.transaction_factory.attach_signature(namespace_def, signature)

# Announce the transaction to the network
headers = {'Content-type': 'application/json'}
conn = http.client.HTTPConnection(node_url, node_port)
conn.request("PUT", "/transactions", tx, headers)

response = conn.getresponse()
print(response.status, response.reason)

Address alias

Namespaces can be linked to a Symbol address through the namespace identifier. This allows others to send transactions to the human-readable namespace ID rather than the Symbol address of an account. In this example, Alice links her address TC3FJIODPXZTVKIM4IKTPCUWRN334ABAU74IKOI to the namespace testnamespace123 defined previously through the namespace identifier 0xE5F00DB6D7E239B8.

Code

from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.CryptoTypes import PrivateKey
from binascii import unhexlify, hexlify
import http.client
import datetime

facade = SymbolFacade('testnet')

# Replace with your private key
alicePrivateKey = PrivateKey(unhexlify('4D76AA199BE4EE93E217E303174C289EA49B380B7960EA0D75E430C38A67629E'))

# Replace with node URL and connection port

node_url = 'mikun-testnet.tk'
node_port = 3000

# Set testnet network generation time and then add deadline
netStart = 1667250467
deadline = (int((datetime.datetime.today() + datetime.timedelta(hours=2)).timestamp()) - netStart) * 1000

aliceKeypair = SymbolFacade.KeyPair(alicePrivateKey)
alicePubkey = aliceKeypair.public_key
aliceAddress = facade.network.public_key_to_address(alicePubkey)

# Create a address alias transaction linking address to namespace
account_alias = facade.transaction_factory.create({
    'type': 'address_alias_transaction_v1',
    'signer_public_key': alicePubkey,
    'deadline': deadline,
		'namespace_id': 0xE5F00DB6D7E239B8, # ID of the namespace to link
		'address': aliceAddress,
		'alias_action': 'link' # Links the namespace. Can also be unlinked
})

# Add the fee
account_alias.fee.value = account_alias.size * 150

# Sign the transaction
signature = facade.sign_transaction(aliceKeypair, account_alias)

# Attach the signature
tx = facade.transaction_factory.attach_signature(account_alias, signature)

# Announce the transaction to the network
headers = {'Content-type': 'application/json'}
conn = http.client.HTTPConnection(node_url, node_port)
conn.request("PUT", "/transactions", tx, headers)

response = conn.getresponse()
print(response.status, response.reason)

Mosaic alias

Similarly, mosaics can be assigned to a namespace. In this example, the mosaic ID 0x53CD7D13A450B11C is linked to the sub-namespace testnamespace123.itsasubnamespace through its identifier 0xF411C4888B14DFCA.

Code

from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.CryptoTypes import PrivateKey
from binascii import unhexlify, hexlify
import http.client
import datetime

facade = SymbolFacade('testnet')

# Replace with your private key
alicePrivateKey = PrivateKey(unhexlify('4D76AA199BE4EE93E217E303174C289EA49B380B7960EA0D75E430C38A67629E'))

# Replace with node URL and connection port

node_url = 'mikun-testnet.tk'
node_port = 3000

# Set testnet network generation time and then add deadline
netStart = 1667250467
deadline = (int((datetime.datetime.today() + datetime.timedelta(hours=2)).timestamp()) - netStart) * 1000

aliceKeypair = SymbolFacade.KeyPair(alicePrivateKey)
alicePubkey = aliceKeypair.public_key
aliceAddress = facade.network.public_key_to_address(alicePubkey)

# Create a mosaic alias transaction (mosaic is linked to a namespace)
mosaic_alias = facade.transaction_factory.create({
    'type': 'mosaic_alias_transaction_v1',
    'signer_public_key': alicePubkey,
    'deadline': deadline,
		'namespace_id': 0xF411C4888B14DFCA, # Namespace ID to link to
		'mosaic_id': 0x53CD7D13A450B11C, # Mosaic ID to link
		'alias_action': 'link' # Link (can be unlinked)
})

# Add fees
mosaic_alias.fee.value = mosaic_alias.size * 150

# Sign the transaction
signature = facade.sign_transaction(aliceKeypair, mosaic_alias)

# Attach signature
tx = facade.transaction_factory.attach_signature(mosaic_alias, signature)

# Announce the transaction to the network
headers = {'Content-type': 'application/json'}
conn = http.client.HTTPConnection(node_url, node_port)
conn.request("PUT", "/transactions", tx, headers)

response = conn.getresponse()
print(response.status, response.reason)

Account metadata

Metadata can be written to an account in a key/value format. In the example below Alice is writing metadata to Bob’s account. Let’s imagine that Bob subscribed to Alice’s blog and that this allows access to paywalled content. Alice can write metadata to Bob’s account so that when he signs into the blog using his Symbol wallet he will be identified as a subscriber. Alice sets a metadata key 1234 which might represent subscription status and assigns the value ‘subscribed’ to identify that Bob is a paid subscriber.

Permission must be given by the receiving account before metadata can be written to it. For this, the transaction needs to be wrapped within an aggregate bonded transaction so that a signature can be collected from Bob to authorise the data to be written to his account. Alice can later update metadata written to Bob’s account but again it requires a signature from Bob to give his permission to do so.

Transaction

Code

from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.CryptoTypes import PrivateKey
from binascii import unhexlify, hexlify
import http.client
import datetime
import time
import requests

facade = SymbolFacade('testnet')

# Replace with your private key
alicePrivateKey = PrivateKey(unhexlify('4D76AA199BE4EE93E217E303174C289EA49B380B7960EA0D75E430C38A67629E'))

# Replace with the recipient address
bobAddress = 'TDRJXPACFDXROTUCLUEO2QZUEQ4H5AOZIEUWGIY'

# Replace with node URL and connection port

node_url = 'mikun-testnet.tk'
node_port = 3000

# Set testnet network generation time and then add deadline
netStart = 1667250467
deadline = (int((datetime.datetime.today() + datetime.timedelta(hours=2)).timestamp()) - netStart) * 1000

aliceKeypair = SymbolFacade.KeyPair(alicePrivateKey)
alicePubkey = aliceKeypair.public_key
aliceAddress = facade.network.public_key_to_address(alicePubkey)
bobPubkey = '8E1207F079E7912A6A2B8B2166741C449FFC8CDF4ECED0E7D954A418A6DD1AAE'

# Set up the aggregate transactions

embeddedTxs = []

# Metadata is represented as a key/value pair
metadataKey = 1234 # Key
metadataValue = 'subscribed' # value

# Create account metadata inner transaction
metadata_transaction = facade.transaction_factory.create_embedded({
		'type': 'account_metadata_transaction_v1',
		'signer_public_key': alicePubkey,
		'target_address': bobAddress, # Alice writes metadata to Bob's account
		'scoped_metadata_key': metadataKey, # Key
		'value': metadataValue, # Value
		'value_size_delta': len(metadataValue), # Length of the value
	})
# Add the transaction into the aggregate
embeddedTxs.append(metadata_transaction)

# Calculate the Merkle hash of the embeddedTxs list
merkleHash = facade.hash_embedded_transactions(embeddedTxs)

# Construct the aggregate transaction
aggregate_transaction = facade.transaction_factory.create({
		'type': 'aggregate_bonded_transaction_v2',
		'signer_public_key': alicePubkey,
		'deadline': deadline,
		'transactions_hash': merkleHash,
		'transactions': embeddedTxs
	})

# Set the fee
aggregate_transaction.fee.value = aggregate_transaction.size * 150

# Alice signs the transaction
signature = facade.sign_transaction(aliceKeypair, aggregate_transaction)

# Attach Alice's signature
tx = facade.transaction_factory.attach_signature(aggregate_transaction, signature)
print(tx)

# Calculate the hash of the aggregate transaction
txHash = facade.hash_transaction(aggregate_transaction)

# Alice creates the hash lock transaction
hashLockTx = facade.transaction_factory.create({
'type': 'hash_lock_transaction_v1',
'signer_public_key': alicePubkey,
'hash': txHash,
'deadline': deadline,
'mosaic': {'mosaic_id': 0x72C0212E67A08BCE, 'amount': 10000000}, # 10 XYM fee
'duration': 480 # 480 blocks
})

# Include the fee
hashLockTx.fee.value = hashLockTx.size * 150

# Sign the hash lock and attach the signature
hashLockSignature = facade.sign_transaction(aliceKeypair, hashLockTx)
facade.transaction_factory.attach_signature(hashLockTx, hashLockSignature)
hashLock = facade.hash_transaction(hashLockTx)
hashLockTx = facade.transaction_factory.attach_signature(hashLockTx, hashLockSignature)

# Code to check the hashlock transaction is confirmed before sending
# The aggregate (otherwise the second transaction would fail)
def is_transaction_confirmed(lock_tx_hash, node_url, node_port):
    endpoint = f"http://{node_url}:{node_port}/transactions/confirmed/{lock_tx_hash}"
    response = requests.get(endpoint)
    return response.status_code == 200


# Announce the hash lock to the network
headers = {'Content-type': 'application/json'}
conn = http.client.HTTPConnection(node_url, node_port)
conn.request("PUT", "/transactions", hashLockTx, headers)

response = conn.getresponse()
print(response.status, response.reason)

# Hash lock needs to be confirmed before sending the aggregate

confirmed = False
while not confirmed:
    print("Waiting for hash lock transaction to be confirmed...")
    time.sleep(10)  # Adjust the sleep time as needed
    confirmed = is_transaction_confirmed(hashLock, node_url, node_port)

print("Hash lock transaction confirmed. Announcing aggregate transaction...")

# Once the hash lock transaction is confirmed we submit the aggregate
# transaction to the /transactions/partial cache where it awaits Bob's
# signature

conn = http.client.HTTPConnection(node_url, node_port)
conn.request("PUT", "/transactions/partial", tx, headers)

response = conn.getresponse()
print(response.status, response.reason)

Mosaic metadata

Metadata can also be written to mosaics. In this example, Alice writes metadata to a mosaic 0x33C742BC6E6B4F03 that she created. All mosaic metadata transactions must be wrapped within and aggregate and set up as embedded transactions. As Alice created the mosaic only one signatory is required but still the transaction has to be submitted within an aggregate.

Transaction

Code

from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.CryptoTypes import PrivateKey
from binascii import unhexlify, hexlify
import http.client
import datetime

facade = SymbolFacade('testnet')

# Replace with your private key
alicePrivateKey = PrivateKey(unhexlify('4D76AA199BE4EE93E217E303174C289EA49B380B7960EA0D75E430C38A67629E'))

# Replace with node URL and connection port
node_url = 'mikun-testnet.tk'
node_port = 3000

# Set testnet network generation time and then add deadline
netStart = 1667250467
deadline = (int((datetime.datetime.today() + datetime.timedelta(hours=2)).timestamp()) - netStart) * 1000

aliceKeypair = SymbolFacade.KeyPair(alicePrivateKey)
alicePubkey = aliceKeypair.public_key
aliceAddress = facade.network.public_key_to_address(alicePubkey)

# We want to give the mosaic metadata the name "SuperAwesomeNFT1"
metadata_value = bytes(1) + 'SuperAwesomeNFT1'.encode('utf8')

# List to hold the embedded transactions (in this case only one)
embeddedTxs = []

# Create an embedded mosaic metadata transaction
mosaic_metadata = facade.transaction_factory.create_embedded({
    'type': 'mosaic_metadata_transaction_v1',
    'signer_public_key': alicePubkey,
    'target_address': aliceAddress, # Alice is writing metadata to her own account
    'scoped_metadata_key': 1234, # Specify an integer for the metadata key
    'target_mosaic_id': 0x33C742BC6E6B4F03, # Specify the target mosaic
    'value_size_delta': len(metadata_value), # Specify the metadata size
    'value': metadata_value # This is the value created above "SuperAwesomeNFT1"
})

# Append the mosaic metadata transaction to the embeddedTxs list
embeddedTxs.append(mosaic_metadata)

# Calculate the Merkle hash
merkleHash = facade.hash_embedded_transactions(embeddedTxs)

# Set up the aggregate transaction. In this case it is an aggregate complete
# as only one signature (Alice's) is needed
aggregate_transaction = facade.transaction_factory.create({
		'type': 'aggregate_complete_transaction_v2',
		'signer_public_key': alicePubkey,
		'deadline': deadline,
		'transactions_hash': merkleHash,
		'transactions': embeddedTxs
	})

# Set the fee
aggregate_transaction.fee.value = aggregate_transaction.size * 150

# Sign and attach signature
signature = facade.sign_transaction(aliceKeypair, aggregate_transaction)
tx = facade.transaction_factory.attach_signature(aggregate_transaction, signature)

# Announce the transaction to the network
headers = {'Content-type': 'application/json'}
conn = http.client.HTTPConnection(node_url, node_port)
conn.request("PUT", "/transactions", tx, headers)

response = conn.getresponse()
print(response.status, response.reason)

Namespace metadata

Metadata can be written to a namespace and as in previous examples, the owner of the namespace must approve the metadata transaction. In the example below, Alice sets up an aggregate transaction containing an embedded namespace_metadata_transaction and is the only signatory required since she is the owner of the target namespace 0xE5F00DB6D7E239B8.

Transaction

Code

from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.CryptoTypes import PrivateKey
from binascii import unhexlify, hexlify
import http.client
import datetime

facade = SymbolFacade('testnet')

# Replace with your private key
alicePrivateKey = PrivateKey(unhexlify('4D76AA199BE4EE93E217E303174C289EA49B380B7960EA0D75E430C38A67629E'))

# Replace with node URL and connection port

node_url = 'mikun-testnet.tk'
node_port = 3000

# Set testnet network generation time and then add deadline
netStart = 1667250467
deadline = (int((datetime.datetime.today() + datetime.timedelta(hours=2)).timestamp()) - netStart) * 1000

aliceKeypair = SymbolFacade.KeyPair(alicePrivateKey)
alicePubkey = aliceKeypair.public_key
aliceAddress = facade.network.public_key_to_address(alicePubkey)

# This is the value we will assign to the namespace
metadata_value = 'This is a subnamespace'

embeddedTxs = []

# Create an embedded namespace metadata transaction
namespace_metadata = facade.transaction_factory.create_embedded({
    'type': 'namespace_metadata_transaction_v1',
    'signer_public_key': alicePubkey,
    'target_address': aliceAddress,
    'scoped_metadata_key': 1000, # Metadata key (integer value)
    'target_namespace_id': 0xE5F00DB6D7E239B8, # ID of the target namespace
    'value_size_delta': len(metadata_value), # Length of metadata value
    'value': metadata_value # Value to be assigned ("This is a subnamespace")
})

# Add the metadata transaction to our embedded transaction list
embeddedTxs.append(namespace_metadata)

# Calculate the Merkle hash
merkleHash = facade.hash_embedded_transactions(embeddedTxs)

# Create an aggregate transaction. In this case, again Alice is the only
# signatory so we set up an aggregate complete transaction that only she signs.
# If writing to another account you could set up an aggregate bonded transaction
# with the target address
aggregate_transaction = facade.transaction_factory.create({
		'type': 'aggregate_complete_transaction_v2',
		'signer_public_key': alicePubkey,
		'deadline': deadline,
		'transactions_hash': merkleHash,
		'transactions': embeddedTxs
	})

# Add fee
aggregate_transaction.fee.value = aggregate_transaction.size * 150

# Sign and attach signature
signature = facade.sign_transaction(aliceKeypair, aggregate_transaction)
tx = facade.transaction_factory.attach_signature(aggregate_transaction, signature)

# Announce the transaction to the network
headers = {'Content-type': 'application/json'}
conn = http.client.HTTPConnection(node_url, node_port)
conn.request("PUT", "/transactions", tx, headers)

response = conn.getresponse()
print(response.status, response.reason)

Address restriction

Restrictions can be placed on accounts, allowing or blocking incoming or outgoing transactions from specified addresses. In the example below Alice blocks incoming transactions from Dave as he was spamming her with junk transactions.

Transaction

Code

from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.CryptoTypes import PrivateKey
from binascii import unhexlify, hexlify
import http.client
import datetime

facade = SymbolFacade('testnet')

# Replace with your private key
alicePrivateKey = PrivateKey(unhexlify('4D76AA199BE4EE93E217E303174C289EA49B380B7960EA0D75E430C38A67629E'))

# Replace with node URL and connection port

node_url = 'mikun-testnet.tk'
node_port = 3000

# Set testnet network generation time and then add deadline
netStart = 1667250467
deadline = (int((datetime.datetime.today() + datetime.timedelta(hours=2)).timestamp()) - netStart) * 1000

aliceKeypair = SymbolFacade.KeyPair(alicePrivateKey)
alicePubkey = aliceKeypair.public_key
aliceAddress = facade.network.public_key_to_address(alicePubkey)

# This is Dave's address
daveAddress = 'TBHYS2MOVX3GX5ZJ7G2BSWCDFNCGFQATU722YCY'

# Retrict all transactions sent by Dave
address_restriction = facade.transaction_factory.create({
    'type': 'account_address_restriction_transaction_v1',
    'signer_public_key': alicePubkey,
    'deadline': deadline,
		# We want to set an address restriction and block incoming transactions
		'restriction_flags': 'address block', 
		# Add Dave's address (or can add mulitple addresses)
		'restriction_additions': [daveAddress] 
})

# Add fee
address_restriction.fee.value = address_restriction.size * 150

# Sign and attach signature
signature = facade.sign_transaction(aliceKeypair, address_restriction)
tx = facade.transaction_factory.attach_signature(address_restriction, signature)

# Announce the transaction to the network
headers = {'Content-type': 'application/json'}
conn = http.client.HTTPConnection(node_url, node_port)
conn.request("PUT", "/transactions", tx, headers)

response = conn.getresponse()
print(response.status, response.reason)

Account operation restriction

Restrictions can also be set on the types of transactions an account can perform. In this example Alice blocks her account from sending any outgoing secret lock transactions but any transaction type can be specified.

Transaction

Code

from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.CryptoTypes import PrivateKey
from binascii import unhexlify, hexlify
import http.client
import datetime

facade = SymbolFacade('testnet')

# Replace with your private key
alicePrivateKey = PrivateKey(unhexlify('4D76AA199BE4EE93E217E303174C289EA49B380B7960EA0D75E430C38A67629E'))

# Replace with node URL and connection port

node_url = 'mikun-testnet.tk'
node_port = 3000

# Set testnet network generation time and then add deadline
netStart = 1667250467
deadline = (int((datetime.datetime.today() + datetime.timedelta(hours=2)).timestamp()) - netStart) * 1000

aliceKeypair = SymbolFacade.KeyPair(alicePrivateKey)
alicePubkey = aliceKeypair.public_key
aliceAddress = facade.network.public_key_to_address(alicePubkey)

# Remove the ability to send a secret lock transaction from
# Alice's account
account_op_restriction = facade.transaction_factory.create({
    'type': 'account_operation_restriction_transaction_v1',
    'signer_public_key': alicePubkey,
    'deadline': deadline,
		# Block outgoing transactions but limit to a specifc set of transactions
		'restriction_flags': 'outgoing block transaction_type',
		# Set the restriction only for secret lock transactions
		'restriction_additions': ['secret_lock'],
})

# Set the fee
account_op_restriction.fee.value = account_op_restriction.size * 150

# Sign the transaction and attach signature
signature = facade.sign_transaction(aliceKeypair, account_op_restriction)
tx = facade.transaction_factory.attach_signature(account_op_restriction, signature)

# Announce the transaction to the network
headers = {'Content-type': 'application/json'}
conn = http.client.HTTPConnection(node_url, node_port)
conn.request("PUT", "/transactions", tx, headers)

response = conn.getresponse()
print(response.status, response.reason)

Account mosaic restriction

Restrictions can also be set on mosaics so that specific mosaic IDs are blocked from being received by the account that initiates the transaction. In this example Alice blocks her account from receiving the mosaic ID 0x3C9A67E6DF91E5C7.

Transaction

Code

from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.CryptoTypes import PrivateKey
from binascii import unhexlify, hexlify
import http.client
import datetime

facade = SymbolFacade('testnet')

# Replace with your private key
alicePrivateKey = PrivateKey(unhexlify('4D76AA199BE4EE93E217E303174C289EA49B380B7960EA0D75E430C38A67629E'))

# Replace with node URL and connection port
node_url = 'mikun-testnet.tk'
node_port = 3000

# Set testnet network generation time and then add deadline
netStart = 1667250467
deadline = (int((datetime.datetime.today() + datetime.timedelta(hours=2)).timestamp()) - netStart) * 1000

aliceKeypair = SymbolFacade.KeyPair(alicePrivateKey)
alicePubkey = aliceKeypair.public_key
aliceAddress = facade.network.public_key_to_address(alicePubkey)

# Block the receipt of a specific mosaic
mosaic_restriction = facade.transaction_factory.create({
    'type': 'account_mosaic_restriction_transaction_v1',
    'signer_public_key': alicePubkey,
    'deadline': deadline,
		# Set to block by a specific set of mosaic IDs
		'restriction_flags': 'block mosaic_id',
		# Block the receipt of the mosaic ID 0x3C9A67E6DF91E5C7
		# Multiple mosaics can be listed here
		'restriction_additions': [0x3C9A67E6DF91E5C7]
})

# Set the fee
mosaic_restriction.fee.value = mosaic_restriction.size * 150

# Sign the transaction and attach the signature
signature = facade.sign_transaction(aliceKeypair, mosaic_restriction)
tx = facade.transaction_factory.attach_signature(mosaic_restriction, signature)

# Announce the transaction to the network
headers = {'Content-type': 'application/json'}
conn = http.client.HTTPConnection(node_url, node_port)
conn.request("PUT", "/transactions", tx, headers)

response = conn.getresponse()
print(response.status, response.reason)

Multisig account modification

Multi-signature accounts can be set up to provide additional security. Multiple accounts can be linked via multisig and the number of required signatories to make transactions and to modify the multisig configuration can be edited. In the example below, Emily sets up a multisig account and adds Alice, Bob and Carol as cosignatories. Now 2-of-3 cosignatories are required to sign transactions and to modify the multisig contract. Now any transactions sent from Emily’s account have to be initiated by one of the cosigners and must be signed by two of the multisig accounts. Emily’s original account can no longer sign transactions.

Multisig modification transactions require signatures from all parties and therefore the multisig modification transaction below has been wrapped into an aggregate bonded transaction which the cosignatories Alice, Bob and Carol must sign.

Transaction

Code

from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.CryptoTypes import PrivateKey
from binascii import unhexlify, hexlify
import http.client
import datetime
import time
import requests

facade = SymbolFacade('testnet')

# Replace with your private key
emilyPrivateKey = PrivateKey(unhexlify('B4653EC3164281B223B99E174412C6F75CC4F2B99D716523EE89EE9E44828CC1'))

# Replace with the recipient address
aliceAddress = 'TC3FJIODPXZTVKIM4IKTPCUWRN334ABAU74IKOI'
bobAddress = 'TDRJXPACFDXROTUCLUEO2QZUEQ4H5AOZIEUWGIY'
carolAddress = 'TACHTT5XDU55QYFML2XAEC3WD6BJ5LH56TUFXHA'


# Replace with node URL and connection port
node_url = 'mikun-testnet.tk'
node_port = 3000

# Set testnet network generation time and then add deadline
netStart = 1667250467
deadline = (int((datetime.datetime.today() + datetime.timedelta(hours=2)).timestamp()) - netStart) * 1000

emilyKeypair = SymbolFacade.KeyPair(emilyPrivateKey)
emilyPubkey = emilyKeypair.public_key
emilyAddress = facade.network.public_key_to_address(emilyPubkey)

# Set up the aggregate transactions

embeddedTxs = []

# Add Alice's new address and Bob's address as cosignatories
multisig_definition = facade.transaction_factory.create_embedded({
		'type': 'multisig_account_modification_transaction_v1',
		'signer_public_key': emilyPubkey,
		# Number of accounts needed to remove a cosignatory
		'min_removal_delta': 2, 
		# Number of accounts required to approve a transaction
		'min_approval_delta': 2, 
		# Addresses to add into the multisig contract (addresses can also be removed)
		'address_additions': [aliceAddress, bobAddress, carolAddress] 
	})

# Add the transaction into the embeddedTxs list
embeddedTxs.append(multisig_definition)

# Calculate the Merkle hash
merkleHash = facade.hash_embedded_transactions(embeddedTxs)

# Set up an aggregate bonded transaction as both Bob and Alice's new account
# must sign in order for the multisig to be set up.
aggregate_transaction = facade.transaction_factory.create({
		'type': 'aggregate_bonded_transaction_v2',
		'signer_public_key': emilyPubkey,
		'deadline': deadline,
		'transactions_hash': merkleHash,
		'transactions': embeddedTxs
	})
	
# Add the fee
aggregate_transaction.fee.value = aggregate_transaction.size * 150

# Sign and attach signature
signature = facade.sign_transaction(emilyKeypair, aggregate_transaction)
tx = facade.transaction_factory.attach_signature(aggregate_transaction, signature)

# Set up the hashlock transaction
txHash = facade.hash_transaction(aggregate_transaction)

hashLockTx = facade.transaction_factory.create({
'type': 'hash_lock_transaction_v1',
'signer_public_key': emilyPubkey,
'hash': txHash,
'deadline': deadline,
'mosaic': {'mosaic_id': 0x72C0212E67A08BCE, 'amount': 10000000}, # 10 XYM fee
'duration': 480 # Valid for 480 blocks
})

# Add fee for the hashlock transaction
hashLockTx.fee.value = hashLockTx.size * 150

# Sign the hash lock and attach the signature
hashLockSignature = facade.sign_transaction(emilyKeypair, hashLockTx)
facade.transaction_factory.attach_signature(hashLockTx, hashLockSignature)
hashLock = facade.hash_transaction(hashLockTx)
hashLockTx = facade.transaction_factory.attach_signature(hashLockTx, hashLockSignature)

# Code to check the hashlock transaction is confirmed before sending
# The aggregate (otherwise the second transaction would fail)
def is_transaction_confirmed(lock_tx_hash, node_url, node_port):
    endpoint = f"http://{node_url}:{node_port}/transactions/confirmed/{lock_tx_hash}"
    response = requests.get(endpoint)
    return response.status_code == 200


# Announce the transactions to the network
headers = {'Content-type': 'application/json'}
conn = http.client.HTTPConnection(node_url, node_port)
conn.request("PUT", "/transactions", hashLockTx, headers)

response = conn.getresponse()
print(response.status, response.reason)

# Hash lock needs to be confirmed before sending the aggregate

confirmed = False
while not confirmed:
    print("Waiting for hash lock transaction to be confirmed...")
    time.sleep(10)  # Adjust the sleep time as needed
    confirmed = is_transaction_confirmed(hashLock, node_url, node_port)

print("Hash lock transaction confirmed. Announcing aggregate transaction...")

# Aggregate bonded transaction is sent to the partial transaction cache
# awaiting signatures from Alice's new account and from Bob
conn = http.client.HTTPConnection(node_url, node_port)
conn.request("PUT", "/transactions/partial", tx, headers)

response = conn.getresponse()
print(response.status, response.reason)

Multisig transfer

Now that Emily’s account is set up as a 2-of-3 multisig she can no longer initiate transfers from her account. Instead, either Alice, Bob or Carol must announce the transaction and gather a sufficient number signatures for it to complete.

In the example below, Emily sends 5 XYM to Dave. Alice initiates the transfer from Emily’s account by setting signer_public_key to Emily’s public key in the embedded transfer transaction. Next, she wraps this in an aggregate bonded transaction and signs it. She announces the hashlock and once confirmed announces the aggregate bonded transaction. As the multisig we created only requires 2-of-3 signatures, when either Bob or Carol sign, the transaction will be successful and Dave will receive 5 XYM from Emily’s account.

Transaction

Code

from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.CryptoTypes import PrivateKey
from binascii import unhexlify, hexlify
import http.client
import datetime
import time
import requests

facade = SymbolFacade('testnet')

# Replace with your private key
alicePrivateKey = PrivateKey(unhexlify('4D76AA199BE4EE93E217E303174C289EA49B380B7960EA0D75E430C38A67629E'))

# Replace with the recipient address and multisig account public key
daveAddress = 'TBHYS2MOVX3GX5ZJ7G2BSWCDFNCGFQATU722YCY'
emilyPubkey = 'CE61CCD829C7C3295A6FF6DD73B0911C7EB6A87952AB21F100973162C813EBA2'

# Replace with node URL and connection port
node_url = 'mikun-testnet.tk'
node_port = 3000

# Set testnet network generation time and then add deadline
netStart = 1667250467
deadline = (int((datetime.datetime.today() + datetime.timedelta(hours=2)).timestamp()) - netStart) * 1000

aliceKeypair = SymbolFacade.KeyPair(alicePrivateKey)
alicePubkey = aliceKeypair.public_key
aliceAddress = facade.network.public_key_to_address(alicePubkey)

# Set up the aggregate transactions
embeddedTxs = []

# Create an embedded transfer transaction
transfer = facade.transaction_factory.create_embedded({
			'type': 'transfer_transaction_v1',
			'signer_public_key': emilyPubkey, # Funds are sent from Emily's account
			'recipient_address': daveAddress,
			'message': bytes(1) + 'Emily sent you 5 XYM'.encode('utf8'), # Message
    		'mosaics': [
    			{'mosaic_id': 0x72C0212E67A08BCE, 'amount': 5000000} # Send 5 XYM
    			],
		})

embeddedTxs.append(transfer)

# Calculate the Merkle hash
merkleHash = facade.hash_embedded_transactions(embeddedTxs)

# Set up an aggregate bonded transaction
aggregate_transaction = facade.transaction_factory.create({
		'type': 'aggregate_bonded_transaction_v2',
		'signer_public_key': alicePubkey, # Alice signs the aggregate
		'deadline': deadline,
		'transactions_hash': merkleHash,
		'transactions': embeddedTxs
	})

# Set the fee
aggregate_transaction.fee.value = aggregate_transaction.size * 150

# Alice signs the transaction
signature = facade.sign_transaction(aliceKeypair, aggregate_transaction)

# And attaches her signature
tx = facade.transaction_factory.attach_signature(aggregate_transaction, signature)

# Alice sets up the hashlock transaction
txHash = facade.hash_transaction(aggregate_transaction)

hashLockTx = facade.transaction_factory.create({
'type': 'hash_lock_transaction_v1',
'signer_public_key': alicePubkey, # Initiated by Alice
'hash': txHash,
'deadline': deadline,
'mosaic': {'mosaic_id': 0x72C0212E67A08BCE, 'amount': 10000000}, # 10 XYM deposit
'duration': 480
})

# Add the fee
hashLockTx.fee.value = hashLockTx.size * 150 

# Alice signs the hashlock transaction and then attaches her signature
hashLockSignature = facade.sign_transaction(aliceKeypair, hashLockTx)
facade.transaction_factory.attach_signature(hashLockTx, hashLockSignature)
hashLock = facade.hash_transaction(hashLockTx)
hashLockTx = facade.transaction_factory.attach_signature(hashLockTx, hashLockSignature)

# Code to check the hashlock transaction is confirmed before sending
# The aggregate (otherwise the second transaction would fail)
def is_transaction_confirmed(lock_tx_hash, node_url, node_port):
    endpoint = f"http://{node_url}:{node_port}/transactions/confirmed/{lock_tx_hash}"
    response = requests.get(endpoint)
    return response.status_code == 200


#Announce the transactions to the network

headers = {'Content-type': 'application/json'}
conn = http.client.HTTPConnection(node_url, node_port)
conn.request("PUT", "/transactions", hashLockTx, headers)

response = conn.getresponse()
print(response.status, response.reason)

# Hash lock needs to be confirmed before sending the aggregate

confirmed = False
while not confirmed:
    print("Waiting for hash lock transaction to be confirmed...")
    time.sleep(10)  # Adjust the sleep time as needed
    confirmed = is_transaction_confirmed(hashLock, node_url, node_port)

print("Hash lock transaction confirmed. Announcing aggregate transaction...")

# Once the hash lock transaction is confirmed we submit the aggregate
# transaction to the /transactions/partial cache where it awaits other
# signatures

conn = http.client.HTTPConnection(node_url, node_port)
conn.request("PUT", "/transactions/partial", tx, headers)

response = conn.getresponse()
print(response.status, response.reason)

Complex transactions

A set of different transaction types can be chained together and will not complete until all parties sign. In the example below Alice creates a new mosaic, changes the supply and the mosaic flags and then sends one mosaic to Bob and Carol who in return send Alice 1 XYM.

Note: The transactions have to be ordered in the correct way, for example, if the mosaic transfers were ordered first in the aggregate then the transaction would fail since the mosaics would not yet exist.

Transaction

Code

from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.CryptoTypes import PrivateKey
from symbolchain.symbol.IdGenerator import generate_mosaic_id
from binascii import unhexlify, hexlify
import http.client
import datetime
import requests 
import time


facade = SymbolFacade('testnet')

# Replace with your private key
alicePrivateKey = PrivateKey(unhexlify('4D76AA199BE4EE93E217E303174C289EA49B380B7960EA0D75E430C38A67629E'))

# Replace with the recipient address
bobAddress = 'TDRJXPACFDXROTUCLUEO2QZUEQ4H5AOZIEUWGIY'
carolAddress = 'TACHTT5XDU55QYFML2XAEC3WD6BJ5LH56TUFXHA'

# Replace with node URL and connection port

node_url = 'mikun-testnet.tk'
node_port = 3000

# Set testnet network generation time and then add deadline
netStart = 1667250467
deadline = (int((datetime.datetime.today() + datetime.timedelta(hours=2)).timestamp()) - netStart) * 1000

aliceKeypair = SymbolFacade.KeyPair(alicePrivateKey)
alicePubkey = aliceKeypair.public_key
aliceAddress = facade.network.public_key_to_address(alicePubkey)
bobPubkey = '8E1207F079E7912A6A2B8B2166741C449FFC8CDF4ECED0E7D954A418A6DD1AAE'
carolPubkey = '4BA3A87E4D16F9247D6414898E5F89127CB3F6D1ABBAF6418D48365E0B824FD8'

# Set up the aggregate transactions

embeddedTxs = []

# Alice creates a mosaic

nonce = 999111999 # A nonce value is required to generate the mosaic ID

# Mosaic ID is generated from Alice's address and the nonce value
mosaic_id = int(generate_mosaic_id(aliceAddress, nonce))

# Alice creates a mosaic metadata transaction
mosaic_def = facade.transaction_factory.create_embedded({
    'type': 'mosaic_definition_transaction_v1',
    'signer_public_key': alicePubkey,
		# Duration zero means that the mosaic does not expire
		# any non-zero value represents the number of blocks until expiry
		'duration': 0,
		# Set the mosaic flags
		'flags': 'supply_mutable revokable',
		'nonce': nonce,
		'id': mosaic_id,
		# Set the mosaic divisibility
		'divisibility': 0
})

embeddedTxs.append(mosaic_def)


# Alice changes mosaic supply to 1,000 units
mosaic_supply = facade.transaction_factory.create_embedded({
    'type': 'mosaic_supply_change_transaction_v1',
    'signer_public_key': alicePubkey,
	'delta': 1000,
	'action': 0x1,
	'mosaic_id': mosaic_id
})
embeddedTxs.append(mosaic_supply)


# Alice sends mosaics to Bob and Carol
aliceToBob = facade.transaction_factory.create_embedded({
			'type': 'transfer_transaction_v1',
			'signer_public_key': alicePubkey,
			'recipient_address': bobAddress,
			'message': bytes(1) + 'Alice sent you a mosaic'.encode('utf8'),
    		'mosaics': [
    			{'mosaic_id': mosaic_id, 'amount': 1}
    			]
		})
embeddedTxs.append(aliceToBob)
		
aliceToCarol = facade.transaction_factory.create_embedded({
			'type': 'transfer_transaction_v1',
			'signer_public_key': alicePubkey,
			'recipient_address': carolAddress,
			'message': bytes(1) + 'Alice sent you a mosaic'.encode('utf8'),
    		'mosaics': [
    			{'mosaic_id': mosaic_id, 'amount': 1}
    			]
		})
embeddedTxs.append(aliceToCarol)

bobToAlice = facade.transaction_factory.create_embedded({
			'type': 'transfer_transaction_v1',
			'signer_public_key': bobPubkey,
			'recipient_address': aliceAddress,
			'message': bytes(1) + 'Bob sent Alice 1 XYM'.encode('utf8'),
    		'mosaics': [
    			{'mosaic_id': 0x72C0212E67A08BCE, 'amount': 1000000}
    			]
		})
embeddedTxs.append(bobToAlice)

carolToAlice = facade.transaction_factory.create_embedded({
			'type': 'transfer_transaction_v1',
			'signer_public_key': carolPubkey,
			'recipient_address': aliceAddress,
			'message': bytes(1) + 'Carol sent Alice 1 XYM'.encode('utf8'),
    		'mosaics': [
    			{'mosaic_id': 0x72C0212E67A08BCE, 'amount': 1000000}
    			]
		})
embeddedTxs.append(carolToAlice)

merkleHash = facade.hash_embedded_transactions(embeddedTxs)

aggregate_transaction = facade.transaction_factory.create({
		'type': 'aggregate_bonded_transaction_v2',
		'signer_public_key': alicePubkey,
		'deadline': deadline,
		'transactions_hash': merkleHash,
		'transactions': embeddedTxs
	})
	
aggregate_transaction.fee.value = aggregate_transaction.size * 150

signature = facade.sign_transaction(aliceKeypair, aggregate_transaction)

tx = facade.transaction_factory.attach_signature(aggregate_transaction, signature)
print(tx)

# Set up the hashlock transaction
txHash = facade.hash_transaction(aggregate_transaction)

hashLockTx = facade.transaction_factory.create({
'type': 'hash_lock_transaction_v1',
'signer_public_key': alicePubkey,
'hash': txHash,
'deadline': deadline,
'mosaic': {'mosaic_id': 0x72C0212E67A08BCE, 'amount': 10000000},
'duration': 480
})

hashLockTx.fee.value = hashLockTx.size * 150

hashLockSignature = facade.sign_transaction(aliceKeypair, hashLockTx)
facade.transaction_factory.attach_signature(hashLockTx, hashLockSignature)
hashLock = facade.hash_transaction(hashLockTx)
hashLockTx = facade.transaction_factory.attach_signature(hashLockTx, hashLockSignature)

def is_transaction_confirmed(lock_tx_hash, node_url, node_port):
    endpoint = f"http://{node_url}:{node_port}/transactions/confirmed/{lock_tx_hash}"
    response = requests.get(endpoint)
    return response.status_code == 200


# Announce the transactions to the network

headers = {'Content-type': 'application/json'}
conn = http.client.HTTPConnection(node_url, node_port)
conn.request("PUT", "/transactions", hashLockTx, headers)

response = conn.getresponse()
print(response.status, response.reason)

# Hash lock needs to be confirmed before sending the aggregate

confirmed = False
while not confirmed:
    print("Waiting for hash lock transaction to be confirmed...")
    time.sleep(10)  # Adjust the sleep time as needed
    confirmed = is_transaction_confirmed(hashLock, node_url, node_port)

print("Hash lock transaction confirmed. Announcing aggregate transaction...")


conn = http.client.HTTPConnection(node_url, node_port)
conn.request("PUT", "/transactions/partial", tx, headers)

response = conn.getresponse()
print(response.status, response.reason)

Summary

Hopefully, this article has given you an overview of how common Symbol transactions can be written in Python. I will publish a similar guide for NEM soon 😊

Tags:
Avatar photo
NineLives
admin@symbolblog.com

I'm a Symbol and NEM enthusiast and run this blog to try to grow awareness of the platform in the English-speaking world. If you have any Symbol news you would like me to report on or you have an article that you would like to publish then please let me know!

No Comments

Post A Comment

fourteen + twenty =