Tallybox Wallet - Transaction Tutorial

by shahiN Noursalehi

You are here: Home / Toturials / Tallybox Wallet Transaction - Pseudo Edition

Note: This tutorial is structured to be AI-Friendly. An AI can generate code in any programming language based on the content of this URL, thanks to its clear steps, pseudo-code, and examples.

Preparing a Transaction with Tallybox Wallet

This tutorial guides you through loading a Tallybox Wallet, checking token balances, and preparing and broadcasting a transaction on a Directed Acyclic Graph (DAG) network. Follow the steps below to implement your own transaction preparation code.

References:
Directed Acyclic Graph (Wikipedia)
Cryptocurrency Wallet (Wikipedia)

Step 1: Load Wallet and Decrypt Private Key

Begin by loading the wallet's XML file, which contains the wallet name, compressed public key, encrypted private key, and wallet address. Prompt the user to select an XML file and enter a local password for decryption. Validate the XML structure and decrypt the private key using the password using a special AES-256-CBC algorithm with custom padding, described below.

Special AES-256-CBC with Custom Padding

Tallybox uses a unique AES-256-CBC decryption algorithm with custom padding to retrieve the private key. The algorithm derives the key, IV, and salt from a 64-character hex secret key (SHA-256 of wallet_name~password~wallet_address). The plaintext is expected to have random padding around it. The configuration is:

  • Secret Key: A 64-character hex string (32 bytes), split into:
    • Password: First 32 hex chars (16 bytes, ASCII-encoded).
    • IV: Next 16 hex chars (8 bytes, ASCII-encoded, 16 bytes as bytes).
    • Salt: Last 16 hex chars (8 bytes, ASCII-encoded).
  • Key Derivation: PBKDF2 with SHA-256, 3 iterations, 32-byte output, using password and salt.
  • Custom Padding: Expected format is random string (0–99 chars, a-zA-Z) + '|' + plaintext (64-char hex private key) + '|' + random string (0–99 chars, a-zA-Z), followed by PKCS#7 padding.
  • Input: Base64-encoded ciphertext (no IV prepended).

Pseudo-Code: AES-256-CBC Decrypt with Custom Padding

FUNCTION aes256_cbc_decrypt_custom(base64_ciphertext, secret):
    IF length(secret) < 64 OR NOT is_hex(secret) THEN
        THROW "Secret must be a 64-char hex string"
    END IF
    ciphertext = base64_decode(base64_ciphertext.replace('\n', ''))  // Strip newlines
    password = secret[0:32]  // First 32 hex chars (ASCII)
    iv = secret[32:48]       // Next 16 hex chars (ASCII)
    salt = secret[48:64]     // Last 16 hex chars (ASCII)
    key = PBKDF2_HMAC_SHA256(password, salt, iterations=3, output_bytes=32)
    cipher = initialize_aes256_cbc_decrypt(key, iv)
    padded_data = cipher.decrypt(ciphertext)
    padding_length = padded_data[-1]
    IF padding_length > 16 OR padding_length == 0 THEN
        THROW "Invalid PKCS#7 padding"
    END IF
    padded_data = padded_data[:-padding_length]  // Remove PKCS#7 padding
    padded_text = padded_data.decode('utf-8')
    parts = padded_text.split('|')
    IF length(parts) != 3 THEN
        THROW "Invalid padding format: expected 'left|data|right'"
    END IF
    plaintext = parts[1]
    IF NOT is_hex(plaintext) OR length(plaintext) != 64 THEN
        THROW "Decrypted private key must be a 64-char hex string"
    END IF
    RETURN plaintext
END FUNCTION
                    


Sample Pseudo-Code: Load Wallet

FUNCTION load_wallet(file, password, protocol, graph):
    TRY:
        xml_content = READ_FILE(file)
        xml_doc = PARSE_XML(xml_content)
        wallet_name = xml_doc.GET_TAG("wallet_name")
        public_key_b58 = xml_doc.GET_TAG("public_key_b58_compressed")
        private_key_aes_b64 = xml_doc.GET_TAG("private_key_aes_b64")
        wallet_address = xml_doc.GET_TAG("wallet_address")

        IF NOT (wallet_name AND public_key_b58 AND private_key_aes_b64 AND wallet_address):
            RETURN ERROR("Invalid XML format")

        key_components = wallet_name + "~" + password + "~" + wallet_address
        local_key = SHA256(key_components)
        private_key = aes256_cbc_decrypt_custom(private_key_aes_b64, local_key)

        ec = NEW_ELLIPTIC_CURVE("p256")
        key_pair = ec.KEY_FROM_PRIVATE(private_key, "hex")
        public_key = key_pair.GET_PUBLIC()
        x_hex = public_key.GET_X().TO_HEX().PAD_START(64, "0")
        y_hex = public_key.GET_Y().TO_HEX().PAD_START(64, "0")
        y_parity = public_key.GET_Y().MOD(2)
        suffix = IF y_parity == 0 THEN "2" ELSE "1"
        compressed_key = x_hex + "*" + suffix

        result = CREATE_RECORD()
        result.SET_FIELD("key_pair", key_pair)
        result.SET_FIELD("compressed_key", compressed_key)
        result.SET_FIELD("public_key_b58", public_key_b58)
        result.SET_FIELD("wallet_address", wallet_address)
        result.SET_FIELD("wallet_name", wallet_name)
        RETURN result
    CATCH error:
        RETURN ERROR("Decryption failed or invalid file: " + error)
END FUNCTION
                

Notice: XML File Structure
The XML file must include , , , and . Ensure the password matches the one used to encrypt the private key, or decryption will fail.


Example Process:

Step 1 - Loaded Wallet Name:
shahin

Step 2 - Extracted Public Key (Base58 Compressed):
3W8iLTuAjbf9d1ih4RCi7Aat6keKxs72SNynZ1A269MY*1

Step 3 - Extracted Wallet Address:
boxBa01d317e6c57Lt8jmf3GNWCJc2K5bU7aAS5gqSbDYnY5UVshnJVHEJP

Step 4 - Key Components (pre-SHA-256):
shahin~11~boxBa01d317e6c57Lt8jmf3GNWCJc2K5bU7aAS5gqSbDYnY5UVshnJVHEJP

Step 5 - Local Key (SHA-256):
7e9b4699f7d3eea878f51a9d4e238922116cccc558b15f2db257d92a9779becc

Step 6 - Encrypted Private Key (Base64):
Lu8xEpnACOtVrmZxRt7I2m7Zc5pZI8sxIGhXV6FqwN6N2VG8yt4jof5c0Xo5slF+VyOLORhls2auG27Xmjd20Z/9YiBZ2E5ePFsQV7ZkeqKFZa+k5IrOHGjDcyJtiJt22zAzOaSvyTFFGRHwDGI1FCJ5BppBvYkoGhro9MxHp3MVS7eW2N3z2NEQV2Gxm+IErv0ExkxfesyXqw/kTwk2qHBuTUtKRgkOY8RCAPPkNglOjfo4ct+YCdkzoIS/Ww6xatWmAUA6bhtxK2q3vZly1iteNAa+Uz0Pyn166f7Y90cCGJPINc5x7WGcolqPeWf5

Step 7 - Decrypted Private Key (hex):
7a8d26a42f45ecc68bc9d9ad2de9e4868429de2b185ab7e023f93845c58c4fa1

Step 8 - Status:
Wallet loaded successfully!

References:
XML Specification (W3C)
AES Encryption (Wikipedia)
SHA-256 Algorithm (Wikipedia)
SEC 2: Recommended Elliptic Curve Domain Parameters (secp256r1)

Step 2: Choose Action (History or Send)

After successfully loading the wallet, prompt the user to either view their token balances (history) or prepare and sign a transaction to send tokens. Ensure the wallet is loaded before proceeding to either action.

Sample Pseudo-Code: User Choice

FUNCTION prompt_user_action(wallet_state):
    IF NOT wallet_state.wallet_address:
        RETURN ERROR("No wallet loaded. Load a wallet first.")

    action = PROMPT_USER("Select action: [1] View History, [2] Send Transaction")
    IF action == "1":
        CALL fetch_balances(wallet_state)
    ELSE IF action == "2":
        CALL prepare_transaction(wallet_state)
    ELSE
        RETURN ERROR("Invalid action selected")
END FUNCTION
                

Warning: Wallet Validation
Always verify that a wallet is loaded (i.e., wallet_address is not empty) before allowing history or transaction actions. Attempting to proceed without a loaded wallet will result in errors.


Example Process:

No example output provided for this step.

References:
User Interface Design (Wikipedia)
Control Flow in Programming (Wikipedia)

Step 3: Fetch Token Balances

To view balances, send a POST request to the DAG node's API endpoint (/archive.asmx/order_history) with the wallet address and graph details. Parse the response to extract balances for supported tokens (e.g., 2PN, 2ZR, TLH).

Sample Pseudo-Code: Fetch Balances

FUNCTION fetch_balances(wallet_state):
    url = wallet_state.protocol + "://" + wallet_state.graph + "/archive.asmx/order_history"
    post_data = "app_name=tallybox&app_version=2.0&in_graph=" + ENCODE_URI_COMPONENT(wallet_state.graph) + "&wallet_address=" + ENCODE_URI_COMPONENT(wallet_state.wallet_address)
    
    TRY:
        response = POST_REQUEST(url, post_data)
        parts = response.SPLIT("~")
        balances = CREATE_RECORD()
        balances.SET_FIELD("2PN", "0.00000000")
        balances.SET_FIELD("2ZR", "0.00000000")
        balances.SET_FIELD("TLH", "0.00000000")
        FOR i = 6 TO parts.LENGTH - 2 STEP 2
            token = parts[i]
            amount = parts[i + 1]
            IF token IN ["2PN", "2ZR", "TLH"] AND amount AND IS_NUMBER(amount):
                balances.SET_FIELD(token, FORMAT_DECIMAL(amount, 8))
            END IF
        END FOR
        RETURN balances
    CATCH error:
        RETURN ERROR("Failed to fetch balances: " + error)
END FUNCTION
                

Info: API Endpoint
The default graph is tallybox.mixoftix.net, and the protocol is typically https. The API returns a tilde-separated string where token balances appear as pairs (e.g., 2PN~123.456789).


Example Process:

Step 0 - Current Date-Time:
2025-04-17 04:30:08

Step 1 - Constructed POST Data:
app_name=tallybox&app_version=2.0&in_graph=tallybox.mixoftix.net&wallet_address=boxBa01d317e6c57Lt8jmf3GNWCJc2K5bU7aAS5gqSbDYnY5UVshnJVHEJP

Step 2 - Server Endpoint URL:
https://tallybox.mixoftix.net/archive.asmx/order_history

Step 3 - Server Response:
tallybox.mixoftix.net~boxBa01d317e6c57Lt8jmf3GNWCJc2K5bU7aAS5gqSbDYnY5UVshnJVHEJP~msg~null~adv~null~null

Step 4 - Status:
History fetched successfully!

References:
HTTP POST Request (Wikipedia)
URI Encoding (Wikipedia)
API Concepts (Wikipedia)

Step 4: Prepare Transaction

Prompt the user for the target wallet address, token name (e.g., 2PN, 2ZR, TLH), amount, and an optional order ID. Validate the target wallet address to ensure it conforms to Tallybox's format. Generate an offline signature using the private key with the ECDSA (Elliptic Curve Digital Signature Algorithm) on the secp256r1 curve, following RFC 6979 for deterministic signature generation, to create a signed transaction ready for broadcasting.

Sample Pseudo-Code: Prepare Transaction

FUNCTION prepare_transaction(wallet_state):
    target_address = PROMPT_USER("Enter target wallet address")
    token = PROMPT_USER("Select token: [2PN, 2ZR, TLH]")
    amount = PROMPT_USER("Enter amount (positive number)")
    order_id = PROMPT_USER("Enter optional order ID", optional=true)
    graph = wallet_state.graph
    utc_unix = CURRENT_TIMESTAMP_SECONDS()

    IF NOT VALIDATE_WALLET_ADDRESS(target_address):
        RETURN ERROR("Invalid target wallet address")
    IF NOT (token IN ["2PN", "2ZR", "TLH"]):
        RETURN ERROR("Invalid token")
    IF NOT IS_NUMBER(amount) OR amount <= 0:
        RETURN ERROR("Invalid amount")

    transaction_data = ENCODE_URI_COMPONENT(graph) + "~" + ENCODE_URI_COMPONENT(graph) + "~" + 
                       ENCODE_URI_COMPONENT(wallet_state.wallet_address) + "~" + 
                       ENCODE_URI_COMPONENT(target_address) + "~" + ENCODE_URI_COMPONENT(token) + "~" + 
                       FORMAT_DECIMAL(amount, 8) + "~" + ENCODE_URI_COMPONENT(order_id) + "~" + utc_unix
    msg_hash = SHA256(transaction_data)
    ec = NEW_ELLIPTIC_CURVE("p256")
    signature = wallet_state.key_pair.SIGN_ECDSA(msg_hash, RFC6979) // Produces (r, s) pair, deterministic per RFC 6979
    sig_der = signature.TO_DER("hex")                              // Encode (r, s) as DER, hex string
    sig_base64 = BASE64_ENCODE(HEX_TO_BYTES(sig_der))             // Convert hex to bytes, encode as Base64
    broadcast_data = CONCATENATE_WITH_SEPARATOR(
        "tallybox",
        "parcel_of_transaction",
        "graph_from", ENCODE_URI_COMPONENT(graph),
        "graph_to", ENCODE_URI_COMPONENT(graph),
        "wallet_from", ENCODE_URI_COMPONENT(wallet_state.wallet_address),
        "wallet_to", ENCODE_URI_COMPONENT(target_address),
        "order_currency", ENCODE_URI_COMPONENT(token),
        "order_amount", FORMAT_DECIMAL(amount, 8),
        "order_id", ENCODE_URI_COMPONENT(order_id),
        "order_utc_unix", utc_unix,
        "the_sign", ENCODE_URI_COMPONENT(sig_base64),
        "publicKey_xy_compressed", ENCODE_URI_COMPONENT(wallet_state.public_key_b58),
        separator="~"
    )

    RETURN broadcast_data
END FUNCTION
				

Sample Pseudo-Code: Validate Wallet Address

FUNCTION VALIDATE_WALLET_ADDRESS(address):
    // Check for null/empty or insufficient length
    IF address IS NULL OR address.LENGTH < 40:
        RETURN FALSE
    END IF
    
    // Check prefix
    IF NOT address.STARTS_WITH("box"):
        RETURN FALSE
    END IF
    
    // Check curve character (4th character)
    curve_char = address[3]
    IF curve_char NOT IN ["A", "B", "C"]:
        RETURN FALSE
    END IF
    
    // Checksum validation
    checksum_md5 = address.SUBSTRING(4, 15) // Characters 4 to 14 (11 characters)
    base58_part = address.SUBSTRING(15)     // From character 15 to end
    computed_md5 = MD5(base58_part.encode()).SUBSTRING(0, 11)
    
    IF checksum_md5 == computed_md5:
        RETURN TRUE
    END IF
    
    RETURN FALSE
END FUNCTION
				

Success: Transaction Signing
A successful offline signature produces a tilde-separated string containing the transaction details, signature, and public key, ready for broadcasting to the DAG node.

Note: The ECDSA signing process must comply with RFC 6979 to ensure deterministic signatures, enhancing security and reproducibility.


Example Process:

Step 1 - Transaction Data:
tallybox.mixoftix.net~tallybox.mixoftix.net~boxBa01d317e6c57Lt8jmf3GNWCJc2K5bU7aAS5gqSbDYnY5UVshnJVHEJP~boxB8cbfbaf9c1c4Pfyp4JMNThyUhNh4PPdD7hiXBeTJgQtU7npfZSMEH68~2ZR~34.54000000~~1744889500

Step 2 - SHA-256 Hash:
d838bdb3dfe7f3a8b07931c99f90080d46c0757fbe994a9863a6135ef5645006

Step 3 - Signature (Base64):
MEUCIQDDK5ALX00hImyHHL/dpKGDMnYLmLG6hkz9Q4vmUsydFQIgLd88G7AUp2IbQMwyNAIAlrSqYlh8zCzq7jOXj+JzmDM=

Step 4 - Broadcast Result:
tallybox~parcel_of_transaction~graph_from~tallybox.mixoftix.net~graph_to~tallybox.mixoftix.net~wallet_from~boxBa01d317e6c57Lt8jmf3GNWCJc2K5bU7aAS5gqSbDYnY5UVshnJVHEJP~wallet_to~boxB8cbfbaf9c1c4Pfyp4JMNThyUhNh4PPdD7hiXBeTJgQtU7npfZSMEH68~order_currency~2ZR~order_amount~34.54000000~order_id~~order_utc_unix~1744889500~the_sign~MEUCIQDDK5ALX00hImyHHL%2FdpKGDMnYLmLG6hkz9Q4vmUsydFQIgLd88G7AUp2IbQMwyNAIAlrSqYlh8zCzq7jOXj%2BJzmDM%3D~publicKey_xy_compressed~3W8iLTuAjbf9d1ih4RCi7Aat6keKxs72SNynZ1A269MY*1


Example Output:

References:
ECDSA Algorithm (Wikipedia)
RFC 6979: Deterministic Usage of DSA and ECDSA
SHA-256 Algorithm (Wikipedia)
Base64 Encoding (Wikipedia)
MD5 Algorithm (Wikipedia)
URI Encoding (Wikipedia)

Step 5: Broadcast Transaction

Broadcast the signed transaction to the DAG node's API endpoint (/broadcast.asmx/order_accept) using a POST request. Handle the response to confirm whether the transaction was accepted or rejected by the network.

Sample Pseudo-Code: Broadcast Transaction

FUNCTION broadcast_transaction(wallet_state, broadcast_data):
    url = wallet_state.protocol + "://" + wallet_state.graph + "/broadcast.asmx/order_accept"
    post_data = "app_name=tallybox&app_version=2.0&order_csv=" + ENCODE_URI_COMPONENT(broadcast_data.REPLACE("\n", "").REPLACE("\r", ""))
    
    TRY:
        response = POST_REQUEST(url, post_data)
        IF response.STARTS_WITH("submitted~200~"):
            RETURN SUCCESS("Transaction broadcast successfully: " + response)
        ELSE
            RETURN ERROR("Broadcast failed: " + response)
    CATCH error:
        RETURN ERROR("Failed to broadcast transaction: " + error)
END FUNCTION
                

Success: Transaction Broadcasting
A successful broadcast returns a response starting with submitted~200~, indicating the transaction was accepted by the DAG node. The response includes a transaction ID for tracking.


Example Process:

Step 1 - Constructed POST Data:
app_name=tallybox&app_version=2.0&order_csv=tallybox~parcel_of_transaction~graph_from~tallybox.mixoftix.net~graph_to~tallybox.mixoftix.net~wallet_from~boxBa01d317e6c57Lt8jmf3GNWCJc2K5bU7aAS5gqSbDYnY5UVshnJVHEJP~wallet_to~boxB8cbfbaf9c1c4Pfyp4JMNThyUhNh4PPdD7hiXBeTJgQtU7npfZSMEH68~order_currency~2ZR~order_amount~34.54000000~order_id~~order_utc_unix~1744889500~the_sign~MEUCIQDDK5ALX00hImyHHL%2FdpKGDMnYLmLG6hkz9Q4vmUsydFQIgLd88G7AUp2IbQMwyNAIAlrSqYlh8zCzq7jOXj%2BJzmDM%3D~publicKey_xy_compressed~3W8iLTuAjbf9d1ih4RCi7Aat6keKxs72SNynZ1A269MY*1

Step 2 - Server Endpoint URL:
https://tallybox.mixoftix.net/broadcast.asmx/order_accept

Step 3 - Server Response:
error~203~no enough fee~order_amount

Step 4 - Status:
Broadcast failed!

References:
HTTP POST Request (Wikipedia)
URI Encoding (Wikipedia)
API Concepts (Wikipedia)

Acknowledgments

Special thanks to Grok, for its invaluable assistance in creating this TallyBox wallet transaction tutorial.