diff --git a/main/.vuepress/config.js b/main/.vuepress/config.js index 4ad254db7..5313ad6ec 100644 --- a/main/.vuepress/config.js +++ b/main/.vuepress/config.js @@ -175,6 +175,20 @@ module.exports = { '/guides/getting-started/syncing-up.html', ], }, + { + title: 'Zoe', + path: '/guides/zoe/', + collapsible: false, + children: [ + '/guides/zoe/contract-basics.html', + '/guides/zoe/', + '/guides/zoe/offer-enforcement.html', + '/guides/zoe/offer-safety.html', + '/guides/zoe/proposal.html', + '/guides/zoe/contract-requirements.html', + '/guides/zoe/price-authority.html', + ], + }, { title: 'Agoric CLI', path: '/guides/agoric-cli/', @@ -214,19 +228,6 @@ module.exports = { '/guides/ertp/purses-and-payments.html', ], }, - { - title: 'Zoe', - path: '/guides/zoe/', - collapsible: false, - children: [ - '/guides/zoe/', - '/guides/zoe/offer-enforcement.html', - '/guides/zoe/offer-safety.html', - '/guides/zoe/proposal.html', - '/guides/zoe/contract-requirements.html', - '/guides/zoe/price-authority.html', - ], - }, { title: 'Permissioned Contract Deployment', path: '/guides/coreeval/', @@ -281,6 +282,8 @@ module.exports = { '/guides/dapps/', '/guides/dapps/dapp-templates.html', '/guides/dapps/starting-multiuser-dapps.html', + '/guides/getting-started/deploying.html', + '/guides/getting-started/contract-rpc.html', ], }, { diff --git a/main/guides/coreeval/README.md b/main/guides/coreeval/README.md index 8b47c3855..eaaf13a05 100644 --- a/main/guides/coreeval/README.md +++ b/main/guides/coreeval/README.md @@ -1,7 +1,7 @@ # Permissioned Contract Deployment Until mainnet enters the Mainnet-3 phase of the [multi-phase mainnet rollout](https://agoric.com/blog/announcements/mainnet-phase-0-launch), -permissionless [contract installation with Zoe](/guides/zoe/#writing-and-installing-a-contract) +permissionless [contract installation with Zoe](/guides/zoe/#contract-installation) is limited to development environments. Until then, permission to deploy contracts can be granted using an Agoric extension to [Cosmos SDK Governance](https://hub.cosmos.network/main/delegators/delegator-guide-cli.html#participating-in-governance) called `swingset.CoreEval`. As discussed in [governance using Hardened JavaScript: swingset\.CoreEval](https://community.agoric.com/t/bld-staker-governance-using-hardened-javascript-swingset-coreeval/99), @@ -12,6 +12,3 @@ To do try it out in a local testnet chain: 1. [Declare the capabilities that the proposal will require](./permissions.md). 2. [Code the proposal itself](./proposal.md). 3. [Deploy the proposal to a local testnet](./local-testnet.md). - - - diff --git a/main/guides/zoe/README.md b/main/guides/zoe/README.md index 9d63c9cea..d955344f6 100644 --- a/main/guides/zoe/README.md +++ b/main/guides/zoe/README.md @@ -2,24 +2,24 @@ ## What is Zoe? -Zoe is Agoric's smart contract framework. Use Zoe to: +Zoe is the heart of Agoric's smart contract framework. Use Zoe to: -* **Run your code on-chain** -* **Mint new digital assets** -* **Credibly trade assets** +- **Run your code on-chain** +- **Mint new digital assets** +- **Credibly trade assets** ## Why Use Zoe? -### For Users ### +### For Users **Zoe is safer.** Traditionally, putting digital assets in a smart contract has carried the risk of losing them. But Zoe guarantees you get either what you wanted or a full refund of the assets you put in. You will never leave a smart contract empty-handed, even if it is buggy or malicious. -### For Developers ### +### For Developers -**Zoe is easier.** Traditionally, writing a smart contract meant +**Zoe is easier.** Traditionally, writing a smart contract meant learning a new, untried language. And don't make any mistakes - if you do, your users might lose millions. @@ -28,253 +28,296 @@ Moreover, Zoe automatically escrows all user digital assets and handles their subsequent payout. **Even a buggy contract can't cause users to lose their assets.** -### Contracts on Zoe +::: warning TODO tone down the claims above -Agoric has written [a number of example contracts that you can -use](/zoe/guide/contracts/), including: -* an [Automated Market Maker (AMM) - implementation](/zoe/guide/contracts/constantProductAMM.md) -* a [covered call option contract](./contracts/covered-call.md) -* an [OTC Desk market maker contract](./contracts/otc-desk.md) -* contracts for [minting fungible](./contracts/mint-payments.md) and - [non-fungible tokens](./contracts/mint-and-sell-nfts.md) +::: -## Using an Example Zoe Smart Contract +## Bundling a Contract -You must have a Zoe invitation to a specific contract instance to join -and participate in it. Let's imagine your friend Alice has sent an -invitation for a contract instance to your [wallet](/guides/wallet/). +In [deploying the basic dapp contract](../getting-started/#starting-the-dapp-smart-contract), +the first step was to _bundle_ all of its modules into a single artifact. +We used the [agoric run](../agoric-cli/#agoric-run) command in that case. +The core mechanism used in `agoric run` is a call to `bundleSource()`. -Compare this to a smart contract on Ethereum. On Ethereum, the smart -contract developer must guard against malicious calls and store an -internal access control list to check whether the message sender is -allowed to send such a message. Zoe, built on Agoric's [object -capability](/glossary/#object-capabilities) security model, is just -easier. +In the `contract` directory of the dapp, +run `test-bundle-source.js` following `ava` conventions: -This particular invitation is for an [Atomic Swap -contract](/zoe/guide/contracts/atomic-swap.md). -In an Atomic Swap, one party puts up digital assets they want to -exchange and sends an invitation to a second party for them to -possibly complete the exchange. In this example, Alice has already -escrowed the assets she wants to swap and is asking you to pay a -specified price to receive her digital assets. +```sh +cd contract +yarn ava test/test-bundle-source.js +``` -### Inspecting an Invitation +The results look something like... -So you have an invitation, but how do you use it? First, you use Zoe -to inspect and validate the invitation. +```console + ✔ bundleSource() bundles the contract for use with zoe (2.7s) + ℹ 1e1aeca9d3ebc0bd39130fe5ef6fbb077177753563db522d6623886da9b43515816df825f7ebcb009cbe86dcaf70f93b9b8595d1a87c2ab9951ee7a32ad8e572 + ℹ Object @Alleged: BundleInstallation {} + ─ + + 1 test passed +``` -<<< @/snippets/test-intro-zoe.js#details +::: details Test Setup -::: warning Note +The test uses `createRequire` from the node `module` API to resolve the main module specifier: -E() is part of the Agoric platform and is used to [call methods on -remote objects and receive a promise for the -result](/guides/js-programming/eventual-send.md). -Code on the Agoric platform is put in separate environments, called -[vats](/glossary/#vat), for security. Zoe is a remote object in its own vat, -so we must use E(). +<<< @/snippets/zoe/contracts/test-bundle-source.js#bundleSourceImports + +<<< @/snippets/zoe/contracts/test-bundle-source.js#contractPath ::: -Invitations include information about their contract's installation. -Essentially, this is the contract's source code as installed on Zoe. -From this overall contract installation, people use Zoe to create and -run specific instances of the contract. For example, if a real estate -company has a contract for selling a house, they would create an -instance of the contract for each individual house they have up for -sale. +`bundleSource()` returns a bundle object with `moduleFormat`, a hash, and the contents: + +<<< @/snippets/zoe/contracts/test-bundle-source.js#testBundleSource{1} -You use object identity comparison to quickly check that you recognize -this contract installation, without having to compare source code -line-by-line. If the installation matches, you're -sure the invitation is for participating in an instance of the -expected contract rather than an unknown and possibly malicious one. +::: details Getting the zip file from inside a bundle -<<< @/snippets/test-intro-zoe.js#isCorrectCode +An endo bundle is a zip file inside JSON. To get it back out: -However, if you don't recognize the installation, you can inspect its -code directly by calling: +```sh +jq -r .endoZipBase64 bundle-xyz.json | base64 -d >xyz.zip +``` -<<< @/snippets/test-intro-zoe.js#inspectCode +You can then, for example, look at its contents: -In most cases, the bundle contains a base64-encoded zip file that you can -extract for review: ```sh -echo "$endoZipBase64" | base64 -d > bundle.zip -unzip bundle.zip +unzip -l xyz.zip ``` -Contracts can add their own specific information to invitations. In -this case, the Atomic Swap contract adds information about what is -being traded: the `asset` [amount](/guides/ertp/amounts.md#amounts) -Alice has escrowed, and the `price` amount that you must pay to get it. -Note that both are _descriptions_ of digital assets with no intrinsic value of their own. +::: + +## Contract Installation + +To identify the code of contracts that parties consent to participate in, Zoe +uses _Installation_ objects. + +Let's try it with the contract from our [basic dapp](../getting-started/): -### Making an Offer +```sh +yarn ava test/test-contract.js -m 'Install the contract' +``` -You've successfully checked out the invitation, so now you can make an -offer. +``` + ✔ Install the contract + ℹ Object @Alleged: BundleInstallation {} +``` -An offer has three required parts: -* a Zoe invitation -* a proposal -* a [payment](/guides/ertp/purses-and-payments.md#payments) containing - the digital assets you're offering to swap +::: details Test Setup -The `proposal` states what you want from the offer, and what you will -give in return. Zoe uses the proposal as an invariant to ensure you -don't lose your assets in the trade. This invariant is known as **offer -safety**. +The test starts by using `makeZoeKitForTest` to set up zoe for testing: -You use the invitation's `asset` and `price` amounts to make your -proposal. Let's say `asset` is an amount of 3 Moola, and `price` is an amount -of 7 Simoleans (Moola and Simoleans are made-up currencies for this example). +<<< @/snippets/zoe/contracts/test-bundle-source.js#importZoeForTest -<<< @/snippets/test-intro-zoe.js#ourProposal +```js +const { zoeService: zoe } = makeZoeKitForTest(); +``` -Proposals must use Keywords, which are -[identifier](https://developer.mozilla.org/en-US/docs/Glossary/Identifier) -properties that start with an upper case letter and contain no non-ASCII characters. -Here, the specific keywords, `Asset` and `Price`, are [determined by the -contract code](/zoe/guide/contracts/atomic-swap.md). +::: -You said you would give 7 Simoleans, so you must send 7 Simoleans as a payment. -You happen to have some Simoleans lying around in a Simolean -[purse](/guides/ertp/purses-and-payments.md) (used to hold digital -assets of a specific type). You withdraw a payment of 7 Simoleans from -the purse for your offer, and construct an object using the same -Keyword as your `proposal.give`: +It gets an installation using a bundle as in the previous section: -<<< @/snippets/test-intro-zoe.js#getPayments +```js{1} +const installation = await E(zoe).install(bundle); +t.log(installation); +t.is(typeof installation, 'object'); +``` -Now you need to [harden](https://github.com/endojs/endo/blob/HEAD/packages/ses/docs/guide.md) your -just created `proposal` and `payments` objects. Hardening is -transitively freezing an object. For security reasons, we must harden -any objects that will be passed to a remote object like Zoe. +The `installation` identifies the basic contract that we'll +go over in detail in the sections below. -<<< @/snippets/test-intro-zoe.js#harden +::: details gameAssetContract.js listing -You've put the required pieces together, so now you can make an offer: +<<< @/snippets/zoe/src/gameAssetContract.js#file -<<< @/snippets/test-intro-zoe.js#offer +::: -At this point, Zoe confirms your invitation's validity and [burns](/glossary/#burn) it. -Zoe also escrows your payments, representing their value as -amounts in your **[Allocation](/reference/zoe-api/zoe-data-types.md#allocation)** -in the contract. +## Starting a Contract Instance -::: tip Troubleshooting missing brands in offers +Now we're ready to start an _instance_ of the [basic dapp](../getting-started/) contract: -If you see... +```sh +yarn ava test/test-contract.js -m 'Start the contract' +``` ``` -Error#1: key Object [Alleged: IST brand] {} not found in collection brandToIssuerRecord + ✔ Start the contract (652ms) + ℹ terms: { + joinPrice: { + brand: Object @Alleged: PlayMoney brand {}, + value: 5n, + }, + } + ℹ Object @Alleged: InstanceHandle {} ``` -then it may be that your offer uses brands that are not known to the contract. -Use [E(zoe).getTerms()](/reference/zoe-api/zoe.md#e-zoe-getterms-instance) to find out what issuers -are known to the contract. +Contracts can be parameterized by _terms_. +The price of joining the game is not fixed in the source code of this contract, +but rather chosen when starting an instance of the contract. +Likewise, when starting an instance, we can choose which asset _issuers_ +the contract should use for its business: + +```js{8} +const money = makeIssuerKit('PlayMoney'); +const issuers = { Price: money.issuer }; +const terms = { joinPrice: AmountMath.make(money.brand, 5n) }; +t.log('terms:', terms); + +/** @type {ERef>} */ +const installation = E(zoe).install(bundle); +const { instance } = await E(zoe).startInstance(installation, issuers, terms); +t.log(instance); +t.is(typeof instance, 'object'); +``` -If you're writing or instantiating the contract, you can tell the contract about issuers -when you are [creating an instance](#creating-an-instance) or by using -[zcf.saveIssuer()](/reference/zoe-api/zoe-contract-facet.md#zcf-saveissuer-issuer-keyword). +_`makeIssuerKit` and `AmountMath.make` are covered in the [ERTP](../ertp/) section, along with `makeEmptyPurse`, `mintPayment`, and `getAmountOf` below._ -::: +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. +The contract uses this `zcf` to get its terms. Likewise it uses `zcf` to +make a `gameSeat` where it can store assets that it receives in trade +as well as a `mint` for making assets consisting of collections (bags) of Places: -### Using Your UserSeat +<<< @/snippets/zoe/src/gameAssetContract.js#start -Making an offer as a user returns a [UserSeat](/reference/zoe-api/user-seat.md) -representing your position in the ongoing contract instance (your -"seat at the table"). You can use this seat to: +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. -1. Exit the contract. -2. Get information about your position such as your current allocation. -3. Get your payouts from Zoe. +<<< @/snippets/zoe/src/gameAssetContract.js#started -Check that your offer was successful: +## Trading with Offer Safety -<<< @/snippets/test-intro-zoe.js#offerResult +Our [basic dapp](../getting-started/) includes a test of trading: -In response to your offer, the `atomicSwap` contract returns the -message: "The offer has been accepted. Once the contract has been -completed, please check your payout." Other contracts and offers may -return something different. The offer's result is entirely up to the -contract. +```sh +yarn ava test/test-contract.js -m 'Alice trades*' +``` -### Getting Payouts +``` + ✔ Alice trades: give some play money, want some game places (674ms) + ℹ Object @Alleged: InstanceHandle {} + ℹ Alice gives { + Price: { + brand: Object @Alleged: PlayMoney brand {}, + value: 5n, + }, + } + ℹ Alice payout brand Object @Alleged: Place brand {} + ℹ Alice payout value Object @copyBag { + payload: [ + [ + 'Park Place', + 1n, + ], + [ + 'Boardwalk', + 1n, + ], + ], + } +``` + +We start by putting some money in a purse for Alice: -The `atomicSwap` contract of this example is over once the second -party escrows the correct assets. You can get your payout of Moola -with the Keyword you used ('Asset'): +```js{4} +const alicePurse = money.issuer.makeEmptyPurse(); +const amountOfMoney = AmountMath.make(money.brand, 10n); +const moneyPayment = money.mint.mintPayment(amountOfMoney); +alicePurse.deposit(moneyPayment); +``` -<<< @/snippets/test-intro-zoe.js#getPayout +Then we pass the contract instance and the purse to our code for `alice`: -Alice also receives her payouts: +```js +await alice(t, zoe, instance, alicePurse); +``` -
+Alice starts by using the `instance` to get the contract's `publicFacet` and `terms` from Zoe: -<<< @/snippets/test-intro-zoe.js#alicePayout + -
+<<< @/snippets/zoe/contracts/alice-trade.js#queryInstance -## Writing and Installing a Contract +Then she constructs a _proposal_ to give the `joinPrice` in exchange +for 1 Park Place and 1 Boardwalk, denominated in the game's `Place` brand; and she withdraws a payment from her purse: -Now that you've seen how to participate in a contract instance, let's -look at how you'd create a contract and its instances. +<<< @/snippets/zoe/contracts/alice-trade.js#makeProposal -Let's pretend Alice wrote that contract from scratch, even though -`atomicSwap` is one of Agoric's example contracts (see [Atomic Swap](./contracts/atomic-swap.md)). -Note: All Zoe contracts must have this format: +She then requests an _invitation_ to join the game; makes an _offer_ with +(a promise for) this invitation, her proposal, and her payment; +and awaits her **Places** payout: + + + +<<< @/snippets/zoe/contracts/alice-trade.js#trade + +::: details Troubleshooting missing brands in offers + +If you see... + +``` +Error#1: key Object [Alleged: IST brand] {} not found in collection brandToIssuerRecord +``` + +then it may be that your offer uses brands that are not known to the contract. +Use [E(zoe).getTerms()](/reference/zoe-api/zoe.md#e-zoe-getterms-instance) to find out what issuers +are known to the contract. + +If you're writing or instantiating the contract, you can tell the contract about issuers +when you are [creating an instance](#starting-a-contract-instance) or by using +[zcf.saveIssuer()](/reference/zoe-api/zoe-contract-facet.md#zcf-saveissuer-issuer-keyword). -::: details Show contract format -<<< @/snippets/contract-format.js#contractFormat ::: -Alice fills in this code template with `atomicSwap`'s particulars. -To install this particular code, Alice first must bundle it off-chain, -meaning the code and its imports are flattened together: +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. -<<< @/snippets/test-intro-zoe.js#importBundleSource + -<<< @/snippets/test-intro-zoe.js#bundle +<<< @/snippets/zoe/src/gameAssetContract.js#makeInvitation -Then Alice must install it on Zoe: +The offer handler is invoked with a _seat_ representing the party making the offer. +It extracts the `give` and `want` from the party's offer and checks that +they are giving at least the `joinPrice` and not asking for too many +places in return. -<<< @/snippets/test-intro-zoe.js#install +With all these prerequisites met, the handler instructs `zcf` to mint the requested +**Place** assets, allocate what the player is giving into its own `gameSeat`, +and allocate the minted places to the player. Finally, it concludes its business with the player. -The return value is an `installation`, which we saw -[earlier](#inspecting-an-invitation). It is an -object identifying a particular piece of code installed on Zoe. It can -be compared to other installations, and you can call -`E(atomicSwapInstallation).getBundle()` to see the code itself. + -### Creating an Instance +<<< @/snippets/zoe/src/gameAssetContract.js#handler -Now Alice uses the installation to create a new instance. She must -also tell Zoe about the ERTP issuers she wants to use, by specifying -their role with Keywords. Alice was escrowing Moola, so she uses the -keyword `Asset` to label the `moolaIssuer`. She wanted Simoleans, so -she uses the keyword `Price` to label the `simoleanIssuer`. +Zoe checks that the contract's instructions are consistent with +the offer and with conservation of assets. Then it allocates +the escrowed payment to the contract's gameSeat and pays out +the place NFTs to Alice in response to the earlier `getPayout(...)` call. -<<< @/snippets/test-intro-zoe.js#startInstance +Alice asks the `Place` issuer what her payout is worth +and tests that it's what she wanted. -Even the creator of a contract instance needs an invitation to -participate in it. Alice uses the returned `creatorInvitation` to -make an offer, from which she gets an invitation that can be sent to -the counter-party. + -<<< @/snippets/test-intro-zoe.js#aliceOffer +<<< @/snippets/zoe/contracts/alice-trade.js#payouts -## Zoe's Two Sides: Zoe Service and Zoe Contract Facet (ZCF) +## Example Contracts -You may have noticed the contract code's `start` method has a `zcf` -parameter. This is the Zoe Contract Facet. Zoe has two sides: the Zoe -Service, which you've seen users interact with, and the Zoe Contract -Facet (ZCF), which is accessible to the contract code. Note that users -have access to the Zoe Service, but do not have access to ZCF. -Contract code has access to ZCF *and* can get access to the Zoe -Service. +Agoric has written [a number of example contracts that you can +use](/zoe/guide/contracts/), including: + +- an [Automated Market Maker (AMM) + implementation](/zoe/guide/contracts/constantProductAMM.md) +- a [covered call option contract](./contracts/covered-call.md) +- an [OTC Desk market maker contract](./contracts/otc-desk.md) +- contracts for [minting fungible](./contracts/mint-payments.md) and + [non-fungible tokens](./contracts/mint-and-sell-nfts.md) + +::: warning Beta Features -To learn more about the Zoe Service, Zoe Contract Facet, and Zoe -Helper APIs, [see our Zoe API documentation](/reference/zoe-api/). +These contracts may depend on features from our Beta release +that are not available in mainnet. + +::: diff --git a/main/guides/zoe/assets/trade-offer-safety-1.mmd b/main/guides/zoe/assets/trade-offer-safety-1.mmd new file mode 100644 index 000000000..f0eb07c8b --- /dev/null +++ b/main/guides/zoe/assets/trade-offer-safety-1.mmd @@ -0,0 +1,18 @@ +sequenceDiagram + autonumber + + box aqua + actor A as Alice + end + + box darksalmon + participant Zoe + end + + box skyblue Contract Instance + participant C as gameAssetContract + end + + A-)Zoe: getPublicFacet(instance) + A-)+Zoe: getTerms(instance) + Zoe--)-A: { issuers, brands, joinPrice } diff --git a/main/guides/zoe/assets/trade-offer-safety-1.svg b/main/guides/zoe/assets/trade-offer-safety-1.svg new file mode 100644 index 000000000..7b70d2a53 --- /dev/null +++ b/main/guides/zoe/assets/trade-offer-safety-1.svg @@ -0,0 +1 @@ +Contract InstancegameAssetContractZoegameAssetContractZoeAlicegetPublicFacet(instance)1getTerms(instance)2{ issuers, brands, joinPrice }3Alice \ No newline at end of file diff --git a/main/guides/zoe/assets/trade-offer-safety-2.mmd b/main/guides/zoe/assets/trade-offer-safety-2.mmd new file mode 100644 index 000000000..51afaeff5 --- /dev/null +++ b/main/guides/zoe/assets/trade-offer-safety-2.mmd @@ -0,0 +1,18 @@ +sequenceDiagram + autonumber + + box aqua + actor A as Alice + end + + box darksalmon + participant Zoe + end + + box skyblue Contract + participant C as gameAssetContract + end + + A-)C: makeJoinInvitation() + A-)Zoe: offer(toJoin, proposal, { Price }) + A-)Zoe: E(seat).getPayout('Places') diff --git a/main/guides/zoe/assets/trade-offer-safety-2.svg b/main/guides/zoe/assets/trade-offer-safety-2.svg new file mode 100644 index 000000000..4ab44c82f --- /dev/null +++ b/main/guides/zoe/assets/trade-offer-safety-2.svg @@ -0,0 +1 @@ +ContractgameAssetContractZoegameAssetContractZoeAlicemakeJoinInvitation()1offer(toJoin, proposal, { Price })2E(seat).getPayout('Places')3Alice \ No newline at end of file diff --git a/main/guides/zoe/assets/trade-offer-safety-3.mmd b/main/guides/zoe/assets/trade-offer-safety-3.mmd new file mode 100644 index 000000000..25eb5b26a --- /dev/null +++ b/main/guides/zoe/assets/trade-offer-safety-3.mmd @@ -0,0 +1,29 @@ +sequenceDiagram + autonumber + + box aqua + actor A as Alice + end + + box darksalmon + participant Zoe + end + + box skyblue Contract + participant C as gameAssetContract + end + + A-)C: makeJoinInvitation() + activate C + C--)Zoe: makeInvitation(joinHandler, ...) + deactivate C + activate Zoe + Zoe--)-C: invitation + activate C + C--)A: Invitation toJoin + deactivate C + A-)+Zoe: offer(toJoin, proposal, { Price }) + + Zoe--)Zoe: escrow Price pmt + + Zoe--)-C: joinHandler(gameSeat) diff --git a/main/guides/zoe/assets/trade-offer-safety-3.svg b/main/guides/zoe/assets/trade-offer-safety-3.svg new file mode 100644 index 000000000..8323004f0 --- /dev/null +++ b/main/guides/zoe/assets/trade-offer-safety-3.svg @@ -0,0 +1 @@ +ContractgameAssetContractZoegameAssetContractZoeAlicemakeJoinInvitation()1makeInvitation(joinHandler, ...)2invitation3Invitation toJoin4offer(toJoin, proposal, { Price })5escrow Price pmt6joinHandler(gameSeat)7Alice \ No newline at end of file diff --git a/main/guides/zoe/assets/trade-offer-safety-4.mmd b/main/guides/zoe/assets/trade-offer-safety-4.mmd new file mode 100644 index 000000000..e3a07e24e --- /dev/null +++ b/main/guides/zoe/assets/trade-offer-safety-4.mmd @@ -0,0 +1,28 @@ +sequenceDiagram + autonumber + + box aqua + actor A as Alice + end + + box darksalmon + participant Zoe + end + + box skyblue Contract + participant C as gameAssetContract + end + + Zoe--)C: joinHandler(gameSeat) + activate C + C--)C: check proposal + C--)Zoe: mintGains(want) + deactivate C + activate Zoe + Zoe--)-C: tmp seat + + C--)Zoe: atomicRearrange(...) + + Zoe--)Zoe: check offer safety + + C--)Zoe: playerSeat.exit(true) \ No newline at end of file diff --git a/main/guides/zoe/assets/trade-offer-safety-4.svg b/main/guides/zoe/assets/trade-offer-safety-4.svg new file mode 100644 index 000000000..eb0b82e51 --- /dev/null +++ b/main/guides/zoe/assets/trade-offer-safety-4.svg @@ -0,0 +1 @@ +ContractgameAssetContractZoegameAssetContractZoeAlicejoinHandler(gameSeat)1check proposal2mintGains(want)3tmp seat4atomicRearrange(...)5check offer safety6playerSeat.exit(true)7Alice \ No newline at end of file diff --git a/main/guides/zoe/assets/trade-offer-safety-5.mmd b/main/guides/zoe/assets/trade-offer-safety-5.mmd new file mode 100644 index 000000000..f8fc74ca8 --- /dev/null +++ b/main/guides/zoe/assets/trade-offer-safety-5.mmd @@ -0,0 +1,20 @@ +sequenceDiagram + autonumber + + box aqua + actor A as Alice + end + + box darksalmon + participant Zoe + end + + box skyblue Contract + participant C as gameAssetContract + end + + A-)+Zoe: E(seat).getPayout('Places') + Note over Zoe: ... many steps above ... + Zoe--)-A: placesPayment + A-)+Zoe: E(issuers.Place).getAmountOf(placesPayment) + Zoe--)-A: { brand: Place brand,
value: [['Park Place, 1n], ['Boardwalk', 1n]] diff --git a/main/guides/zoe/assets/trade-offer-safety-5.svg b/main/guides/zoe/assets/trade-offer-safety-5.svg new file mode 100644 index 000000000..70b42dfe6 --- /dev/null +++ b/main/guides/zoe/assets/trade-offer-safety-5.svg @@ -0,0 +1 @@ +ContractgameAssetContractZoegameAssetContractZoe... many steps above ...AliceE(seat).getPayout('Places')1placesPayment2E(issuers.Place).getAmountOf(placesPayment)3{ brand: Place brand, value: [['Park Place, 1n], ['Boardwalk', 1n]]4Alice \ No newline at end of file diff --git a/main/guides/zoe/contract-basics.md b/main/guides/zoe/contract-basics.md new file mode 100644 index 000000000..374303115 --- /dev/null +++ b/main/guides/zoe/contract-basics.md @@ -0,0 +1,84 @@ +# Smart Contract Basics + +Before we look at how to make a contract such as the one in [the +basic dapp](./) in the previous section, let's cover some basics. + +A contract is defined by a JavaScript module that exports a `start` function +that implements the contract's API. + +<<< @/snippets/zoe/src/01-hello.js#start + +Let's start with a contract with a simple `greet` function: + +<<< @/snippets/zoe/src/01-hello.js#greet + +The `start` function can expose the `greet` function +as part of the contract API by making it +a method of the contract's `publicFacet`: + +<<< @/snippets/zoe/src/01-hello.js#publicFacet + +We mark it `Far(...)` to allow callers to use it from outside the contract +and give it a suggestive interface name for debugging. +_We'll discuss [Far in more detail](../js-programming/far.md) later._ + +Putting it all together: + +<<< @/snippets/zoe/src/01-hello.js#contract + +## Using, testing a contract + +Let's use some tests to explore how a contract is used. + +Agoric contracts are typically tested +using the [ava](https://github.com/avajs/ava) framework. +They start with `@endo/init` to establish a [Hardened JavaScript](../js-programming/hardened-js.md) environment: + +<<< @/snippets/zoe/contracts/test-zoe-hello.js#test-imports + +_We'll talk more about [using `E()` for async method calls](../js-programming/eventual-send.md) later._ + +A test that the `greet` method works as expected looks like: + +<<< @/snippets/zoe/contracts/test-zoe-hello.js#test1 + +## State + +Contracts can use ordinary variables for state. + +<<< @/snippets/zoe/src/02-state.js#startfn + +Using `set` changes the results of the following call to `get`: + +<<< @/snippets/zoe/contracts/test-zoe-hello.js#test-state + +::: tip Heap state is persistent + +Ordinary heap state persists between contract invocations. + +We'll discuss more explicit state management for +large numbers of objects (_virtual objects_) and +objects that last across upgrades (_durable objects_) later. + +::: + +## Access Control with Objects + +We can limit the `publicFacet` API to read-only by omitting the `set()` method. + +The `creatorFacet` is provided only to the caller who creates the contract instance. + +<<< @/snippets/zoe/src/03-access.js + +Trying to `set` using the `publicFacet` throws, but +using the `creatorFacet` works: + +<<< @/snippets/zoe/contracts/test-zoe-hello.js#test-access + +Note that the `set()` method has no access check inside it. +Access control is based on separation of powers between +the `publicFacet`, which is expected to be shared widely, +and the `creatorFacet`, which is closely held. +_We'll discuss this [object capabilities](../js-programming/hardened-js.md#object-capabilities-ocaps) approach more later._ + +Next, let's look at minting and trading assets with [Zoe](../zoe/). diff --git a/package.json b/package.json index 311a1738f..dd72b045b 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "homepage": "https://github.com/Agoric/documentation#readme", "dependencies": { "@agoric/zoe": "beta", + "@agoric/ertp": "beta", "@endo/far": "^0.2.19", "@endo/marshal": "^0.8.6", "@endo/patterns": "^0.2.3", diff --git a/snippets/zoe/contracts/alice-trade.js b/snippets/zoe/contracts/alice-trade.js new file mode 100644 index 000000000..94010f1a8 --- /dev/null +++ b/snippets/zoe/contracts/alice-trade.js @@ -0,0 +1,50 @@ +/** + * @file Test using bundleSource() on the contract. + * + * TODO: how to sync with agoric-labs/dapp-game-places? + */ +// @ts-check +import { E } from '@endo/far'; +import { makeCopyBag } from '@endo/patterns'; +import { AmountMath } from '@agoric/ertp'; + +/** + * Alice joins the game by paying the price from the contract's terms. + * + * @param {import('ava').ExecutionContext} t + * @param {ZoeService} zoe + * @param {ERef>} instance + * @param {Purse} purse + */ +export const alice = async (t, zoe, instance, purse) => { + // #region queryInstance + const publicFacet = E(zoe).getPublicFacet(instance); + const terms = await E(zoe).getTerms(instance); + const { issuers, brands, joinPrice } = terms; + // #endregion queryInstance + + // #region makeProposal + const choices = ['Park Place', 'Boardwalk']; + const choiceBag = makeCopyBag(choices.map(name => [name, 1n])); + const proposal = { + give: { Price: joinPrice }, + want: { Places: AmountMath.make(brands.Place, choiceBag) }, + }; + const Price = await E(purse).withdraw(joinPrice); + t.log('Alice gives', proposal.give); + // #endregion makeProposal + + // #region trade + const toJoin = E(publicFacet).makeJoinInvitation(); + + const seat = E(zoe).offer(toJoin, proposal, { Price }); + const places = await E(seat).getPayout('Places'); + // #endregion trade + + // #region payouts + const actual = await E(issuers.Place).getAmountOf(places); + t.log('Alice payout brand', actual.brand); + t.log('Alice payout value', actual.value); + t.deepEqual(actual, proposal.want.Places); + // #endregion payouts +}; diff --git a/snippets/zoe/contracts/test-bundle-source.js b/snippets/zoe/contracts/test-bundle-source.js new file mode 100644 index 000000000..ae7258ee9 --- /dev/null +++ b/snippets/zoe/contracts/test-bundle-source.js @@ -0,0 +1,37 @@ +/** + * @file Test using bundleSource() on the contract. + */ +// @ts-check + +/* eslint-disable import/order -- https://github.com/endojs/endo/issues/1235 */ +import { test } from '../../prepare-test-env-ava.js'; + +// #region bundleSourceImports +import bundleSource from '@endo/bundle-source'; +import { createRequire } from 'module'; +// #endregion bundleSourceImports +import { E } from '@endo/far'; +// #region importZoeForTest +import { makeZoeKitForTest } from '@agoric/zoe/tools/setup-zoe.js'; +// #endregion importZoeForTest + +// #region contractPath +const myRequire = createRequire(import.meta.url); +const contractPath = myRequire.resolve(`../src/gameAssetContract.js`); +// #endregion contractPath + +test('bundleSource() bundles the contract for use with zoe', async t => { + // #region testBundleSource + const bundle = await bundleSource(contractPath); + t.is(bundle.moduleFormat, 'endoZipBase64'); + t.log(bundle.endoZipBase64Sha512); + t.true(bundle.endoZipBase64.length > 10_000); + // #endregion testBundleSource + + // #region testInstall + const { zoeService: zoe } = await makeZoeKitForTest(); + const installation = await E(zoe).install(bundle); + t.log(installation); + t.is(typeof installation, 'object'); + // #endregion testInstall +}); diff --git a/snippets/zoe/contracts/test-zoe-hello.js b/snippets/zoe/contracts/test-zoe-hello.js new file mode 100644 index 000000000..7a62d6050 --- /dev/null +++ b/snippets/zoe/contracts/test-zoe-hello.js @@ -0,0 +1,36 @@ +// #region test-imports +import '@endo/init'; +import { E } from '@endo/far'; +// eslint-disable-next-line import/no-unresolved -- https://github.com/avajs/ava/issues/2951 +import test from 'ava'; +// #endregion test-imports +import * as state from '../src/02-state.js'; +import * as access from '../src/03-access.js'; +// #region test1 +import { start } from '../src/01-hello.js'; + +test('contract greets by name', async t => { + const { publicFacet } = start(); + const actual = await E(publicFacet).greet('Bob'); + t.is(actual, 'Hello, Bob!'); +}); +// #endregion test1 + +// #region test-state +test('state', async t => { + const { publicFacet } = state.start(); + t.is(await E(publicFacet).get(), 'Hello, World!'); + await E(publicFacet).set(2); + t.is(await E(publicFacet).get(), 2); +}); +// #endregion test-state + +// #region test-access +test('access control', async t => { + const { publicFacet, creatorFacet } = access.start(); + t.is(await E(publicFacet).get(), 'Hello, World!'); + await t.throwsAsync(E(publicFacet).set(2), { message: /no method/ }); + await E(creatorFacet).set(2); + t.is(await E(publicFacet).get(), 2); +}); +// #endregion test-access diff --git a/snippets/zoe/src/01-hello.js b/snippets/zoe/src/01-hello.js new file mode 100644 index 000000000..8353e87cd --- /dev/null +++ b/snippets/zoe/src/01-hello.js @@ -0,0 +1,17 @@ +// #region contract +import { Far } from '@endo/far'; + +// #region greet +const greet = who => `Hello, ${who}!`; +// #endregion greet + +// #region start +export const start = () => { + // #endregion start + // #region publicFacet + return { + publicFacet: Far('Hello', { greet }), + }; + // #endregion publicFacet +}; +// #endregion contract diff --git a/snippets/zoe/src/02-state.js b/snippets/zoe/src/02-state.js new file mode 100644 index 000000000..487d1643d --- /dev/null +++ b/snippets/zoe/src/02-state.js @@ -0,0 +1,13 @@ +import { Far } from '@endo/far'; + +// #region startfn +export const start = () => { + let value = 'Hello, World!'; + const get = () => value; + const set = v => (value = v); + + return { + publicFacet: Far('ValueCell', { get, set }), + }; +}; +// #endregion startfn diff --git a/snippets/zoe/src/03-access.js b/snippets/zoe/src/03-access.js new file mode 100644 index 000000000..6a019c648 --- /dev/null +++ b/snippets/zoe/src/03-access.js @@ -0,0 +1,12 @@ +import { Far } from '@endo/far'; + +export const start = () => { + let value = 'Hello, World!'; + const get = () => value; + const set = v => (value = v); + + return { + publicFacet: Far('ValueView', { get }), + creatorFacet: Far('ValueCell', { get, set }), + }; +}; diff --git a/snippets/zoe/src/debug.js b/snippets/zoe/src/debug.js new file mode 100644 index 000000000..fb502a564 --- /dev/null +++ b/snippets/zoe/src/debug.js @@ -0,0 +1,41 @@ +// @jessie-check + +let debugInstance = 1; + +/** + * @param {string} name + * @param {boolean | 'verbose'} enable + */ +export const makeTracer = (name, enable = true) => { + debugInstance += 1; + let debugCount = 1; + const key = `----- ${name}.${debugInstance} `; + // the cases below define a named variable to provide better debug info + switch (enable) { + case false: { + const logDisabled = (..._args) => {}; + return logDisabled; + } + case 'verbose': { + const infoTick = (optLog, ...args) => { + if (optLog.log) { + console.info(key, (debugCount += 1), ...args); + } else { + console.info(key, (debugCount += 1), optLog, ...args); + } + }; + return infoTick; + } + default: { + const debugTick = (optLog, ...args) => { + if (optLog.log) { + optLog.log(key, (debugCount += 1), ...args); + } else { + console.info(key, (debugCount += 1), optLog, ...args); + } + }; + return debugTick; + } + } +}; +harden(makeTracer); diff --git a/snippets/zoe/src/gameAssetContract.js b/snippets/zoe/src/gameAssetContract.js new file mode 100644 index 000000000..80ad5b174 --- /dev/null +++ b/snippets/zoe/src/gameAssetContract.js @@ -0,0 +1,80 @@ +// #region file +/** @file Contract to mint and sell Place NFTs for a hypothetical game. */ +// @ts-check + +import { Far } from '@endo/far'; +import { M, getCopyBagEntries } from '@endo/patterns'; +import { AmountMath, AssetKind } from '@agoric/ertp/src/amountMath.js'; +import { AmountShape } from '@agoric/ertp/src/typeGuards.js'; +import { atomicRearrange } from '@agoric/zoe/src/contractSupport/atomicTransfer.js'; +import '@agoric/zoe/exported.js'; + +import { makeTracer } from './debug.js'; + +const { Fail, quote: q } = assert; + +const trace = makeTracer('Game', true); + +/** @param {Amount<'copyBag'>} amt */ +const bagValueSize = amt => { + /** @type {[unknown, bigint][]} */ + const entries = getCopyBagEntries(amt.value); // XXX getCopyBagEntries returns any??? + const total = entries.reduce((acc, [_place, qty]) => acc + qty, 0n); + return total; +}; + +/** + * @param {ZCF<{joinPrice: Amount}>} zcf + */ +// #region start +export const start = async zcf => { + const { joinPrice } = zcf.getTerms(); + + const { zcfSeat: gameSeat } = zcf.makeEmptySeatKit(); + const mint = await zcf.makeZCFMint('Place', AssetKind.COPY_BAG); + // #endregion start + + // #region handler + /** @param {ZCFSeat} playerSeat */ + const joinHandler = playerSeat => { + const { give, want } = playerSeat.getProposal(); + trace('join', 'give', give, 'want', want.Places.value); + + AmountMath.isGTE(give.Price, joinPrice) || + Fail`${q(give.Price)} below joinPrice of ${q(joinPrice)}}`; + + bagValueSize(want.Places) <= 3n || Fail`only 3 places allowed when joining`; + + const tmp = mint.mintGains(want); + atomicRearrange( + zcf, + harden([ + [playerSeat, gameSeat, give], + [tmp, playerSeat, want], + ]), + ); + + playerSeat.exit(true); + return 'welcome to the game'; + }; + // #endregion handler + + // #region makeInvitation + const joinShape = harden({ + give: { Price: AmountShape }, + want: { Places: AmountShape }, + exit: M.any(), + }); + + const publicFacet = Far('API', { + makeJoinInvitation: () => + zcf.makeInvitation(joinHandler, 'join', undefined, joinShape), + }); + // #endregion makeInvitation + + // #region started + return { publicFacet }; + // #endregion started +}; +harden(start); +// #endregion file