Skip to main content

Marlowe embedded in JavaScript

Marlowe is written as a Haskell data type, and thus it is straightforward to describe Marlowe smart contracts using Haskell. But since Marlowe contracts are "just" a form of data, we can equally well represent them in other languages that support serialization to JSON or CBOR.

Here we describe a library written in TypeScript that can be used to generate Marlowe smart contracts from TypeScript or JavaScript in a similar way to how one would by using Haskell. If you are not familiar with TypeScript, you can also use the API as if it was written in JavaScript since TypeScript is a superset of JavaScript.

You can try the library online in the Marlowe Playground by selecting Start in JavaScript on the home page, or by opening one of the JavaScript examples.

We begin this section by explaining the embedding, then explain a couple of particular points about embedding in JavaScript, and finally present an example of a full contract described using the JS embedding.

Using the JS Editor in the Marlowe Playground

The library implementation itself is straightforward, and you can find all of its source code here:

It is based on the principle that for each Haskell type there is a corresponding TypeScript type, and corresponding to each constructor there is a constant definition.

import {
Address, Role, Account, Party, ada, AvailableMoney, Constant, ConstantParam,
NegValue, AddValue, SubValue, MulValue, DivValue, ChoiceValue, TimeIntervalStart,
TimeIntervalEnd, UseValue, Cond, AndObs, OrObs, NotObs, ChoseSomething,
ValueGE, ValueGT, ValueLT, ValueLE, ValueEQ, TrueObs, FalseObs, Deposit,
Choice, Notify, Close, Pay, If, When, Let, Assert, SomeNumber, AccountId,
ChoiceId, Token, ValueId, Value, EValue, Observation, Bound, Action, Payee,
Case, Timeout, ETimeout, TimeParam, Contract
} from 'marlowe-js';

The JavaScript/TypeScript library provides constant definitions for Marlowe constructs that have no arguments, as is the case of TimeIntervalStart:

const TimeIntervalStart: Value

or the Close contract:

const Close: Contract

Constructs that have arguments are represented as functions, as in the case of AvailableMoney:

const AvailableMoney: (token: Token, accountId: Party) => Value

You can see the type declarations for each of the constructs and types by hovering over the identifier in the import declaration at the start of the file appearing in the editor of the JS Editor tab. Both the import declaration and the function declaration are grayed-out to signal that they must not be modified, the code that generates the contract must be written inside the body of the function provided, and the resulting contract must be returned as result of the function (using the return instruction).

Internally, the functions and constants of the JavaScript/TypeScript library return a JSON representation of the Marlowe constructs. For example, the function AvailableMoney is defined as follows:

const AvailableMoney =
function (token: Token, accountId: Party) => Value {
return { "amount_of_token": token,
"in_account": accountId };
};

When you click the Compile button in the JS editor of the Marlowe Playground, the code in the body of the tab is executed, and the JSON object returned by the function during the execution is parsed into an actual Marlowe contract. Once that is successful it is possible to Send to Simulator; how this works is described in the next section.

In principle you could write JavaScript code that produces the Marlowe's JSON representation directly, but you should not have to worry about JSON at all when using the JS library.

When you use the JS Marlowe library, and your use of the functions and constants of the library type-checks, then the result of your code should produce a valid JSON representation of a Marlowe contract, so we ensure safety of contract generation through the type system of TypeScript.

The SomeNumber type

There is one important type that is not present in the Haskell definition of Marlowe, we have called that type SomeNumber, and it is defined as follows:

type SomeNumber = string | number | bigint

The reason we have this type is that the native type for numbers in JavaScript and TypeScript loses precision when used with large integer numbers. This is because its implementation relies on floating point numbers.

The following expression is true in JavaScript:

9007199254740992 == 9007199254740993

This can be problematic for financial contracts, since it could ultimately result in loss of money.

We therefore recommend the use of bigint type. But we support three ways of representing numbers for convenience and retrocompatibility with old versions of JS:

  • Native numbers:
    • They are straightforward to use
    • Notation is very simple and can be used with standard operators, e.g: 32 + 57
    • They lose precision for large amounts
  • String representation:
    • Notation just requires adding quotes around the numbers
    • You cannot use standard operators directly, e.g: "32" + "57" = "3257"
    • They do not lose precision
  • bigint type:
    • They are straightforward to use (just add n after number literals)
    • Notation is very simple and can be used with standard operators, e.g: 32n + 57n
    • They do not lose precision

All of these representations are converted to BigNumber internally, but a loss of precision may occur if native numbers are used, as the BigNumber is constructed, before the conversion occurs, and the API cannot do anything about it.

The EValue type and boolean overloading

In Haskell, constant boolean observations are represented by TrueObs and FalseObs, and constant integer values are represented by Constant followed by an Integer. In JavaScript and TypeScript you can also use these constructors, but you don't have to, because the Observation type is overloaded to also accept the native JavaScript booleans, and functions that in Haskell take a Value, in JavaScript they take an EValue instead, and EValue is defined as follows:

type EValue = SomeNumber | Value

Example: Writing a Swap contract in TypeScript

Whether we start by modifying an existing example, or by creating a new JavaScript contract, we are automatically provided with the import list and the function declaration. We can start by deleting everything that is not grayed-out, and start writing inside the curly brackets of the provided function definition.

Let's say we want to write a contract so that Alice can exchange 1000 Ada with Bob for $100.

First let's calculate the amounts we want to work with of each unit, we can define some numerical constants using const:

const lovelacePerAda : SomeNumber = 1000000n;
const amountOfAda : SomeNumber = 1000n;
const amountOfLovelace : SomeNumber = lovelacePerAda * amountOfAda;
const amountOfDollars : SomeNumber = 100n;

The amount in the contract must be written in Lovelace, which is 0.000001 Ada. So we calculate the amount of Lovelace by multiplying the 1,000 Ada for 1,000,000. The amount of dollars is 100 in our example.

The API already provides a constructor for the currency ADA, and there isn't currently a currency symbol in Cardano for dollars, but let us imagine there is, and let's define it as follows:

const dollars : Token = Token("85bb65", "dollar")

The string "85bb65" would in reality correspond to the currency symbol, which is a hash and must be written in base16 (hexadecimal representation of a byte string). And the string "dollar" would correspond to the token name.

Let's now define an object type to hold the information about the parties and what they want to exchange for convenience:

type SwapParty = {
party: Party;
currency: Token;
amount: SomeNumber;
};

We will store the name of the party in the party field, the name of the currency in the currency field, and the amount of the currency that the party wants to exchange in the amount field:

const alice : SwapParty = {
party: Role("alice"),
currency: ada,
amount: amountOfLovelace
}

const bob : SwapParty = {
party: Role("bob"),
currency: dollars,
amount: amountOfDollars
}

Now we are ready to start writing our contract. First let's define the deposits. We take the information from the party that must do the deposit, the timeout until which we'll wait for the deposit to be made, and the continuation contract that will be enforced if the deposit is successful.

function makeDeposit(src: SwapParty, timeout: ETimeout,
timeoutContinuation: Contract, continuation: Contract): Contract {
return When([Case(Deposit(src.party, src.party, src.currency, src.amount),
continuation)],
timeout,
timeoutContinuation);
}

We only need a When construct with a single Case that represents a Deposit of the src party into their own account, this way if we abort the contract before the swap each party will recover what they deposited.

Next we define one of the two payments of the swap. We take the source and destination parties as parameters, as well as the continuation contract that will be enforced after the payment.

const makePayment = function (src: SwapParty, dest: SwapParty,
continuation: Contract): Contract {
return Pay(src.party, Party(dest.party), src.currency, src.amount,
continuation);
}

For this, we just need to use the Pay construct to pay from the account where the source party made the deposit to the destination party.

Finally we can combine all the pieces:

const contract: Contract = makeDeposit(alice, 1700000000n, Close,
makeDeposit(bob, 1700003600n, Close,
makePayment(alice, bob,
makePayment(bob, alice,
Close))))

return contract;

The contract has four steps:

  1. Alice can deposit until POSIX time 1700000000 (2023-11-14 22:13:20 GMT).
  2. Bob can deposit until POSIX time 1700003600 (2023-11-14 23:13:20 GMT), one hour later, otherwise Alice gets a refund and the contract is aborted.
  3. Then we pay Alice's deposit to Bob.
  4. We pay Bob's deposit to Alice.

And that is it. You can find the full source code for a templated version of the swap smart contract in the examples in the Marlowe Playground, which we look at next.