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
- They are straightforward to use (just add
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:
- Alice can deposit until POSIX time 1700000000 (2023-11-14 22:13:20 GMT).
- 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.
- Then we pay Alice's deposit to Bob.
- 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.