Pact Core Concepts Part 2 - Learn Pact in 20 Minutes illustration

Pact Core Concepts Part 2 - Learn Pact in 20 Minutes

The Pact Core concepts series is a companion guide to the Real World Pact teaching repository. Written by Thomas Honeyman, a senior engineer at Awake Security, the series provides a first look at the essential concepts needed to write and test Pact programs on the scalable Chainweb blockchain. The series is a wonderful way to begin your Pact journey; after you’ve finished reading it, you are well-equipped to dive into the projects in the Real World Pact repository.

2. Learn Pact in 20 Minutes

Pact is a wonderful language for smart contract development. It boasts a slew of features specific to the blockchain environment — and, notably, a lack of features that tend to produce costly mistakes — and it's such a small language that you can master it in a few months.
However, Pact has a steep initial learning curve because so many concepts are a departure from standard programming languages you've seen before. It's an immutable, non-Turing-complete Lisp language with language constructs specifically designed for smart contract programming powerful features like fine-grained access control, sophisticated multi-signature authorization, on-chain data storage, cross-chain state transfers, and formal code verification. That's a lot to learn — especially if you're new to blockchain development!
This article is a crash course in the Pact language. We'll go through as many Pact snippets as we can in 20 minutes of reading time; by the time we're through you won't be an expert, but you'll be able to understand most Pact code you see in the wild. I recommend that you follow along in a Pact REPL session and/or by writing Pact code in a file named twenty-minutes.pact. You can see installation instructions on the Pact repo.
This article was written with Pact 4.6. If you use Nix, you can drop into a Pact REPL session with the same executable I used in one line via pact-nix:
1$ nix run github:thomashoneyman/pact-nix#pact-4_6_0
2pact>
3
Alternately, you can get the executable in your shell:
1$ nix develop github:thomashoneyman/pact-nix#pact-4_6_0
2$ pact --version
34.6.0
4
Without further ado: let's go!

Pact code is a list of expressions, where an expression is a literal, s-expression, atom, or reference. Pact code can contain comments; these begin with a semicolon and are ignored.
Pact has several basic literal types:
1"hello" ; a sequence of characters (string), created with quotes
2'hello  ; a unique string literal (symbol), created with a single quote
342      ; an unbounded positive or negative integer
4100.25  ; an unlimited precision positive or negative decimal
5true    ; a true or false (boolean) value
6
It also supports lists and objects. Lists are ordered sequences of values enclosed in square brackets. Objects are key-value pairs enclosed in curly braces where keys are strings.
1[1 2]     ; a homogeneous list of integers
2[1 true]  ; a heterogeneous list
3[1, true] ; commas are optional in lists
4{ "a": 1, "b": [1 2] } ; objects have string keys
5{ 'a: 1, 'b: [1, 2] }  ; symbols are string literals so they're ok too
6
S-expressions are enclosed in parentheses and begin with either an atom, reference, or a special form. An atom is a non-reserved bare identifier (ie. a variable) such as the name of a function.
When an s-expression begins with an atom or reference then it is interpreted as function application. Function parameters are separated by spaces.
1; an s-expression that begins with an atom (the 'reverse' identifier) and is
2; evaluated by applying the referenced reverse function to a list literal
3(reverse [1 2]) ; [2 1]
4
5; an s-expression that begins with a reference (atoms separated by periods)
6; and is evaluated by applying the balance function from the coin module to
7; a string literal
8(coin.balance "k:abc123") ; 1.23223
9
When the s-expression begins with a special form then the special form is interpreted. Pact has many of these which we'll explore later.
Pact has a type system. You can inspect the type of an expression — here are a few common ones, though Pact has more that we'll see later on:
1(typeof "hello")    ; "string"
2(typeof 'my-key)    ; "string"
3(typeof 1234)       ; "integer"
4(typeof 1.234)      ; "decimal"
5(typeof true)       ; "bool"
6(typeof { "a": 1 }) ; "object*"
7(typeof [])         ; "[<a>]", ie. a list of values of type 'a'
8(typeof reverse)    ; "defun", ie. a function
9
You can bind values to variables with let. The bound variables only exist within the scope of the let body. Pact does not allow mutation, so you can't reassign variables.
1(let
2  ; The first set of parentheses is a list of binding pairs — that's right, a
3  ; different list type from the syntax used to create list literals! Variables
4  ; can be annotated with their types with ':'
5  (
6    (x:integer (+ 1 1))   ; bind the integer 2 to the name 'x'
7    (y:[integer] [2 3 4]) ; bind the list of integers [2 3 4] to the name 'y'
8  )
9  ; After the binding pairs comes the let body, which must be an expression and
10  ; which can refer to the binding pairs. Below we take the first 2 (the value
11  ; of 'x') from the list 'y'.
12  (take x y)
13)
14; [2 3]
15
A binding pair in a let cannot refer to other binding pairs. For that, use let* — but note that this incurs a higher gas cost because it is less performant, and you should prefer let when possible.
1(let*
2  ; we can refer to 'x' in 'y' because we used let*, but this will produce a
3  ; "cannot resolve x" error if you only use let — try it yourself!
4  ( (x:integer 2) (y:integer (* x x)) )
5  (+ y x)
6)
7; 6
8
You can destructure objects with the bind function and the := binding operator:
1(bind
2  ; First, we supply the object that we want to destructure
3  { "a": 1, "b": 2, "c": 3, "d": 4 }
4  ; Then, we bind the value of the "a" key to the name 'a-value', and the
5  ; value of the "d" key to the name 'd-value'.
6  { "a" := a-value, "d" := d-value }
7  ; Then comes the body of the bind expression, where we can refer to the atoms
8  ; we bound, just as we've previously done with 'let' expressions.
9  (+ a-value d-value)
10)
11; 5
12
Pact supports anonymous functions via the lambda keyword. A lambda takes a list of arguments and a body, which is an expression that can access those arguments (the same as a let or bind).
Remember to try these snippets in a Pact REPL!
1(let
2  ; We bind the anonymous function denoted by the lambda to the name 'square'.
3  ; This function takes one argument (x) and multiplies it by itself.
4  ( (square (lambda (x) (* x x))) )
5  (square 13)
6)
7; 169
8
Lambdas are especially useful in list-processing functions like map, fold, filter, and zip. These list processing functions are used pervasively because Pact is not Turing-complete; that means it does not support loops or recursion.
The map function applies a function of one argument to every value in a list.
1; Here we use map to square every integer in a list.
2(map (lambda (x:integer) (* x x)) [1 2 3])
3; 1 4 9
4
The filter function removes items from a list according to a predicate function (ie. a function that returns true or false).
1; Here we use filter to remove values less than 10.
2(filter (lambda (x:integer) (>= x 10)) [1 10 100 1000])
3; [10 100 1000]
4
The zip function merges two lists together with a combining function. The combining function takes two arguments, which represents the two list values at a given index, and returns a new value to place in the resulting list. If the list lengths differ then the resulting list is the length of the shortest list.
1; Here we use zip to turn two lists into a list of objects.
2(zip (lambda (x:integer y:integer) { 'x: x, 'y: y }) [1 2] [3 4 5])
3; [ { "x": 1, "y": 3 }, { "x": 2, "y": 4 } ]
4
The fold function reduces a list of values to a value with a reducer function and an initial accumulator value. The reducer function takes two arguments — the accumulated value so far and the next value from the list — and returns a new accumulator value.
1; We can sum a list by adding all the values together.
2(fold (lambda (acc:integer val:integer) (+ acc val)) 0 [2 3 4])
3; 9
4
5; Similarly, we can multiply all values in a list.
6(fold (lambda (acc:integer val:integer) (* acc val)) 1 [2 3 4])
7; 24
8
9; Or we can convert a list of integers into a string by converting each integer
10; into a string and concatenating them together.
11(fold (lambda (acc:string val:integer) (+ acc (int-to-str 10 val))) "" [1 2 3])
12; "123"
13
14; Pact supports partial application, so we could rewrite our sum and fold
15; functions to not use lambdas.
16(fold (+) 0 [2 3 4]) ; 9
17(fold (*) 1 [2 3 4]) ; 24
18
We now know how to create literal values, bind them to names (ie. create atoms), create anonymous functions, and use them to manipulate collections of values. There are many more utilities for working with literal values and collections in Pact which you can find in the built-in functions section of the Pact documentation. We'll see many more of them in this article, but that's a handy reference!
Next, let's turn to conditional logic. Pact supports two forms of conditional logic: the if and cond functions. In Pact, functions are total, which means they always return a value. You can't handle only the true condition of an if statement, for example, unlike languages like JavaScript. Later' we'll see how to terminate a computation with an exception.
The first conditional logic function is the traditional if-else expression. It takes a condition, an expression to return if true, and an expression to return if false.
1(if
2  ; The first expression is a true/false condition
3  (= 4 (+ 2 2))
4  ; If the condition is true, then the first expression is returned
5  "All is well"
6  ; If not, then the second expression is returned
7  "Something horrible has gone wrong"
8)
9; "All is well"
10
The second conditional logic function is cond, which is technically a Pact special form rather than a normal function. It takes a series of if-else if-else expressions and turns them into nested if statements; it can't do anything that if-else can't do, but it's more convenient when you have many conditions.
1(let
2  ((x:integer 5))
3  ; cond takes a list of condition-return pairs (COND VALUE) and a default
4  ; value where if the condition is true then the value is returned, and if
5  ; none of the pairs match then the final default case is returned.
6  (cond
7    ((= x 1) "X is 1")
8    ((> x 10) "X is greater than 10")
9    ((> x 1) "X is greater than 1")
10    "X is less than 1"
11  )
12)
13; "X is greater than 1"
14
Conditional logic is useful to insert branches in your code, but it shouldn't be used for errors. If an error has occurred in your code — like a function received an invalid value — then you should use the enforce function to throw an exception. In a blockchain environment this aborts the transaction.
1(enforce true "This message is shown on error")
2; true
3
4(enforce false "This message is shown on error")
5; <interactive>:0:0: This message is shown on error
6;   at <interactive>:0:0: (enforce false "This message is shown on error")
7
The enforce family of functions provide the only non-local exit allowed by Pact (the only exception to Pact's requirement that functions are total, ie. return a value for all inputs). There are other enforce functions, such as enforce-keyset, enforce-guard, and enforce-one.
You can format strings with the format interpolation function.
1(enforce (= 10 100) (format "The value {} is not equal to {}" [10, 100]))
2; <interactive>:0:0: The value 10 is not equal to 100
3
So far we've focused on Pact code that can be written anywhere. However, a large portion of Pact code can only be written inside of a module — the main unit of code organization in Pact. Defining a module in the REPL mimics deploying it to the blockchain, so we should start to use explicit transactions. You can begin, end, and roll back transactions in the REPL:
1(begin-tx)
2; "Begin Tx 0"
3(commit-tx)
4; "Commit Tx 0"
5(rollback-tx) ; Undoes the transaction effects, but not the transaction counter
6; "Rollback Tx"
7
It's a good practice to use begin-tx and commit-tx in the REPL roughly around the points where you expect to do the same on Chainweb.
A minimal Pact module has a unique name atom (no other modules on-chain can share the name), a governance function, and a body containing the API and data definitions.
1(begin-tx)
2(module example GOVERNANCE
3  (defcap GOVERNANCE () true)
4  (defconst TEN 10))
5; "Loaded module example, hash G1MU80sMEUZxC2E5NQINJ7Pfe03S8nd1I-gdVZ_WPrk"
6(commit-tx)
7
defconst introduces a constant. We can access it by referring to its name within the module:
1(* 2 example.TEN)
2; 20
3
defcap introduces a capability. A capability is a predicate function used for access control. The governance capability controls smart contract upgrades: you can only upgrade a module (ie. to fix a bug) if its governance function is satisfied. For example, this module cannot be upgraded:
1; This module cannot be upgraded because the capability cannot be satisfied
2(module example GOVERNANCE
3  (defcap GOVERNANCE () false))
4
You can also govern a module via a keyset reference. A keyset reference is a string identifying a particular keyset that has been defined on-chain; you can't define a module with a reference to a keyset that doesn't exist yet.
1(module example "my-keyset"
2  (defconst TEN 10))
3; <interactive>:0:0: No such keyset: 'my-keyset
4
So what is a keyset? A keyset combines a list of public keys with a predicate function that determines how many of those keys must have signed a payload for the keyset to be satisfied. You can't write a keyset directly in Pact code, so here are a few keysets in their JSON form:
1// all keys must sign to satisfy the keyset
2{ "keys": ["pubkey1", "pubkey2"], "pred": "keys-all" }
3
4// any key can be used to sign to satisfy the keyset
5{ "keys": ["pubkey1", "pubkey2"], "pred": "keys-any" }
6
7// if the predicate function is omitted it's the same as 'keys-all'
8{ "keys": ["pubkey1", "pubkey2"] }
9
10// the same is true if the keyset is just a list of keys
11["pubkey1", "pubkey2"]
12
13// you can also specify a predicate function from a module for more
14// sophisticated situations
15{ "keys": ["pubkey1", "pubkey2"], "pred": "example.my-keyset-predicate"
16
A keyset reference is a string identifying a keyset that has been registered on Chainweb with define-keyset. We can't write a keyset directly in Pact, but we can write one in JSON, attach it to a transaction, and then read the data from the transaction in Pact. Let's do that in the REPL:
1; Pact code is always executed as part of a transaction. The env-data function
2; sets a JSON payload on that transaction.
3(env-data { "my-keyset": ["pubkey1"], "my-decimal": 1.12 })
4; "Setting transaction data"
5
6; We can then read from the transaction payload using the read-msg
7(read-msg "my-keyset")  ; ["pubkey1"]
8(read-msg "my-decimal") ; 1.12
9
10; However, read-msg can only decipher simple Pact types. It's not suitable
11; when you have a more complicated type like a keyset in mind.
12(typeof (read-msg "my-keyset")) ; "[*]"
13
14; Instead, you can use the read- function that matches the type you are
15; trying to decode.
16(typeof (read-keyset "my-keyset")) ; "keyset"
17
18; Be careful! If you assert the wrong type, weird things can happen.
19(read-integer "my-decimal") ; 1
20(read-integer "my-keyset")
21; <interactive>:0:0: read-integer: parse failed: Failure parsing integer: Array [String "pubkey1"]: ["pubkey1"]
22
A brief aside: on Chainweb, all modules, interfaces, and keyset references must be deployed to a namespace. A namespace is a unique prefix; free and user can be used by anyone, while anyone can create a principal namespace. Principal namespaces are generated, though, so if you want a vanity namespace like my-app it must be granted by the Kadena team.
The REPL doesn't include any namespaces because it isn't an actual blockchain. Some Pact code, though, requires that you are in a namespace, such as defining a keyset reference. We can define and enter a namespace with define-namespace and namespace:
1(define-namespace 'free (read-keyset 'my-keyset) (read-keyset 'my-keyset))
2; "Namespace defined: free"
3
4(namespace 'free)
5; "Namespace set to free"
6
7; We should go ahead and "re-deploy" our example module to the 'free namespace
8; so we are faithfully mimicking Chainweb.
9(module example GOVERNANCE
10  (defcap GOVERNANCE () false))
11; "Loaded module free.example, hash sT4jbDj2GhE_AGZG4A93FT5aAb-5Wfi_QTLS1WkCbJk"
12
One last thing: you can only register a keyset on-chain with define-keyset in a transaction that satisfies the keyset (ie. was signed by all required keys). You can simulate signing a transaction with the env-sigs function.
1; This transaction is signed with the private key associated with 'pubkey1' and
2; the signature is not scoped to any capabilities
3(env-sigs [ { "key": "pubkey1", "caps": [] } ])
4; "Setting transaction signatures/caps"
5
6; This transaction is also signed by pubkey1, but this time the signer has also
7; signed for the GOVERNANCE capability.
8(env-sigs [ { "key": "pubkey1", "caps": [ (free.example.GOVERNANCE) ] } ])
9; "Setting transaction signatures/caps"
10
We can now put it all together and define a keyset:
1; These two lines would not be in the smart contract code, but rather would
2; be on the transaction itself.
3(env-data { "admin-keyset": [ "pubkey1" ] })
4(env-sigs [ { "key": "pubkey1", "caps": [] } ])
5
6; First we enter a namespace
7(namespace "free")
8
9; Then we define our keyset reference
10(define-keyset "free.admin-keyset" (read-keyset "admin-keyset"))
11
12; Then we use it to govern the module we are deploying, which means the module
13; can only be upgraded (on Chainweb) in a transaction signed by this keyset.
14(module example "free.admin-keyset"
15  (defconst TEN 10))
16; "Loaded module free.example, hash oxlUPEWJuToByJ47pe7RwyG8BDiSmfFuJ_QOoxrQ7hc"
17
You can use a keyset reference to govern a Pact module, or you can use a governance capability and the enforce-keyset function, which will throw an exception if the given keyset is not satisfied by the transaction signatures.
1; In practice this is the same as the previous module we defined, except that
2; an upgrade would sign with the GOVERNANCE capability instead of just signing
3; the transaction in general.
4(module example GOVERNANCE
5  (defcap GOVERNANCE () (enforce-keyset "free.admin-keyset")))
6
You can define more than constants and capabilities in a Pact module. You can also define functions, object types (schemas), database tables, formal verification properties, and more.
Functions are defined with the defun special form. They have a name, a list of arguments, and a function body. You can add type annotations for the arguments and the return type.
1(module example "free.admin-keyset"
2  (defun square:integer (x:integer)
3    (* x x))
4  (defun sum:integer (xs:[integer])
5    (fold (+) 0 xs))
6)
7; "Loaded module free.example, hash rIBzCpDKTx0bC3FH84q98U7Emt0yzRJ7qoNHHJaAORc"
8
9(free.example.square 3)    ; 9
10(free.example.sum [1 2 3]) ; 6
11
Modules, functions, and many other Pact forms can have documentation strings:
1(module example "free.admin-keyset"
2  @doc "An example module"
3
4  (defun square (x:integer)
5    @doc "A function to square integers"
6    (* x x))
7
8  (defun sum (xs:[integer])
9    @doc "A function to sum a list"
10    (fold (+) 0 xs))
11)
12
You can write multiline strings with backslash escapes.
1(module example "free.admin-keyset"
2  @doc "An example module that has a very long \
3       \description of its contents."
4
5  (defun add1 (x:integer) (+ 1 x))
6)
7
Pact modules can also define object types (schemas) with the defschema special form.
1(module example "free.admin-keyset"
2  ; A schema takes an atom that is used to refer to this type and then
3  ; a list of fields, optionally associated with types.
4  (defschema person
5    @doc "Schema for a person object type"
6    first:string
7    last:string
8    age:integer)
9
10  ; We can then use this new type in our code.
11  (defun get-age:integer (x:object{person})
12    (at 'age x))
13
14  (defun can-drink-america:bool (x:object{person})
15    (>= (get-age x) 21))
16)
17
18(free.example.get-age { "first": "Eliud", "last": "Kipchoge", "age": 38 })
19; 38
20
21; Specifying the type gets us better error messages from the Pact interpreter
22(free.example.get-age { "first": "Eliud", "age": 38 })
23; <interactive>:10:26: Missing fields for {person}: [last:string]
24
Pact is a unique language in that it allows you to define and interact with database tables directly in your modules with the deftable special form. Pact has many functions for dealing with databases, which we'll see below.
1(begin-tx)
2(namespace 'free)
3(module example GOV
4  (defcap GOV () (enforce false "No governance."))
5
6  (defschema runner
7    country:string
8    age:integer)
9
10  ; We can use this schema to define a table for persistent on-chain storage.
11  ; Tables always have string keys, and each row is an object with the
12  ; specified schema type.
13  (deftable runner-table:{runner}
14    @doc "A table for runner ages")
15
16  (defun add-runner (name:string entry:object{runner})
17    ; We can insert rows into the table (will fail if the key already exists)
18    (insert runner-table name entry))
19
20  (defun get-runner:object{runner} (name:string)
21    ; We can read a value from the table (will fail if the key is missing)
22    (read runner-table name))
23
24  (defun get-runner-age:integer (name:string)
25    ; with-read is a combination of read and bind: it lets you read a row and
26    ; bind its fields to variable names.
27    (with-read runner-table name
28      ; here we bind only the "age" field to the age name
29      { "age" := age }
30      ; and then we return it.
31      age))
32
33  (defun get-runner-age-default:integer (name:string)
34    ; with-default-read is like with-read, but it returns the specified default
35    ; value instead of throwing an exception on failure.
36    (with-default-read runner-table name
37      ; We provide a default value in the case the row cannot be found.
38      { "age": 0, "country": "Uzbekistan" }
39      { "age" := age }
40      age))
41
42  (defun under-30:[object{runner}] ()
43    ; We can also select many rows using a filter function.
44    (select runner-table (where 'age (> 30))))
45
46  (defun names:[string] ()
47    ; Or read all the keys used in the table
48    (keys runner-table))
49)
50; "Loaded module free.example, hash 9uMpahkbL4dgdUHe1tZPVwQy2A4A-kWhsCSeQSeK7A0"
51
Defining a table is not quite enough — we also need to use the create-table top-level function outside our module to create the table. Without it you'll see an error:
1(free.example.names)
2; <interactive>:21:4: : Failure: Database exception: query: no such table: USER_free.example_runner-table
3;  at <interactive>:21:4: (keys (deftable runner-table:(defschema runner  [country...)
4;  at <interactive>:0:0: (names)
5
But everything is OK if we remember to create the table.
1(create-table free.example.runner-table)
2; "TableCreated"
3
We can now interact with our database.
1(free.example.add-runner "Eliud Kipchoge" { 'age: 38, 'country: "Kenya" })
2; "Write succeeded"
3(free.example.add-runner "Brigid Kosgei" { 'age: 29, 'country: "Kenya" })
4; "Write succeeded"
5(free.example.get-runner "Eliud Kipchoge")
6; { "age": 38, "country": "Kenya" }
7(free.example.get-runner "John Titus")
8; <interactive>:15:4: read: row not found: John Titus
9(free.example.under-30)
10; [{ "age": 29, "country": "Kenya" }]
11(free.example.names)
12; ["Brigid Kosgei" "Eliud Kipchoge"]
13(free.example.get-runner-age "John Titus")
14; <interactive>:15:4: read: row not found: John Titus
15(free.example.get-runner-age-default "John Titus")
16; 0
17
In Pact, data access is constrained to only functions defined in the same module or in a transaction where governance of the module has been granted. Since we're in the module deployment we can still freely access our tables outside the module:
1; This can only be run at the top-level because governance of the module has
2; been granted.
3(read free.example.runner-table "Brigid Kosgei")
4; { "age": 38, "country": "Kenya" }
5
However, once we commit our current transaction we can no longer acquire governance (since the governance capability is always false), and can no longer access tables directly. Tables should be accessed through functions instead.
1(commit-tx)
2(read free.example.runner-table "Brigid Kosgei")
3; <interactive>:3:5: No governance
4
You'll find that most Pact modules consist of one or more databases, a selection of functions that manipulate the database, and a set of capabilities that control access to sensitive functions.
We have only a few minutes left, so let's talk about guards and capabilities. A guard is a predicate function over some environment that enables a pass-fail operation. The most typical guard is a keyset, like we've seen before. Assuming you've been following along in the REPL:
1; You can look at the contents of a keyset reference with describe-keyset
2(typeof (describe-keyset "free.example-keyset"))
3; "keyset"
4
5; You can enforce a keyset guard with enforce-keyset. The below will fail,
6; indicating that the transaction signatures do not satisfy the keyset.
7(env-sigs [])
8(enforce-keyset "free.example-keyset")
9; <interactive>:0:0: Keyset failure (keys-all): 'free.example-keyset
10
11; We can fix it by signing the transaction with the required keys.
12(env-sigs [ { "key": "pubkey1", "caps": [] } ])
13(enforce-keyset "free.example-keyset")
14; true
15
Keysets are just one of several types of guard. Some functions expect to work with an arbitrary guard (not just keysets), so you can turn a keyset reference into a more general guard with keyset-ref-guard.
1(typeof (keyset-ref-guard "free.example-keyset"))
2; "guard"
3
Arbitrary guards are enforced with enforce-guard.
1(enforce-guard (keyset-ref-guard "free.example-keyset"))
2; true
3
4; Pact will interpret strings as keyset references by default, so you don't
5; actually have to use keyset-ref-guard with enforce-guard
6(enforce-guard "free.example-keyset")
7; true
8
The two most common guards are capability guards, which let you enforce that a particular capability has been acquired, and user guards, which lets you turn an arbitrary predicate function into a guard.
1(begin-tx)
2(module guard-example "free.example-keyset"
3  (defcap CAP_SUCCEED () (enforce true "succeed"))
4  (defcap CAP_FAIL () (enforce false "fail"))
5  (defun succeed () (enforce true "succeed"))
6  (defun fail () (enforce false "fail"))
7)
8
9(create-capability-guard (free.guard-example.CAP_SUCCEED))
10; CapabilityGuard {name: free.guard-example.CAP_SUCCEED,args: [],pactId: }
11(typeof (create-capability-guard (free.guard-example.CAP_FAIL)))
12; "guard"
13(create-user-guard (free.guard-example.succeed))
14; UserGuard {fun: free.guard-example.succeed,args: []}
15(typeof (create-user-guard (free.guard-example.fail)))
16; "guard"
17
18; Each guard can be enforced with enforce-guard. Note that the user guard
19; functions are simple predicates and can be called directly:
20(enforce-guard (create-user-guard (free.guard-example.succeed)))
21; true
22(enforce-guard (create-user-guard (free.guard-example.fail)))
23; <interactive>:5:17: fail
24
25; But capabilities must be "acquired" — they aren't simple functions.
26(enforce-guard (create-capability-guard (free.guard-example.CAP_SUCCEED)))
27; <interactive>:0:0: Capability not acquired
28(commit-tx)
29
You can acquire a capability with with-capability. However, capabilities are like database tables, in that access can only be granted by code within the same module or when the transaction has governance priviliges. We'll see more about with-capability in a moment.
Guards are especially useful because they can be stored in tables. For example, you can store a list of user accounts along with a guard that must be satisfied to modify the account. That way the only way to modify the database record is by having the keyset or satisfying the account guard.
1(begin-tx)
2(module guard-example GOV
3  (defcap GOV () true)
4
5  (defcap CAP_SUCCEED () (enforce true "succeed"))
6
7  (defcap UPDATE_ADDRESS (name:string)
8    (with-read account-table name
9      { "guard" := guard }
10      ; On this line we enforce that the guard associated with the account
11      ; is satisfied before processing the update.
12      (enforce-guard guard))
13
14  (defschema account
15    name:string
16    address:string
17    ; a 'guard' is a suitable type for storage in a database
18    guard:guard)
19
20  (deftable account-table:{account})
21
22  (defun succeed () (enforce true "succeed"))
23
24  (defun change-address (name:string new-address:string)
25    (with-capability (UPDATE_ADDRESS name)
26      (update account-table name { "address": new-address })))
27)
28
29(create-table free.guard-example.account-table)
30
31; Recall that access to capabilities and tables is only allowed in module
32; functions or when governance is granted for a module. Right now we're in
33; the deployment transaction so we can freely access both.
34
35(insert free.guard-example.account-table "Rick"
36  { "name": "Rick"
37  , "address": "1111 Pine St"
38  , "guard": (create-user-guard (free.guard-example.succeed))
39  })
40; "Write succeeded"
41
42(insert free.guard-example.account-table "Morty"
43  { "name": "Morty"
44  , "address": "1111 Pine St"
45  , "guard": (create-capability-guard (free.guard-example.CAP_SUCCEED))
46  })
47; "Write succeeded"
48
49; We can change Rick's address because it is protected by a guard that
50; always evaluates to 'true'
51(free.guard-example.change-address "Rick" "1234 Pine St")
52; [true "Write succeeded"]
53
54; We can't just change Morty's address because it is protected by a
55; capability — one that is always true, but still must be acquired.
56(free.guard-example.change-address "Morty" "1234 Pine St")
57; <interactive>:15:8: Capability not acquired
58
59; We can acquire a capability via with-capability, but not at the
60; top-level. We'll do it inside a let instead.
61(let ((_ 0))
62  ; with-capability attempts to acquire a capability, failing if the
63  ; predicate function fails (for example, if the capability required
64  ; a signature on the transaction and there was none). in this case,
65  ; the predicate for the capability always returns true, so we're ok.
66  (with-capability (free.guard-example.CAP_SUCCEED)
67    (free.guard-example.change-address "Morty" "1234 Pine St")))
68; [true "Write succeeded"]
69
70(commit-tx)
71
There are a few important functions for working with capabilities. We've seen with-capability, which acquires a capability for the scope of its body, allowing you to take sensitive actions. There is also require-capability which enforces that a capability has been acquired with with-capability.
1; Require that CAPABILITY has already been acquired with with-capability for
2; subsequent lines of code to run — essentially, enforce the capability.
3(require-capability (CAPABILITY))
4
5; It's like to writing this:
6(enforce-guard (create-capability-guard (CAPABILITY)))
7
If you want all the nitty-gritty details on how the Pact capability system works, please see the Capability Theory in Pact wiki entry.
And with that our time is up! You should be able to get the gist of most of the Pact code you encounter in the wild, although you certainly won't be proficient from simply scanning these snippets.
Writing Pact is a different experience from reading it because you both must understand Pact and solve your problem at the same time, translating the solution into usable Pact code.
For more Pact material you may want to check out:
Have fun writing some Pact!
Author:
Thomas Honeyman

Senior Engineer

Date:
Apr 27, 2023
Read:
28 mins read