cardano-transaction-lib (CTL) is a Purescript library for building smart contract transactions on Cardano. It aims to port the functionality and interface of Plutus off-chain code to the browser environment.
Table of Contents
- Stage 1 Build a simple transaction in the browser that works with at least one light wallet (Nami)
- Stage 2 Once we can construct a simple user-to-user transaction, we will try to use the library to submit the tx with nami
- Stage 3 Once we have a simple working transaction, we will seek to build a Plutus smart contract transaction with datum from scratch
- Stage 4 Once we can construct Plutus smart contract transactions, we will seek to build a library/DSL/interface such that transactions can be built using constraints and lookups - as close as possible to a cut-and-paste solution from Plutus'
Contract
monad code in Haskell (but with no guarantee that code changes are not necessary) (In progress) - Stage 5 Once we have a basic
Contract
-style API, we will further refine its public interface, expand wallet support (see below), expose a test interface, provide a more ergonomic JS/TS API, support stake validators, and support CIP workflows on the public testnet - Stage 6 Once CTL's
Contract
interface has been stabilized, we will add support for even more wallets and attempt to deprecate CTL's currently required Haskell server
Support is planned for the following light wallets, roughly in order of implementation:
In order to run CTL's Contract
effects, several services are required. These can be configured through a ContractConfig
that holds websocket connections, information about server hosts/ports, and other requisite information.
Services that are currently required:
- Ogmios
- You must use Ogmios v5.2.0 or greater with CTL
- Ogmios itself requires a running Cardano node, so you may also need to deploy a node. Node v1.34.0 or greater is recommended
- You can also use our fork which has improved Nix integration
ogmios-datum-cache
- This is required to query for datums, which Ogmios itself does not support
- This in turn requires a PostgreSQL DB
- Our Haskell server
- We hope to deprecate this in the future, but we use it at the moment for certain Cardano libraries that have no Purescript analogue
- To build the server project, run the following from the repository root:
nix build -L .#ctl-server:exe:ctl-server
CTL's overlay (contained in its flake outputs
) provides some mechanisms for conveniently launching all runtime services using Arion(itself a wrapper around docker-compose
). To use this, you must have a setup based on Nix flakes (recommended as well for using CTL as a dependency for Purescript projects).
Here is an example that uses the overlay to launch runtime services:
{
inputs = {
# You should probably pin this to a specific revision, especially if using
# it for Purescript projects
cardano-transaction-lib.url = "github:Plutonomicon/cardano-transaction-lib";
# To use the same version of `nixpkgs` as we do
nixpkgs.follows = "cardano-transaction-lib/nixpkgs";
};
outputs = { self, cardano-transaction-lib, nixpkgs, ... }:
# some boilerplate
let
defaultSystems = [ "x86_64-linux" "x86_64-darwin" ];
perSystem = nixpkgs.lib.genAttrs defaultSystems;
# generate `pkgs` with the CTL overlay applied. This gives you access to
# various additional packages, using the same versions of CTL, including:
nixpkgsFor = system: import nixpkgs {
inherit system;
overlays = [ cardano-transaction-lib.overlay.${system} ];
};
# The configuration for the CTL runtime, which will be passed to the
# expression that builds the JSON file used by Arion. This value can be
# shared between `buildCtlRuntime` and `launchCtlRuntime`, as shown below
runtimeConfig = {
# *All* of these values are optional, and shown with their default
# values. If you need even more customization, you can use `overideAttrs`
# to change the values after calling `buildCtlRuntime` (e.g. a secrets
# volume for the `postgres` service)
node = { port = 3001; };
ogmios = { port = 1337; };
ctlServer = { port = 8081; };
postgres = {
port = 5432;
user = "ctxlib";
password = "ctxlib";
db = "ctxlib";
};
# These values will generate the `config.toml` required by ogmios-datum-cache
datumCache = {
port = 9999;
# If you override some part of `postgres` above, you may also need to
# modify the `dbConnectionString`
dbConnectionString = nixpkgs.lib.concatStringsSep
" "
[
"host=postgres"
"port=${toString postgres.port}"
"user=${postgres.user}"
"dbname=${postgres.db}"
"password=${postgres.password}"
];
saveAllDatums = true;
firstFetchBlock = {
slot = 44366242;
id = "d2a4249fe3d0607535daa26caf12a38da2233586bc51e79ed0b3a36170471bf5";
};
};
};
in
{
# `launchCtlRuntime` will generate a Nix expression from the provided
# config, build it into a JSON file, and then run it with Arion
#
# Use `nix run .#<APP>` to run the services (e.g. `nix run .#ctl-runtime`)
apps = perSystem (system: {
ctl-runtime = (nixpkgsFor system).launchCtlRuntime runtimeConfig;
});
# `buildCtlRuntime` will generate a Nix expression that, when built with
# `pkgs.arion.build`, outputs a JSON file compatible with Arion. This can
# be run directly with Arion or passed to another derivation. Or you can
# use `buildCtlRuntime` with `runArion` (from the `hercules-ci-effects`)
# library
#
# Use `nix build .#<PACKAGE` to build. To run with Arion (i.e. in your
# shell): `arion --prebuilt-file ./result up`
packages = perSystem (system:
let
pkgs = nixpkgsFor system;
in
{
ctl-runtime = pkgs.arion.build {
inherit pkgs;
modules = [ (pkgs.buildCtlRuntime runtimeConfig) ];
};
});
};
}
For launching services for developing CTL itself, see below.
In order to run most Contract
actions, you must use Nami wallet for the time being. The following steps must be taken to ensure that you can run CTL contracts:
- Install the Nami extension
- Due to limitations with Nami itself, only Chromium-based browsers are supported
- Make sure that you have an active wallet
- Make sure that you have set collateral for the wallet, which Nami reserves apart from other wallet UTxOs
- Make sure that your wallet is running on the testnet (can be configured via a toggle in the settings menu)
This project uses Nix flakes. In order to use flakes, you will need Nix version 2.4 or greater. You also need to enable additional experimental features. Make sure you have the following enabled in your nix.conf
(typically located in /etc/nix/
or ~/.config/nix/
) or in nix.extraOptions
in your NixOS configuration:
experimental-features = nix-command flakes
You may also choose to enable these every time you use nix
commands (and without modifying your nix.conf
) by passing the following command-line options:
nix <COMMAND> --extra-experimental-features nix-command --extra-experimental-features flakes
Running nix develop
in the root of the repository will place you in a development environment with all of the necessary executables, tools, config, etc... to:
- build the project or use the repl with
spago
- use
npm
and related commands; all of the project's JS dependencies are symlinked from the Nix store intonode_modules
in the repository root - use Ogmios and other tools with the correct configurations, socket path, etc... These are also set in the
devShell
'sshellHook
NOTE: As the Nix devShell
currently symlinks the project's node_modules
, do not use npm install
in order to develop with npm
. Use nix develop
as noted above
To develop locally, you can use one the CTL flake to launch all required services (using default configuration values):
- The easiest way:
nix run -L .#ctl-runtime
will both build and run the services - The same, but indirectly in your shell:
$ nix build -L .#ctl-runtime $ arion --prebuilt-file ./result up
- To build the project without bundling and for a NodeJS environment:
nix build
orspago build
- To test the project, currently only supported when running in a NodeJS environment:
spago test
ornpm run test
will run both the integration and unit testsnix build .#checks.<SYSTEM>.ctl-unit-test
will build and run the unit tests (useful for CI)
- To run or build/bundle the project for the browser:
make run-dev
ornpm run dev
will start a Webpack development server atlocalhost:4008
make run-build
ornpm run build
will output a Webpack-bundled example module todist
nix build -L .#ctl-example-bundle-web
will build an example module using Nix and Webpack
By default, Webpack will build a small Purescript example. Make sure to follow the instructions for setting up Nami before running the examples. You can point Webpack to another Purescript entrypoint by changing the ps-bundle
variable in the Makefile or in the main
argument in the flake's packages.ctl-examples-bundle-web
.
Note: The BROWSER_RUNTIME
environment variable must be set to 1
in order to build/bundle the project properly for the browser (e.g. BROWSER_RUNTIME=1 webpack ...
). For Node environments, leave this variable unset or set it to 0
.
Unfortunately, we rely on spago2nix
, which requires autogenerated Nix code (spago-packages.nix
). This means that it is possible for our declared Purescript dependencies to drift from the autogen Nix code we import in to build Purescript derivations. If you add either a Purescript dependency, make sure to run spago2nix generate
from within the Nix shell to update the autogen code from spago2nix
. Do not edit spago-packages.nix
by hand, or the build will likely break.
If you add a dependency to package.json
, make sure to update the lockfile with npm i --package-lock-only
before entering a new dev shell, otherwise the shellHook
will fail. You'll need to remove the existing symlinked node_modules
to do this (for some reason npm
will still try to write to the node_modules
, but will fail because they're symlinked to the Nix store).
CTL can be imported as an additional dependency into a Purescript project built with Spago (i.e. by listing the project in your packages.dhall
). Note that the following always applies when using CTL from your project:
- Only bundling with Webpack is supported (this is due to our internal dependency on
cardano-serialization-lib
which uses WASM; only Webpack is reliably capable of bundling this properly) - The environment variable
BROWSER_RUNTIME
determines which version ofcardano-serialization-lib
is loaded by CTL, so you must use it as well (i.e. set it to1
for the browser; leave it unset for NodeJS)
Furthermore, CTL exposes an overlay
from its flake. You can use this in the Nix setup of your own project to use the same setup as we do, e.g. the same packages and PS builders. Here is an example flake.nix
that takes CTL as a dependency:
{
inputs = {
cardano-transaction-lib = {
type = "github";
owner = "Plutonomicon";
repo = "cardano-transaction-lib";
# NOTE
# This should match the same revision as the one in your `packages.dhall` to ensure
# the greatest compatibility
rev = "f65eb08656f9da4ad1b83b09d25422bcf4835e9c";
};
# To use the same version of `nixpkgs` as we do
nixpkgs.follows = "cardano-transaction-lib/nixpkgs";
};
outputs = { self, cardano-transaction-lib, ... }:
# some boilerplate
let
defaultSystems = [ "x86_64-linux" "x86_64-darwin" ];
perSystem = nixpkgs.lib.genAttrs defaultSystems;
# generate `pkgs` with the CTL overlay applied. This gives you access to
# various additional packages, using the same versions of CTL, including:
# - all of `easy-purescript-nix`
# - Ogmios and `ogmios-datum-cache`
# - `cardano-cli`
nixpkgsFor = system: import nixpkgs {
inherit system;
overlays = [ cardano-transaction-lib.overlay.${system} ];
};
# The overlay also include several tools for generating a PS project
# using the same approach as CTL, under `purescriptProject`
psProjectFor = system:
let
pkgs = nixpkgsFor system;
# This is the root of the project. Typically, this would be `self`
# for flakes-based projects
#
# You may also want to filter this to avoid bloat or unecessary
# rebuilds when copying the source into derivations
src = self;
in
pkgs.purescriptProject {
inherit pkgs src;
# Will be used to generate derivation name
projectName = "your-project";
# Optional arg to override the version of `nodejs` used, defaulting to
# the version used by CTL itself. This will be used throughout
# `purescriptProject`
#
# Note that the version of `purs` is not configurable, as CTL
# will currently break with any other version (it uses 0.14.5
# internally)
nodejs = pkgs.nodejs-14_x;
# Also optional; the path to the packages generated by `spago2nix`,
# defaulting to:
spagoPackages = ./spago-packages.nix;
# The optional `shell` lets you configure the `devShell` that is
# generated by `purescriptProject`
#
# All of the attrs below are entirely optional (shown here with
# their default values)
shell = {
# Extra packages to include in the shell environment. By default
# a common version of `nodejs`, `purs`, `spago`, and more are
# included
packages = [ ];
# This will be appended to the `shellHook` that runs. By default,
# the `shellHook` loads generated `node_modules` and exports a
# modified `NODE_PATH` and `PATH`
shellHook = "";
# The same as `pkgs.mkShell.inputsFrom`
inputsFrom = [ ];
# Which formatter to be made available, `purty` is another option
formatter = "purs-tidy";
# If `purescript-language-server` should be included in the shell
pursls = true;
};
};
in
{
packages = perSystem (system:
{
# `buildPursProject` just builds all of the project's PS sources;
# it can be helpful for CI to ensure the project builds or to
# pass to another derivation
your-project = (psProjectFor system).buildPursProject {
# A list of directories to copy into the builder, relative to the
# root provided in `purescriptProject.src`, and defaulting to
# `["src"]`. If you have files needed at runtime, you must include
# them as well
sources = ["src"];
};
# `bundlePursProject` creates a JS bundle with webpack
your-project-bundle = (psProjectFor system).bundlePursProject {
# A list of directories to copy into the builder, relative to the
# root provided in `purescriptProject.src`, and defaulting to
# `["src"]`. If you have files needed at runtime, you must include
# them as well
sources = ["src" "exe"];
# All of the following are optional and show with default values:
#
# The main Purscript module entrypoint
main = "Main";
# The JS entrypoint (must correspond to the one listed in the
# webpack config), relative to the `src`
entrypoint = "index.js";
# The HTML template to render the bundle to (must correspond to
# the template listed in the webpack config)
htmlTemplate = "index.html";
# If this should be bundled for the browser
browserRuntime = true;
# The path to the webpack config to use
webpackConfig = "webpack.config.js";
# The module that `spago bundle-module` should write to (must
# match the one that is imported in your JS entrypoint). Is
# relative to the `src` argument provided to `purescriptProject`
bundledModuleName = "output.js";
};
});
checks = perSystem (system:
{
# Build and run a test, also useful for CI
your-project = (psProjectFor system).runPursTest {
# Optional arg, the default value is:
testMain = "Test.Main";
# See note about `sources` above
sources = [ "src" "test" "fixtures" ];
};
});
devShell = perSystem
(system:
# This corresponds to the `shell` argument given above
(psProjectFor system).devShell
);
};
}
We have recenly set up a small scaffolding repository for projects wishing to adopt CTL: https://github.com/mlabs-haskell/ctl-scaffold. More documentation and resources will be added soon to the repo
CTL is directly inspired by the Plutus Application Backend (PAB). Unlike PAB, however, CTL is a library and not a standalone process. Over the course of CTL's development, several questions have been raised as to how best create PAB-as-a-library:
- How do we get the transaction in the right format?
- This is handled by
cardano-serialization-lib
, a Rust library available as WASM
- This is handled by
- How do we query the chain?
- This has been solved using Ogmios
- We may, however, support a BlockFrost backend as well in the future
- How do we query for datums (i.e. the datums themselves and not just their hashes)?
ogmios-datum-cache
solves this problem
- How do we submit the transaction?
- This is done via browser-based light wallet integration in the browser based on CIP-30
- How closely should we follow Plutus'
Contract
API?- CTL's
Contract
model is significantly less restrictive than Plutus' and allows for arbitrary effects within theContract
monad - Certain features cannot be directly translated into Purescript from Haskell due to differences between the two languages (e.g. CTL's
DatumType
andRedeemerType
are type class with fundeps, as Purescript lacks any notion of type families/type-level functions)
- CTL's
- A lingering concern remains around storage solutions, if needed
- This can be in memory, in various browser storage solutions, or a decentralized DB like Fluree
cardano-serialization-lib
(Sundae fork)- Ogmios for chain queries
- CIP-30 (wallet interface - Nami partially implements this)
- Nami docs
- Alonzo CDDL spec