Skip to main content

Writing a Template

This Step By Step guide uses an example Template as a reference and describes each component within.

The goal is to demonstrate various ways to integrate balance sheets and value transfer mechanics in a Template.

To understand more about the SDK, please check the Core Concepts section and SDK section

Step 1 - Template skeleton

We start simply by creating a empty Template which we will fill out as we go.

from luna import Template

class BSATemplate(Template):
pass

We declare an empty class BSATemplate of type Template. Everything we add under this class will be part of the Template.

Step 2 - Define roles

Now that we have our empty Template, we'll start by specifying Roles. A Role in Luna specifies one or more Participants taking part in a Contract.

In our example, we will have 2 Roles:

  • lender, the Participant who sends value.
  • borrower, the Participant who receives value.

We specify Roles as follows:

from luna import Role

lender = Role()
borrower = Role()

At runtime we will have to specify which Participant takes on which Role. Those Roles can now be used throughout the Template. Referring to a Role will implicitly refer to the Participant holding that Role in the given Contract. Note that we may have different Participants for the same Roles in different Contracts.

Step 3 - Define parameters

Now that we have defined who takes part in our Contract, we'll specify what data we require. A Parameter in Luna is used to specify data which must be provided when creating a new Contract.

We will be asking the borrower to supply their asset account through a Parameter. This can be done as follows:

from luna import Parameter
from luna.accounting.types import BalanceSheetAccount

borrower_asset_bsa = Parameter(type_=BalanceSheetAccount, description="The borrower's asset BSA")

This means that when spawning a new Contract, the borrower's asset BSA will have to be supplied.

Step 4 - Define variables

Next up we'll define a Variable. A Variable in Luna is used to persist data internally in a Contract.

Unlike a Parameter, a Variable is created by the Contract and maintained by the Contract itself. It is not supplied by an external actor.

Here we'll use it to create an Equity Balance Sheet Account (BSA) for the borrower automatically at runtime.

from luna import Parameter, BalanceSheetAccount

borrower_equity_bsa = Variable(type_=BalanceSheetAccount | None, value=None)

We use a union type BalanceSheetAccount | None, meaning the Variable can be a BalanceSheetAccount or None. The initial value is set to None. We will set the value to a new BSA using a Clause.

Step 5 - Define clauses

The data input has now been defined, we're ready to define the business logic.

Initialization

We'll start by creating a Clause to initialize our Contract at runtime. More specifically, we need to create the borrower's equity BSA (remember the Variable we set to None), as well as the lender's accounts. We'll create a Clause for each as follows:

from luna import clause, Context
from luna.accounting.types import (
BalanceSheetAccountType,
BalanceSheetUnit,
)
from luna.functions import (
create_balance_sheet_account,
create_default_payment_accounts,
)

@clause(on_init=True)
def create_lender_accounts(self, ctx: Context):
create_default_payment_accounts(
ctx=ctx, unit=BalanceSheetUnit.GBP, participant=self.lender
)

@clause(on_init=True)
def create_borrower_accounts(self, ctx: Context):
self.borrower_equity_bsa = create_balance_sheet_account(
ctx=ctx,
name="BorrowerEquityAccount",
account_type=BalanceSheetAccountType.EQUITY,
code="10002",
unit=BalanceSheetUnit.USD,
participant=self.borrower,
)

The first Clause uses the handy create_default_payment_accounts Function which sets up a basic and default accounting structure for a given Participant. This is useful if we just want to make transfers without thinking too much about the accounting logic behind.

The second Clause sets up the account in a more manual way by using create_balance_sheet_account. We need to supply more information but this also allows for more advanced accounting use cases. Notice how we specify that we're creating an equity account.

Those two Clauses specify on_init=True which means they will only run once when the Contract is created.

Business logic

Now that we have our data and initialization logic, we can implement the business logic.

We'll create two Clauses:

  • One to do a simple transfer between the two Participants. This way of doing a transfer makes a bunch of assumptions regarding the accounting structure but is easy to use.
  • A more specific transfer where we create the accounting entries manually. This is more convoluted but also more powerful and customisable.
@clause(occurrences=1)
def do_simple_transfer(self, ctx: Context):
simple_transfer(
ctx=ctx,
src_participant=self.lender,
dst_participant=self.borrower,
amount=Decimal(100),
unit=BalanceSheetUnit.GBP,
)

This Clause will run only once since we specify occurrences=1. It does a simple transfer between the lender and the borrower of GBP100.

@clause(occurrences=1)
def do_specific_transfer(self, ctx: Context):
value_datetime = datetime.now()
test_event = create_balance_sheet_event(ctx=ctx, value_datetime=value_datetime)
entry_data = [
BalanceSheetEntry(
amount=Decimal(100),
balance_sheet_account=self.borrower_asset_bsa,
balance_sheet_event=test_event,
value_datetime=value_datetime,
),
BalanceSheetEntry(
amount=Decimal(100),
balance_sheet_account=self.borrower_equity_bsa,
balance_sheet_event=test_event,
value_datetime=value_datetime,
),
]
create_balance_sheet_entries(ctx=ctx, entry_data=entry_data)

This final Clause creates actual accounting entries in order to setup the transfer. The entries must be created under a BalanceSheetEvent which serves to group entries together.

Final Template

This is our final Template:

from datetime import datetime
from decimal import Decimal
from luna import Template, Variable, Parameter, Context, clause, Role
from luna.types import BalanceSheetAccount
from luna.accounting.types import (
BalanceSheetEntry,
BalanceSheetAccountType,
BalanceSheetUnit,
)
from luna.functions import (
create_balance_sheet_event,
create_balance_sheet_entries,
create_balance_sheet_account,
create_default_payment_accounts,
simple_transfer,
)

class BSATemplate(Template):
borrower_asset_bsa = Parameter(type_=BalanceSheetAccount, description="The borrower's asset BSA")
borrower_equity_bsa = Variable(
type_=BalanceSheetAccount | None,
value=None,
)
lender = Role()
borrower = Role()

@clause(on_init=True)
def create_lender_accounts(self, ctx: Context):
create_default_payment_accounts(
ctx=ctx, unit=BalanceSheetUnit.GBP, participant=self.lender
)

@clause(on_init=True)
def create_borrower_accounts(self, ctx: Context):
self.borrower_equity_bsa = create_balance_sheet_account(
ctx=ctx,
name="BorrowerEquityAccount",
account_type=BalanceSheetAccountType.EQUITY,
code="10002",
unit=BalanceSheetUnit.USD,
participant=self.borrower,
)

@clause(occurrences=1)
def do_simple_transfer(self, ctx: Context):
simple_transfer(
ctx=ctx,
src_participant=self.lender,
dst_participant=self.borrower,
amount=Decimal(100),
unit=BalanceSheetUnit.GBP,
)

@clause(occurrences=1)
def do_specific_transfer(self, ctx: Context):
value_datetime = datetime.now()
test_event = create_balance_sheet_event(ctx=ctx, value_datetime=value_datetime)
entry_data = [
BalanceSheetEntry(
amount=Decimal(100),
balance_sheet_account=self.borrower_asset_bsa,
balance_sheet_event=test_event,
value_datetime=value_datetime,
),
BalanceSheetEntry(
amount=Decimal(100),
balance_sheet_account=self.borrower_equity_bsa,
balance_sheet_event=test_event,
value_datetime=value_datetime,
),
]
create_balance_sheet_entries(ctx=ctx, entry_data=entry_data)

Balance Sheet Accounts

This section provides more details on how accounts and payments work.

Creating accounts

There are two ways for the Template to create Balance Sheet Accounts:

  1. create_default_payment_accounts

    This Function will create two default accounts for the provided Participant. It will create an ASSET and EQUITY account with codes 10001 and 10002 respectively. These are considered the default accounts required to make or receive simple payments. If these accounts already exist no actions will be taken.

  2. create_balance_sheet_account

    This Function allows for the creation of a single Balance Sheet Account. It's more verbose than create_default_payment_accounts and allows the author to specify the finer details. This Function will also create a Balance Sheet if none already exists for given Participant.

Injecting accounts

There are two ways to inject a Balance Sheet Account into a Contract:

  1. Parameter

    A Balance Sheet Account can be defined as a Parameter with type BalanceSheetAccount. The ID of required Balance Sheet Account can then be passed in as a Parameter during Contract creation.

  2. Variable

    A Balance Sheet Account can be defined as a Variable. The default value will be None which can then be redefined within a Clause during the runtime of a Contract as desired.

Simple Payments

The simple_transfer Function represents the easiest way to define a simple transfer between two Participants. This Function will expect each Participant to have the default accounts ASSET and EQUITY with codes 10001 and 10002 to exist, as per create_default_payment_accounts function. These accounts will then be used to process the transfer. A single Balance Sheet Event will be create and four Balance Sheet Entries will be created within that Event. These four Entries will be:

  1. src_participant | ASSET | -100
  2. src_participant | EQUITY | -100
  3. dst_participant | ASSET | 100
  4. dst_participant | EQUITY | 100

Specific Accounting Event And Entry Creation

There are two SDK functions that allow for the fine grained creation of Balance Sheet Events and Entries. These are:

  1. create_balance_sheet_event

    This Function creates a single event using the arguments provided.

  2. create_balance_sheet_entries

    This Function will create Balance Sheet Entries in Bulk. These Entries will be validated by the calculation ASSET = EQUITY + LIABILITY. So each Entry against an ASSET account will need to be Balanced by corresponding entries to EQUITY and/or LIABILITY accounts. This calculation will group Entries by Participant, Event, Unit and Time.