|
| 1 | +# Complete Contract Walk-Through |
| 2 | + |
| 3 | +Let's look at the contract from [the basic dapp](../getting-started/) in some detail. |
| 4 | + |
| 5 | +## Bundling a Contract |
| 6 | + |
| 7 | +In [deploying the basic dapp contract](../getting-started/#starting-the-dapp-smart-contract), |
| 8 | +the first step was to _bundle_ all of its modules into a single artifact. |
| 9 | +We used the [agoric run](../agoric-cli/#agoric-run) command in that case. |
| 10 | +The core mechanism used in `agoric run` is a call to `bundleSource()`. |
| 11 | + |
| 12 | +In the `contract` directory of the dapp, |
| 13 | +run `test-bundle-source.js` following `ava` conventions: |
| 14 | + |
| 15 | +```sh |
| 16 | +cd contract |
| 17 | +yarn ava test/test-bundle-source.js |
| 18 | +``` |
| 19 | + |
| 20 | +The results look something like... |
| 21 | + |
| 22 | +```console |
| 23 | + ✔ bundleSource() bundles the contract for use with zoe (2.7s) |
| 24 | + ℹ 1e1aeca9d3ebc0bd39130fe5ef6fbb077177753563db522d6623886da9b43515816df825f7ebcb009cbe86dcaf70f93b9b8595d1a87c2ab9951ee7a32ad8e572 |
| 25 | + ℹ Object @Alleged: BundleInstallation {} |
| 26 | + ─ |
| 27 | + |
| 28 | + 1 test passed |
| 29 | +``` |
| 30 | + |
| 31 | +::: details Test Setup |
| 32 | + |
| 33 | +The test uses `createRequire` from the node `module` API to resolve the main module specifier: |
| 34 | + |
| 35 | +<<< @/../snippets/zoe/contracts/test-bundle-source.js#bundleSourceImports |
| 36 | + |
| 37 | +<<< @/../snippets/zoe/contracts/test-bundle-source.js#contractPath |
| 38 | +::: |
| 39 | + |
| 40 | +`bundleSource()` returns a bundle object with `moduleFormat`, a hash, and the contents: |
| 41 | + |
| 42 | +<<< @/../snippets/zoe/contracts/test-bundle-source.js#testBundleSource{1} |
| 43 | + |
| 44 | +::: details Getting the zip file from inside a bundle |
| 45 | + |
| 46 | +An endo bundle is a zip file inside JSON. To get it back out: |
| 47 | + |
| 48 | +```sh |
| 49 | +jq -r .endoZipBase64 bundle-xyz.json | base64 -d >xyz.zip |
| 50 | +``` |
| 51 | + |
| 52 | +You can then, for example, look at its contents: |
| 53 | + |
| 54 | +```sh |
| 55 | +unzip -l xyz.zip |
| 56 | +``` |
| 57 | + |
| 58 | +::: |
| 59 | + |
| 60 | +## Contract Installation |
| 61 | + |
| 62 | +To identify the code of contracts that parties consent to participate in, Zoe |
| 63 | +uses _Installation_ objects. |
| 64 | + |
| 65 | +Let's try it with the contract from our [basic dapp](../getting-started/): |
| 66 | + |
| 67 | +```sh |
| 68 | +yarn ava test/test-contract.js -m 'Install the contract' |
| 69 | +``` |
| 70 | + |
| 71 | +``` |
| 72 | + ✔ Install the contract |
| 73 | + ℹ Object @Alleged: BundleInstallation {} |
| 74 | +``` |
| 75 | + |
| 76 | +::: details Test Setup |
| 77 | + |
| 78 | +The test starts by using `makeZoeKitForTest` to set up zoe for testing: |
| 79 | + |
| 80 | +<<< @/../snippets/zoe/contracts/test-bundle-source.js#importZoeForTest |
| 81 | + |
| 82 | +```js |
| 83 | +const { zoeService: zoe } = makeZoeKitForTest(); |
| 84 | +``` |
| 85 | + |
| 86 | +::: |
| 87 | + |
| 88 | +It gets an installation using a bundle as in the previous section: |
| 89 | + |
| 90 | +```js{1} |
| 91 | +const installation = await E(zoe).install(bundle); |
| 92 | +t.log(installation); |
| 93 | +t.is(typeof installation, 'object'); |
| 94 | +``` |
| 95 | + |
| 96 | +The `installation` identifies the basic contract that we'll |
| 97 | +go over in detail in the sections below. |
| 98 | + |
| 99 | +::: details gameAssetContract.js listing |
| 100 | + |
| 101 | +<<< @/../snippets/zoe/src/gameAssetContract.js#file |
| 102 | + |
| 103 | +::: |
| 104 | + |
| 105 | +## Starting a Contract Instance |
| 106 | + |
| 107 | +Now we're ready to start an _instance_ of the [basic dapp](../getting-started/) contract: |
| 108 | + |
| 109 | +```sh |
| 110 | +yarn ava test/test-contract.js -m 'Start the contract' |
| 111 | +``` |
| 112 | + |
| 113 | +``` |
| 114 | + ✔ Start the contract (652ms) |
| 115 | + ℹ terms: { |
| 116 | + joinPrice: { |
| 117 | + brand: Object @Alleged: PlayMoney brand {}, |
| 118 | + value: 5n, |
| 119 | + }, |
| 120 | + } |
| 121 | + ℹ Object @Alleged: InstanceHandle {} |
| 122 | +``` |
| 123 | + |
| 124 | +Contracts can be parameterized by _terms_. |
| 125 | +The price of joining the game is not fixed in the source code of this contract, |
| 126 | +but rather chosen when starting an instance of the contract. |
| 127 | +Likewise, when starting an instance, we can choose which asset _issuers_ |
| 128 | +the contract should use for its business: |
| 129 | + |
| 130 | +```js{8} |
| 131 | +const money = makeIssuerKit('PlayMoney'); |
| 132 | +const issuers = { Price: money.issuer }; |
| 133 | +const terms = { joinPrice: AmountMath.make(money.brand, 5n) }; |
| 134 | +t.log('terms:', terms); |
| 135 | +
|
| 136 | +/** @type {ERef<Installation<GameContractFn>>} */ |
| 137 | +const installation = E(zoe).install(bundle); |
| 138 | +const { instance } = await E(zoe).startInstance(installation, issuers, terms); |
| 139 | +t.log(instance); |
| 140 | +t.is(typeof instance, 'object'); |
| 141 | +``` |
| 142 | + |
| 143 | +_`makeIssuerKit` and `AmountMath.make` are covered in the [ERTP](../ertp/) section, along with `makeEmptyPurse`, `mintPayment`, and `getAmountOf` below._ |
| 144 | + |
| 145 | +_See also [E(zoe).startInstance(...)](/reference/zoe-api/zoe.md#e-zoe-startinstance-installation-issuerkeywordrecord-terms-privateargs)._ |
| 146 | + |
| 147 | +Let's take a look at what happens in the contract when it starts. A _facet_ of Zoe, the _Zoe Contract Facet_, is passed to the contract `start` function. |
| 148 | +The contract uses this `zcf` to get its terms. Likewise it uses `zcf` to |
| 149 | +make a `gameSeat` where it can store assets that it receives in trade |
| 150 | +as well as a `mint` for making assets consisting of collections (bags) of Places: |
| 151 | + |
| 152 | +<<< @/../snippets/zoe/src/gameAssetContract.js#start |
| 153 | + |
| 154 | +It defines a `joinShape` and `joinHandler` but doesn't do anything with them yet. They will come into play later. It defines and returns its `publicFacet` and stands by. |
| 155 | + |
| 156 | +<<< @/../snippets/zoe/src/gameAssetContract.js#started |
| 157 | + |
| 158 | +## Trading with Offer Safety |
| 159 | + |
| 160 | +Our [basic dapp](../getting-started/) includes a test of trading: |
| 161 | + |
| 162 | +```sh |
| 163 | +yarn ava test/test-contract.js -m 'Alice trades*' |
| 164 | +``` |
| 165 | + |
| 166 | +``` |
| 167 | + ✔ Alice trades: give some play money, want some game places (674ms) |
| 168 | + ℹ Object @Alleged: InstanceHandle {} |
| 169 | + ℹ Alice gives { |
| 170 | + Price: { |
| 171 | + brand: Object @Alleged: PlayMoney brand {}, |
| 172 | + value: 5n, |
| 173 | + }, |
| 174 | + } |
| 175 | + ℹ Alice payout brand Object @Alleged: Place brand {} |
| 176 | + ℹ Alice payout value Object @copyBag { |
| 177 | + payload: [ |
| 178 | + [ |
| 179 | + 'Park Place', |
| 180 | + 1n, |
| 181 | + ], |
| 182 | + [ |
| 183 | + 'Boardwalk', |
| 184 | + 1n, |
| 185 | + ], |
| 186 | + ], |
| 187 | + } |
| 188 | +``` |
| 189 | + |
| 190 | +We start by putting some money in a purse for Alice: |
| 191 | + |
| 192 | +```js{4} |
| 193 | +const alicePurse = money.issuer.makeEmptyPurse(); |
| 194 | +const amountOfMoney = AmountMath.make(money.brand, 10n); |
| 195 | +const moneyPayment = money.mint.mintPayment(amountOfMoney); |
| 196 | +alicePurse.deposit(moneyPayment); |
| 197 | +``` |
| 198 | + |
| 199 | +Then we pass the contract instance and the purse to our code for `alice`: |
| 200 | + |
| 201 | +```js |
| 202 | +await alice(t, zoe, instance, alicePurse); |
| 203 | +``` |
| 204 | + |
| 205 | +Alice starts by using the `instance` to get the contract's `publicFacet` and `terms` from Zoe: |
| 206 | + |
| 207 | +<img src="./assets/trade-offer-safety-1.svg" |
| 208 | + style="border: 2px solid" width="600" /> |
| 209 | + |
| 210 | +<<< @/../snippets/zoe/contracts/alice-trade.js#queryInstance |
| 211 | + |
| 212 | +Then she constructs a _proposal_ to give the `joinPrice` in exchange |
| 213 | +for 1 Park Place and 1 Boardwalk, denominated in the game's `Place` brand; and she withdraws a payment from her purse: |
| 214 | + |
| 215 | +<<< @/../snippets/zoe/contracts/alice-trade.js#makeProposal |
| 216 | + |
| 217 | +She then requests an _invitation_ to join the game; makes an _offer_ with |
| 218 | +(a promise for) this invitation, her proposal, and her payment; |
| 219 | +and awaits her **Places** payout: |
| 220 | + |
| 221 | +<img src="./assets/trade-offer-safety-2.svg" |
| 222 | + style="border: 2px solid" width="600" /> |
| 223 | + |
| 224 | +<<< @/../snippets/zoe/contracts/alice-trade.js#trade |
| 225 | + |
| 226 | +::: details Troubleshooting missing brands in offers |
| 227 | + |
| 228 | +If you see... |
| 229 | + |
| 230 | +``` |
| 231 | +Error#1: key Object [Alleged: IST brand] {} not found in collection brandToIssuerRecord |
| 232 | +``` |
| 233 | + |
| 234 | +then it may be that your offer uses brands that are not known to the contract. |
| 235 | +Use [E(zoe).getTerms()](/reference/zoe-api/zoe#e-zoe-getterms-instance) to find out what issuers |
| 236 | +are known to the contract. |
| 237 | + |
| 238 | +If you're writing or instantiating the contract, you can tell the contract about issuers |
| 239 | +when you are [creating an instance](#starting-a-contract-instance) or by using |
| 240 | +[zcf.saveIssuer()](/reference/zoe-api/zoe-contract-facet#zcf-saveissuer-issuer-keyword). |
| 241 | + |
| 242 | +::: |
| 243 | + |
| 244 | +The contract gets Alice's `E(publicFacet).makeJoinInvitation()` call and uses `zcf` to make an invitation with an associated handler, description, and proposal shape. Zoe gets Alice's `E(zoe).offer(...)` call, checks the proposal against the proposal shape, escrows the payment, and invokes the handler. |
| 245 | + |
| 246 | +<img src="./assets/trade-offer-safety-3.svg" |
| 247 | + style="border: 2px solid" width="600" /> |
| 248 | + |
| 249 | +<<< @/../snippets/zoe/src/gameAssetContract.js#makeInvitation |
| 250 | + |
| 251 | +The offer handler is invoked with a _seat_ representing the party making the offer. |
| 252 | +It extracts the `give` and `want` from the party's offer and checks that |
| 253 | +they are giving at least the `joinPrice` and not asking for too many |
| 254 | +places in return. |
| 255 | + |
| 256 | +With all these prerequisites met, the handler instructs `zcf` to mint the requested |
| 257 | +**Place** assets, allocate what the player is giving into its own `gameSeat`, |
| 258 | +and allocate the minted places to the player. Finally, it concludes its business with the player. |
| 259 | + |
| 260 | +<img src="./assets/trade-offer-safety-4.svg" |
| 261 | + style="border: 2px solid" width="600" /> |
| 262 | + |
| 263 | +<<< @/../snippets/zoe/src/gameAssetContract.js#handler |
| 264 | + |
| 265 | +Zoe checks that the contract's instructions are consistent with |
| 266 | +the offer and with conservation of assets. Then it allocates |
| 267 | +the escrowed payment to the contract's gameSeat and pays out |
| 268 | +the place NFTs to Alice in response to the earlier `getPayout(...)` call. |
| 269 | + |
| 270 | +Alice asks the `Place` issuer what her payout is worth |
| 271 | +and tests that it's what she wanted. |
| 272 | + |
| 273 | +<img src="./assets/trade-offer-safety-5.svg" |
| 274 | + style="border: 2px solid" width="600" /> |
| 275 | + |
| 276 | +<<< @/../snippets/zoe/contracts/alice-trade.js#payouts |
0 commit comments