-
Notifications
You must be signed in to change notification settings - Fork 12
/
Copy pathfaucet.repl
261 lines (244 loc) · 13.6 KB
/
faucet.repl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
; REPL files are a fast way to test our contracts from the terminal. It's a
; common practice to develop contracts with an associated REPL file. This lets
; you exercise various functions from the contract, and provides other
; developers concrete usage examples.
;
; This REPL file tests the goliath-faucet module. Along the way, we'll learn
; a number of useful REPL-only functions that you can use when writing your
; own contracts. In the rest of this file, we will:
;
; 1. Load a REPL environment from the ../pact-repl-utils/init.repl file
; 2. Deploy the faucet contract
; 3. Formally verify our contract
; 4. Execute code from our contract, test the results, and measure the total
; gas consumption of various functions.
;
; You can execute this file from the command line with:
; $ pact faucet.repl
;
; Alternately, to execute this file and remain in the REPL so you can interact
; with the running program, run `pact` from the command line, then:
; pact> (load "faucet.repl")
;
; This file is written with the expectation you have already read through
; the associated smart contract. Concepts like namespaces, keysets, interfaces,
; modules, schemas, tables, constants, functions, and formal verification are
; explained there.
;
; One last thing: we are going to wrap each logical step in our REPL session in
; (begin-tx) and (commit-tx). This mimics executing a transaction on chain. It
; also allows us to run a few steps and then undo them without committing via
; the (rollback-tx) function.
; https://pact-language.readthedocs.io/en/latest/pact-functions.html#begin-tx
; https://pact-language.readthedocs.io/en/latest/pact-functions.html#commit-tx
; https://pact-language.readthedocs.io/en/latest/pact-functions.html#rollback-tx
;
; --------------------
; 1. Load the REPL environment
; --------------------
; The REPL environment is not Chainweb, so it doesn't have data that our
; contract will expect to exist on Chainweb when it is deployed, such as the
; modules and keysets it depends on. We've implemented a REPL file that creates
; a suitable environment in the REPL for testing our code. Specifically, it
; creates and enters the "free" namespace, creates and funds several test
; accounts, and loads the modules our contract depends on (such as the "coin"
; module).
(load "../pact-repl-utils/init.repl")
; --------------------
; 2. Create the test environment
; --------------------
; We are now in an environment similar to Chainweb. Next, let's mimic the
; deployment process we will take in the real world. We need to create and fund
; the admin faucet account and use it to deploy the faucet contract.
; Accounts can be created on Chainweb using the (coin.create-account) function
; or by transferring funds to the account with (coin.transfer-create). Since we
; loaded the 'init.repl' file, we have access to a 'sender00' account that has
; funds they can transfer to create the faucet admin account.
;
; In the real world, you might create a k:account, then purchase KDA on an
; exchange and have the exchange transfer the funds to your address. You can
; then use your account to deploy contracts.
;
; Recall that the (coin.transfer-create) function relies on the (coin.TRANSFER)
; capability, and therefore we must scope the "sender00" signature to this
; capability as part of the transaction. We can add a signature to the current
; transaction with the REPL-only (env-sigs) function:
; https://pact-language.readthedocs.io/en/stable/pact-functions.html#env-sigs
(env-sigs [ { "key": test-keys.SENDER00, "caps": [ (coin.TRANSFER "sender00" "goliath-faucet" 1000.0) ] } ])
(env-data { "goliath-faucet-keyset": { "keys": [ "goliath-faucet-public-key" ], "pred": "keys-all" } })
(begin-tx)
(coin.transfer-create "sender00" "goliath-faucet" (read-keyset "goliath-faucet-keyset") 1000.0)
(commit-tx)
(env-data {})
(env-sigs [])
; Recall that our contract expects to read an "init" boolean from transaction
; data so it knows whether it needs to initialize data or not. It also reads
; the "goliath-faucet-keyset" field to know what keyset to register to governy
; the contract. We can use the (env-data) REPL-only function to associate
; transaction data:
; https://pact-language.readthedocs.io/en/stable/pact-functions.html#env-data
;
; Our contract also includes a safety check that ensures that the faucet keys
; were used to deploy the contract. We therefore must use (env-sigs) again to
; sign the deployment.
(env-data { "init": true, "goliath-faucet-keyset": { "keys": [ "goliath-faucet-public-key" ], "pred": "keys-all" } })
(env-sigs [ { "key": "goliath-faucet-public-key", "caps": [] } ])
(begin-tx)
; And now we "deploy" our contract, which will also register our keyset at the
; name "free.goliath-faucet-keyset".
(load "./faucet.pact")
(commit-tx)
(env-sigs [])
(env-data {})
; --------------------
; 2. Formally verify the contract
; --------------------
; Pact provides a powerful formal verification system that can prove that
; invalid states cannot be reached in our contract code. Our functions have some
; property testing models and our table has some invariants that it would like
; Pact to verify. We can typecheck our code and run formal verification in the
; REPL with (verify):
; https://pact-language.readthedocs.io/en/latest/pact-properties.html
; https://pact-language.readthedocs.io/en/latest/pact-functions.html#verify
;
; Try disabling some of the (enforce) checks in the Pact module to see how the
; formal verification catches invalid inputs!
(verify "free.goliath-faucet")
; If you are not using any formal verification in your Pact code, but you would
; still like Pact to verify that your types are correct, then you can use the
; (typecheck) function. This isn't necessary in our case, because (verify) will
; also typecheck the module.
; https://pact-language.readthedocs.io/en/latest/pact-functions.html#typecheck
; --------------------
; 3. Execute code from our contract and test the results
; --------------------
; We've come a long way. We've set up our pseudo-Chainweb environment. We've
; "deployed" and formally verified our contract. Our test environment is fully
; set up: our faucet is funded and ready to begin sending funds. We've enabled
; metrics so we can measure how much gas is consumed by our contract functions.
; Let's start testing!
;
; Here's how our faucet works:
;
; 1. Anyone can ask for KDA from the faucet, but the faucet must sign the tx.
; 2. By default, the faucet will send a maximum of 20 KDA in a single call to
; (request-funds) and a maximum of 100 KDA per address.
; 3. The per-request and per-account limits can be raised by the faucet using
; the (set-account-limit) and (set-request-limit) functions.
; 4. Accounts can return funds to the faucet with (return-funds), which will
; credit against their limits.
; 5. Account limits can be checked at any time with (get-limits)
;
; Our tests should verify this behaviour and serve as an example of how to call
; functions in our contract.
; We'll quickly register a keyset for our test user for ease of use in our tests.
; We'll then use this user account to request and return funds.
(env-data { "user-keyset": { "keys": [ "user-public-key" ], "pred": "keys-all" } })
(begin-tx)
(namespace "free")
(define-keyset "free.user-keyset" (read-keyset "user-keyset"))
(commit-tx)
(env-data {})
; Let's set a gaslimit of 150_000 to simulate everything in this REPL file
; happening in the space of a single block. (Chainweb nodes reject transactions
; over this gas amount.)
(env-gaslimit 150000)
; First we try requesting funds. Recall that the (request-funds) function relies
; on the (coin.TRANSFER) capability because it calls the (coin.transfer-create)
; function, and therefore we must scope the faucet's signature to this capability
; as part of the transaction.
;
; Below, the goliath faucet keys sign the transaction, and the signature is
; scoped to the TRANSFER capability. The capability takes three arguments to
; restrict how much authorization is being granted: the account to transfer
; from, the account to transfer to, and the maximum amount that can be
; transferred. If the Pact code in the transaction tries to transfer to a
; different account or tries to transfer a larger amount, then the capability
; will not be granted and the transaction will fail. Try it for yourself – see
; what happens if you comment out the below line.
(env-sigs [ { "key": "goliath-faucet-public-key", "caps": [ (coin.TRANSFER "goliath-faucet" "user" 1000.0) ] } ])
(begin-tx)
(free.goliath-faucet.request-funds "user" (describe-keyset "free.user-keyset") 20.0)
; Great – we've requested funds for the "user" account! It began with no funds
; at all, so let's let's verify this worked by checking that this account now
; has 20 KDA.
;
; We can use the (expect), (expect-that), and (expect-failure) functions to make
; test assertions in our REPL code:
; https://pact-language.readthedocs.io/en/latest/pact-functions.html#expect
; https://pact-language.readthedocs.io/en/latest/pact-functions.html#expect-that
; https://pact-language.readthedocs.io/en/latest/pact-functions.html#expect-failure
;
; The (coin.get-balance) function records how much KDA a particular account
; has, or fails with an error if the account does not exist. We can use it
; to verify we did indeed create the user account.
(expect "User account has been created and has 20 KDA." 20.0 (coin.get-balance "user"))
(expect "Faucet has spent 20 KDA and has 980 KDA remaining." 980.0 (coin.get-balance "goliath-faucet"))
(commit-tx)
(env-sigs [])
; We now know that requesting funds works. What about our restrictions? What
; happens if you exceed the per-request limit? In the below test, we try to
; request more funds in a single call than we're allowed to, and then we raise
; the per-request limit for the user account, and ensure that our previously
; over-the-limit request succeeds.
;
; Recall that the (free.goliath.set-request-limit) function is guarded by the
; SET_LIMIT capability; we need to scope the faucet's signature to that
; capability in order for the transaction to succeed.
(env-sigs [ { "key": "goliath-faucet-public-key", "caps": [ (coin.TRANSFER "goliath-faucet" "user" 1000.0), (free.goliath-faucet.SET_LIMIT) ] } ])
(begin-tx)
; First, let's verify that exceeding the per-request limit fails, as expected.
(expect-failure "80.0 would exceed per-request limit." (free.goliath-faucet.request-funds "user" (describe-keyset "free.user-keyset") 80.0))
; Then, let's adjust the per-request limit to be higher than the default 20.0.
; This time, we'll use (env-gaslog) to measure the gas consumption of the
; (set-request-limit) function. We can do this because we set up gas metrics in
; the faucet.setup.repl file.
; https://pact-language.readthedocs.io/en/stable/pact-functions.html#env-gaslog
(env-gaslog)
(free.goliath-faucet.set-request-limit "user" 200.0)
(env-gaslog)
; We can measure how much gas a call to (set-request-limit) takes.
(let (( expected-gas 314 ))
(expect (format "Setting request limit costs {} gas" [expected-gas]) expected-gas (env-gas)))
; And we can verify that the limit was indeed raised by calling (get-limits)
(expect "User request limit has been raised to 200.0." { "account-limit": 100.0, "request-limit": 200.0, "account-limit-remaining": 80.0 } (free.goliath-faucet.get-limits "user"))
; Now our request for 80.0 KDA should succeed.
(free.goliath-faucet.request-funds "user" (describe-keyset "free.user-keyset") 80.0)
(expect "User account has 100.0 KDA." 100.0 (coin.get-balance "user"))
(commit-tx)
(env-sigs [])
; Now, let's implement the same test, except this time we'll reach the per-
; account limit, then raise the limit, then successfully request funds over the
; prior limit. This time, we'll also record how much a call to (request-funds)
; costs in terms of gas.
(env-sigs [ { "key": "goliath-faucet-public-key", "caps": [ (coin.TRANSFER "goliath-faucet" "user" 1000.0), (free.goliath-faucet.SET_LIMIT) ] } ])
(begin-tx)
(expect-failure "Would exceed per-account limit." (free.goliath-faucet.request-funds "user" (describe-keyset "free.user-keyset") 1.0))
(free.goliath-faucet.set-account-limit "user" 200.0)
(expect "User account limit has been raised to 200.0." { "account-limit": 200.0, "request-limit": 200.0, "account-limit-remaining": 100.0 } (free.goliath-faucet.get-limits "user"))
(env-gaslog)
(free.goliath-faucet.request-funds "user" (describe-keyset "free.user-keyset") 100.0)
(env-gaslog)
(let (( expected-gas 1116 ))
(expect (format "Requesting funds costs {} gas" [expected-gas]) expected-gas (env-gas)))
(expect "User account has 200.0 KDA." 200.0 (coin.get-balance "user"))
(commit-tx)
(env-sigs [])
; Finally, let's verify that we can return funds to the faucet and it will
; credit back to our overall limits.
(env-sigs [ { "key": "user-public-key", "caps": [ (coin.TRANSFER "user" "goliath-faucet" 1000.0) ] } ])
(begin-tx)
(expect "User account has reached its account limit." { "account-limit": 200.0, "request-limit": 200.0, "account-limit-remaining": 0.0 } (free.goliath-faucet.get-limits "user"))
(free.goliath-faucet.return-funds "user" 50.0)
(expect "User account limit is no longer reached." { "account-limit": 200.0, "request-limit": 200.0, "account-limit-remaining": 50.0 } (free.goliath-faucet.get-limits "user"))
(commit-tx)
(env-sigs [])
; And that's that! Our faucet.repl file formally verifies our contract, performs
; unit tests, and serves as a concrete demonstration of calling functions from
; our contract. We've also estimated the gas cost of various functions from the
; contract. I encourage you to tweak the contract to see how the gas consumption
; changes.
;
; If you haven't yet used the request files to interact with a deployed version
; of the contract on a simulation of Chainweb, you should do that next. If you
; have, then you're ready to move on to the Goliath wallet frontend!