Overview

Each ADAPT packet’s state transition API (packet interface) consists of special functions called transactions. Transactions can be invoked from outside of a packet that defines them. For node-level packets, transaction invocation can be the result of a user action or a network message. For nested packets, transactions are invoked by the outer packet using the transact statement.

The trn statement defines each transaction that comprises a packet’s interface. The statement specifies:

using this syntax:

trn [readonly] <transaction_name> <argument_name> [: <argument_type>]
{
    // transaction body
}

This syntax can also be used to define a transaction:

trn [readonly] <transaction_name> <argument_name> [: <argument_type>] = (expression).

Transaction Name

The name is used to identify the transaction. External calls to the transaction require a full path to it, including the parent namespace. For example:

application xyz { 
    module abc { 
        trn some_state_transition ... 
    }
}

is externally known by the name ::xyz::abc::some_state_transition.

All externally-known transaction names always start with ::

Readonly Option

This option specifies that the transaction is executed as readonly, which means that the new packet state gets discarded. A typical use-case for this option is to make get-request to the packet, meaning that the request is guaranteed not to alter the packet state.

An example of a readonly transaction:

trn readonly generate_id _
{
    return _new_id $id_salt.
}

Transaction Argument

Each transaction takes exactly one argument. You can specify the type of the argument and use pattern destructuring for record types in order to simulate multi-argument transactions.

For example:

trn transaction_multiple_arguments
_:($arg1 -> arg1: arg1_type, $arg2 -> arg2: arg2_type)
{
    _print arg1 arg2.
}

To validate data passed into the transaction from outside of the current ADAPT node, the argument passed to the transaction is automatically checked using a safe cast. If the cast fails, the transaction results in an error.

Transaction Body

The transaction body contains the code to execute when the transaction is invoked.

Like functions, transactions can return a value (using the MUFL return statement). The type of the value is any. Transactions may return any MUFL values that are composed of dictionaries, blinded dictionaries, and/or the str, int, NIL, FALSE, TRUE, hash and crypto basic MUFL types.

Function values, safe-references, and references cannot be returned from transactions.

Transaction Output

The value returned by the transaction body is the transaction output. For node-level packets, the ADAPT environment expects the output in a specific return format. For nested packets, the value returned is up to the developer.

ADAPT Wrapper Return Values

For node-level packets, the transaction output comprises instructions to the ADAPT wrapper, for example, “send a message to another packet” or “initiate a timeout callback”. Your transaction definitions must follow the ADAPT transaction return convention and include a properly-formed return value.

The transaction return convention is defined in the transaction.mm library, which resides at /mufl/transactions/transaction.mm in the ADAPT Docker development kit and at ./transactions/transaction.mm at the main git repository.

Upon completion of a transaction, your MUFL code must return either success or failure. For convenience and convention consistency, the transaction.mm library contains transaction::success and transaction::failure functions to use as the return value.

Success Result

A success result indicates that the transaction completed successfully. A properly-formed success result uses this syntax:

return transaction::success [<actions>].

where <actions> is a list of MUFL data structures that define the actions to be performed after the transaction is executed. The ADAPT wrapper then differentiates between these structures and makes the consequent corresponding calls.

When further actions are not required, return an empty list [].

For example, successful evaluation of a transaction might result in sending requests to different nodes to execute other transactions.

return transaction::success [
        transaction::action::send destination_packet_id destination_transaction,
        transaction::action::send destination_packet_id another_transaction,
        transaction::action::send another_destination another_transaction
    ].

This example uses the transaction.mm library to send actions to other nodes as a result of successful transaction evaluation.

The code returns a success result and three new transaction requests as a result of a successful transaction execution. Two of the requests are addressed to the packet with ID destination_packet_id and the third request to another packet with ID another_destination. The ADAPT wrapper receives the result and initiates the transactions.

Available actions

Action name Arguments Description
send packet_id, transaction Send a transaction to another packet.
return_data data (any) Return data to the ADAPT wrapper. This action is useful to create a custom data driver interface between the ADAPT packet and the ADAPT wrapper.
timeout continuation_id, seconds After timeout elapses, the ::continuation::continue_transaction with the provided id is executed.
verify continuation_id, data (any) Request a web user to verify some action. If verified, the ::continuation::continue_transaction with the provided id is executed.

The set of actions may be expanded in the future.

Please, note that the different types of actions can be combined in a single action list:

return transaction::success [
        transaction::action::send destination_packet_id destination_transaction,
        transaction::action::return_data $very_important_data,
        transaction::action::timeout continuation_id 10
    ].

Failure Result

A failure result indicates that the transaction completed, but without an acceptable result. A properly-formed failure result uses this syntax:

return transaction::failure <message>.

where <message> is a string indicating the reason for returning a failure result.

For example,

trn my_trn arg
{
    if arg == NIL then
        return transaction::failure "The argument is NIL".
    end
    return transaction::success [].
}

Transaction execution can be terminated with an abort operation, using either the _abort_transaction primitive or the abort statement. When a transaction is aborted, the new state of the packet is not created.

Nested Packet Return Values

When implementing nested packets, you can develop your own convention for the transaction result. The transact statement, which is used to invoke nested packet transactions, can optionally include a result clause.

For more information, refer to Transaction Return Values for nested packets.

Examples

This example builds and returns a list of transactions with different types of actions:

application multiple_actions loads library continuation uses transactions
{
    trn transaction_multiple_actions destination_packet_id: global_id
    {
        actions is ?transaction::success/reducer = [].

        // add timeouts
        sc (1..10) -- (->i) {
            fn continuation_func (cnt: int)
            {
                _print "Timeout elapsed: " cnt "\n".
                return ::transaction::success []. // return empty list of actions from continuation because this function is executed in terms of ::continuation::continue_transaction. 
            }
            continuation_id = continuation::add_continuation (continuation_func i).
    
            actions (_count actions|) -> transaction::action::timeout continuation_id 10. // after 10 seconds the function above is executed
        }
    
        // add user verification request (only on Web)
        sc (1..10) -- (->i) {
            fn continuation_func (cnt: int)
            {
                _print "User confirmed: " cnt "\n".
                return ::transaction::success []. // return empty list of actions from continuation because this function is executed in terms of ::continuation::continue_transaction. 
            }
            continuation_id = continuation::add_continuation (continuation_func i).
    
            actions (_count actions|) -> transaction::action::verify continuation_id ("Please confirm that " + (_str i) + " == " + (_str i)). 
        }
    
        // return data
        actions (_count actions|) -> transaction::action::return_data "We are returning any data we want. Even, for example, dictionaries.".
        actions (_count actions|) -> transaction::action::return_data ($this -> "is", $a -> "dictionary", $value -> 100).
    
        // send
        actions (_count actions|) -> transaction::action::send destination_packet_id ($name -> "::library::transaction_name", $targ -> "transaction arguments").
    
        return transaction::success actions.
    }
}

For more information, refer to the source code of the library.

Querying Transaction Information

The code that executes a transaction can access information related to that transaction, such as timestamp, origin, sender, and more. To retrieve the information, use the current_transaction_info.mm library.

The library resides at /mufl/mufl_stdlib/current_transaction_info.mm in the ADAPT Docker development kit and at ./mufl_stdlib/current_transaction_info.mm in the main git repository. For information about the possible queries, and how to invoke them, refer to the source code.

In some situations, we need to check where the transaction came from.
current_transaction_info::validate_origin takes a set of allowed origins and compares the actual origin against it.

In general, the origin of a transaction is one of these types:

  • External - sent from another packet.
  • Local - executed in a nested packet by the host.
  • User - sent by the web application containing an ADAPT node in response to an action taken by the application’s user.

current_transaction_info::is_signed() returns true if the transaction is signed, otherwise false. current_transaction_info::is_encrypted() returns true if the transaction is encrypted, otherwise false. current_transaction_info::get_transaction_time() returns the timestamp of the transaction.

Examples

Here are some examples using current_transaction_info:

application transaction_origin_examples loads library current_transaction_info uses transactions
{
trn transaction_user _
{
    current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
}

trn transaction_external_or_internal _
{
    current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::local, transaction::envelope::origin::external,).
}

trn transaction_user_or_internal _
{
    current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::local, transaction::envelope::origin::user,).
}

trn transaction_get_external_envelope _
{
    envelope = current_transaction_info::get_external_envelope_or_abort ().
}

trn signed_and_encrypted_transaction _
{
    abort "Transaction is expected to be encrypted!" when not current_transaction_info::is_encrypted().
    abort "Transaction is expected to be singed!" when not current_transaction_info::is_signed().
}

trn retrieve_transaction_timestamp _
{
    timestamp = current_transaction_info::get_transaction_time().
}

}

For more information about envelopes and origins, refer to the source code.