# Zoe Contract Facet
A Zoe Contract Facet is an API object for a running contract instance to access the Zoe state
for that instance. A Zoe Contract Facet is accessed synchronously from within the contract,
and usually is referred to in code as zcf
.
The contract instance is launched by E(zoe).startInstance()
, and is given access to
the zcf
object during that launch. In the operations below, instance
is
the handle for the running contract instance.
# start
This section covers the code you need to have at the start of your contract code.
To warn if the correct return values for your contract are not being returned, add this right before the start of your contract code. It also lets TypeScript-aware tools (IDEs like vsCode and WebStorm) inform the developer about required parameters and return values and warn when methods are misused.
/**
* @type {ContractStartFn}
*/
Your contract code must export a function start
as a non-default export.
zcf
is the Zoe Contract Facet and is the first argument provided to
the contract. The second argument, privateArgs
, is used by the
caller of startInstance
to pass in any arguments that should not be
part of the public terms. privateArgs
is an object with keys and
values as decided by the caller of startInstance
. If no private
arguments are passed, privateArgs
is undefined.
const start = (zcf, privateArgs) => {
...
// your code here
return harden({ creatorFacet, creatorInvitation, publicFacet });
}
harden(start);
export { start };
The contract must return a record with any (or none) of the following:
creatorFacet
- an object, usually with admin authority. It is only given to the entity that callsE(zoe).startInstance()
; i.e. the party that was the creator of the current contractinstance
. It createsinvitations
for other parties, and takes actions that are unrelated to making offers.creatorInvitation
- a Zoeinvitation
only given to the entity that callsE(zoe).startInstance()
; i.e. the party that was the creator of the current contractinstance
. This is usually used when a party has to make an offer first, such as escrowing the underlying good for sale in an auction or covered call.publicFacet
- an object available through Zoe to anyone who knows the contract instance. Use thepublicFacet
for general queries and actions, such as getting the current price or creating publicinvitations
.
# zcf.makeZCFMint(keyword, assetKind, displayInfo)
keyword
{String}
assetKind
{AssetKind}
(defaults toAssetKind.NAT
)displayInfo
{DisplayInfo}
(optional)- Returns:
{Promise<ZCFMint>}
Creates a synchronous Zoe mint, allowing users to mint and reallocate digital assets synchronously
instead of relying on an asynchronous ERTP mint
. The optional displayInfo
parameter takes values
like { decimalPlaces: 16 }
that tell the UI how to display values associated with the created mint's
brand. It defaults to undefined.
Important: ZCFMints
do not have the same methods as an ERTP mint
. Do not try to use
ERTP methods on a ZCFMint
or vice versa.
Important: On the other hand, the issuer
and brand
associated with a zcfMint
do have the same methods as their ERTP-derived counterparts. Assets created by a zcfMint
are treated
the same as ERTP mint
-created assets by ERTP methods.
The following demonstrates zcf.makeZCFMint
:
Note: The call to make the ZCFMint
is asynchronous, but
calls to the resulting ZCFMint
are synchronous.
const mySynchronousMint = await zcf.makeZCFMint('MyToken', AssetKind.SET);
const { brand, issuer } = mySynchronousMint.getIssuerRecord();
mySynchronousMint.mintGains({ MyKeyword: amount }, seat);
ZCFMints
have three methods, two of which use an AmountKeywordRecord
getIssuerRecord()
mintGains(gains, zcfSeat)
burnLosses(losses, zcfSeat)
# AmountKeywordRecord
AmountKeywordRecord
is a record in which the keys are keywords, and
the values are amounts
. Keywords are unique identifiers per contract,
that tie together the proposal
, payments
to be escrowed, and payouts
to the user. In the below example, Asset
and Price
are keywords.
Users should submit their payments
using keywords:
const payments = { Asset: quatloosPayment };
And, users will receive their payouts
with keywords as the keys of a payout
:
quatloosPurse.deposit(payout.Asset);
For example:
const quatloos5 = AmountMath.make(quatloosBrand, 5n);
const quatloos9 = AmountMath.make(quatloosBrand, 9n);
const myAmountKeywordRecord =
{
Asset: quatloos5,
Price: quatloos9
}
# ZCFMint.getIssuerRecord()
- Returns:
{IssuerRecord}
- Returns an
issuerRecord
containing theissuer
andbrand
associated with thezcfMint
.
# ZCFMint.mintGains(gains, zcfSeat)
gains
:AmountKeywordRecord
zcfSeat
:{ZCFSeat}
- optional- Returns:
{ZCFSeat}
- All
amounts
ingains
must be of thisZCFMint
'sbrand
. Thegains
' keywords are in thatseat
's namespace. Mint thegains
amount
of assets and add them to thatseat
'sallocation
. If aseat
is provided, it is returned. Otherwise a newseat
is returned. zcfMint.mintGains({ Token: amount }, seat);
# ZCFMint.burnLosses(losses, zcfSeat)
losses
:AmountKeywordRecord
zcfSeat
:{ZCFSeat}
- Returns: void
- All
amounts
inlosses
must be of thisZCFMint
'sbrand
. Thelosses
' keywords are in thatseat
's namespace. Subtractlosses
from thatseat
'sallocation
, then burn thatamount
of assets from the pooledpurse
. zcfMint.burnLosses({ Token: amount }, seat);
getIssuerRecord()`
# zcf.getInvitationIssuer()
- Returns:
{Issuer}
Zoe has a single invitationIssuer
for the entirety of its
lifetime. This method returns the Zoe InvitationIssuer
, which
validates user-received invitations
to participate
in contract instances.
"All invitations
come from this single invitation
issuer
and its mint
, which
mint invitations
and validate their amounts
."
const invitationIssuer = await zcf.getInvitationIssuer();
# zcf.saveIssuer(issuer, keyword)
issuer
{Issuer}
keyword
{String}
- Returns:
{Promise<IssuerRecord>}
Informs Zoe about an issuer
and returns a promise for acknowledging
when the issuer
is added and ready. The keyword
is the one associated
with the new issuer
. It returns a promise for issuerRecord
of the new issuer
This saves an issuer
in Zoe's records for this contract instance
.
It also has saved the issuer
information such that Zoe can handle offers involving
this issuer
and ZCF can provide the issuerRecord
synchronously on request.
An IssuerRecord
has two fields, each of which holds the namesake object
associated with the issuer
value of the record:
issuerRecord.brand
and issuerRecord.issuer
.
await zcf.saveIssuer(secondaryIssuer, keyword);
# zcf.makeInvitation(offerHandler, description, customProperties)
offerHandler
{ZCFSeat => Object}
description
{String}
customProperties
{Object}
- Returns:
{Promise<Invitation>}
Make a credible Zoe invitation
for a smart contract. Note that invitations
are a special case
of an ERTP payment
. They are associated with the invitationIssuer
and its mint
, which
validate and mint invitations
. zcf.makeInvitation()
serves as an interface to
the invitation
mint
.
The invitation
's
value
specifies:
- The specific contract
instance
. - The Zoe
installation
. - A unique
handle
The second argument is a required description
for the invitation
,
and should include whatever information is needed for a potential recipient of the invitation
to know what they are getting in the optional customProperties
argument, which is
put in the invitation
's value
.
const creatorInvitation = zcf.makeInvitation(makeCallOption, 'makeCallOption')
# zcf.makeEmptySeatKit()
- Returns:
{ZCFSeat, Promise<UserSeat>}
Returns an empty ZCFSeat
and a promise for a UserSeat
Zoe uses seats
to represent offers, and has two seat facets (a
particular view or API of an object;
there may be multiple such APIs per object) a ZCFSeat
and a UserSeat
.
const { zcfSeat: mySeat } = zcf.makeEmptySeatKit();
# ZCFSeat
object
Zoe uses seats
to access or manipulate offers. Seats represent active
offers and let contracts and users interact with them. Zoe has two kinds
of seats. ZCFSeats
are used within contracts and with zcf
methods.
UserSeats
represent offers external to Zoe and the contract.
A ZCFSeat
includes synchronous queries for the current state of the
associated offer, such as the amounts of assets that are currently
allocated to the offer. It also includes synchronous operations
to manipulate the offer. The queries and operations are as follows:
# ZCFSeat.hasExited()
- Returns:
{Boolean}
- Returns
true
if theseat
has exited,false
if it is still active.
# ZCFSeat.getNotifier()
- Returns:
{Notifier<Allocation>}
- Returns a
notifier
associated with theseat
'sallocation
. You use anotifier
wherever some piece of code has changing state that other code wants updates on. Thisnotifier
provides updates on changingallocations
for thisseat
, and tells when theseat
has been exited. For more onnotifiers
, see the Distributed Programming Guide.
# ZCFSeat.getProposal()
- Returns:
{ProposalRecord}
- A
Proposal
is represented by aProposalRecord
. It is the rules accompanying the escrow ofpayments
dictating what the user expects to get back from Zoe. It has keysgive
,want
, andexit
.give
andwant
are records with keywords as keys andamounts
as values. Theproposal
is a user's understanding of the contract that they are entering when they make an offer. SeeE(zoe).offer()
for full details. - Example:
const { want, give, exit } = sellerSeat.getProposal();
# ZCFSeat.getAmountAllocated(keyword, brand)
keyword
:{String}
brand
:{Brand}
Returns:
{Amount}
Returns the
amount
from the part of theallocation
that matches thekeyword
andbrand
. If thekeyword
is not in theallocation
, it returns an emptyamount
of thebrand
argument. (Afterexit()
has been called, it continues to report the final allocation balance, which was transferred to a payout.)This is similar to the next method,
getCurrentAllocation()
.getAmountAllocated()
gets theallocation
of one keyword at a time, whilegetCurrentAllocation()
returns all the currentallocations
at once.
# ZCFSeat.getCurrentAllocation()
Returns:
{<Allocation>}
An
Allocation
is anAmountKeywordRecord
of key-value pairs where the key is a keyword such asAsset
orPrice
applicable to the contract. The value is anamount
with itsvalue
andbrand
.Allocations
represent theamounts
to be paid out to eachseat
on exit. (Afterexit()
has been called, the final allocation balances, which were transferred to payouts, continue to be reported.) Normal reasons for exiting are the user requesting to exit or the contract explicitly choosing to close out theseat
. The guarantees also hold if the contract encounters an error or misbehaves. There are several methods for finding out whatamount
a currentallocation
is.This is similar to the previous method,
getAmountAllocated()
.getAmountAllocated()
gets theallocation
of one keyword at a time, whilegetCurrentAllocation()
returns all the currentallocations
at once.An
Allocation
example:{ Asset: AmountMath.make(quatloosBrand, 5n), Price: AmountMath.make(quatloosBrand, 9n) }
# ZCFSeat.incrementBy(amountKeywordRecord)
amountKeywordRecord
:{AmountKeywordRecord}
Returns:
{AmountKeyRecord}
Adds the
amountKeywordRecord
argument to theZCFseat
's staged allocation and returns the sameamountKeywordRecord
so it can be reused in another call. Note that this letszcfSeat1.incrementBy(zcfSeat2.decrementBy(amountKeywordRecord))
work as a usage pattern.Note that you can add amounts to original or staged allocations which do not have the specified keyword for the amount. The result is for the keyword and amount to become part of the allocation. For example, if we start with a new, empty, allocation:
// Make an empty seat. const { zcfSeat: zcfSeat1 } = zcf.makeEmptySeatKit(); // The allocation is currently empty, i.e. `{}` const stagedAllocation = zcfSeat1.getStagedAllocation(); const empty = AmountMath.makeEmpty(brand, AssetKind.NAT); // Try to incrementBy empty. This succeeds, and the keyword is added // with an empty amount. zcfSeat1.incrementBy({ RUN: empty }); t.deepEqual(zcfSeat1.getStagedAllocation(), { RUN: empty });
While this incremented the allocation by an empty amount, any amount would have been added to the allocation in the same way.
# ZCFSeat.decrementBy(amountKeywordRecord)
amountKeywordRecord
:{AmountKeywordRecord}
Returns:
{AmountKeywordRecord}
Subtracts the
amountKeywordRecord
argument from theZCFseat
's staged allocation and returns the sameamountKeywordRecord
so it can be used in another call. Note that this letszcfSeat1.incrementBy(zcfSeat2.decrementBy(amountKeywordRecord))
work as a usage pattern.The amounts to subtract cannot be greater than the staged allocation (i.e. negative results are not allowed).
decrementBy()
has different behavior fromincrementBy()
if the original or staged allocation does not have the keyword specified for an amount in theamountKeywordRecord
argument. There are two cases to look at; when the corresponding amount to subtract is empty and when it isn't.// Make an empty seat. const { zcfSeat: zcfSeat1 } = zcf.makeEmptySeatKit(); // The allocation is currently {} const stagedAllocation = zcfSeat1.getStagedAllocation(); const empty = AmountMath.makeEmpty(brand, AssetKind.NAT); // decrementBy empty does not throw, and does not add a keyword zcfSeat1.decrementBy({ RUN: empty }); t.deepEqual(zcfSeat1.getStagedAllocation(), {});
The result here is not to add the keyword to the allocation. It wasn't there to begin with, and the operation was to try to subtract it from the allocation. Subtracting something that's not there does not add it to the original value. For example, if I tell you I'm taking away the Mona Lisa from you and you are not the Louvre and don't have it, you still don't have it after I try to take it away. In the above example, trying to take away an empty amount from an empty allocation is effectively a null operation; the allocation is still empty, didn't add the new keyword, and no error is thrown.
However, decrementing a non-empty amount from an empty allocation has a different result. For example:
// Make an empty seat. const { zcfSeat: zcfSeat1 } = zcf.makeEmptySeatKit(); // The allocation is currently {} const stagedAllocation = zcfSeat1.getStagedAllocation(); // decrementBy throws for a keyword that does not exist on the stagedAllocation and a non-empty amount zcfSeat1.decrementBy({ RUN: runFee });
It throws an error because you cannot subtract something from nothing. So trying to decrement an empty allocation by a non-empty amount is an error, while decrementing an empty allocation by an empty amount is effectively a null operation with no effects.
# ZCFSeat.clear()
- Returns:
{void}
- Deletes the
ZCFSeat
's current staged allocation, if any.
# ZCFSeat.getStagedAllocation()
- Returns:
{<Allocation>}
- Gets and returns the
stagedAllocation
, which is the allocation committed if the seat is reallocated over, if offer safety holds and rights are conserved.
# ZCFSeat.hasStagedAllocation()
- Returns:
{boolean}
- Returns
true
if there is a staged allocation, i.e. whetherincrementBy()
ordecrementBy()
has been called andclear()
andreallocate()
have not. Otherwise returnsfalse
.
# ZCFSeat.exit(completion)
completion
:{Object}
Returns:
void
Causes the
seat
to exit, concluding its existence. Allpayouts
, if any, are made, and theseat
object can no longer interact with the contract. Thecompletion
argument is usually a string, but this is not required. Its only use is for the notification sent to the contract instance'sdone()
function. Any other still open seats or outstanding promises and the contract instance continue.Note: You should not use
ZCFSeat.exit()
when exiting with an error. Use the method described next,ZCFSeat.fail()
, instead.
# ZCFSeat.fail(msg)
msg
:{String}
Returns:
void
The
seat
exits, displaying the optionalmsg
string, if there is one, on the console. This is equivalent to exiting, except thatexit
is successful whilefail()
signals an error occurred while processing the offer. The contract still gets its currentallocation
and theseat
can no longer interact with the contract. Any other still open seats or outstanding promises and the contract instance continue.Agoric recommends you exit a seat with an error as follows:
throw seat.fail(Error('you did it wrong'));
# ZCFSeat.isOfferSafe(newAllocation)
newAllocation
:{Allocation}
- Returns
{Boolean}
- Takes an
allocation
as an argument and returnstrue
if thatallocation
satisfies offer safety,false
if it doesn't. Essentially, it checksnewAllocation
for offer safety, against theseat
'sproposal
. It checks whethernewAllocation
fully satisfiesproposal.give
(giving a refund) or whether it fully satisfiesproposal.want
. Both can be fully satisfied. See the ZoeHelpersatisfies()
method for more details.
# zcf.getInstance()
- Returns:
{Instance}
The contract code can request its own current instance, so it can be sent elsewhere.
# zcf.getBrandForIssuer(issuer)
issuer
{Issuer}
- Returns:
{Brand}
Returns the brand
associated with the issuer
.
# zcf.getIssuerForBrand(brand)
brand
{Brand}
- Returns:
{Issuer}
Returns the issuer
of the brand
argument.
# zcf.getAssetKind(brand)
brand
{Brand}
- Returns:
{AssetKind}
Returns the AssetKind
associated with the brand
argument.
const quatloosAssetKind = zcf.getAssetKind(quatloosBrand);
# zcf.stopAcceptingOffers()
- The contract requests Zoe to not accept offers for this contract instance. It can't be called from outside the contract unless the contract explicitly makes it accessible.
# zcf.shutdown(completion)
Shuts down the entire vat and contract instance and gives payouts.
All open seats
associated with the current instance
have fail()
called on them.
Call when:
- You want nothing more to happen in the contract, and
- You don't want to take any more offers
The completion
argument is usually a string, but this
is not required. It is used for the notification sent to the
contract instance's done()
function. Any still open seats or
other outstanding promises are closed with a generic 'vat terminated'
message.
zcf.shutdown();
# zcf.getTerms()
- Returns:
{Object}
Returns the issuers
, brands
, and custom terms
the current contract instance
was instantiated with.
The returned values look like:
{ brands, issuers, customTermA, customTermB ... }
// where brands and issuers are keywordRecords, like:
{
brands: { A: moolaKit.brand, B: simoleanKit.brand },
issuers: { A: moolaKit.issuer, B: simoleanKit.issuer },
customTermA: 'something',
customTermB: 'something else',
};
Note that there is also an E(zoe).getTerms(instance)
. Often the choice of which to use is not which method
to use, but which of Zoe Service or ZCF you have access to. On the contract side, you more easily have access
to zcf
, and zcf
already knows what instance is running. So in contract code, you use zcf.getTerms()
. From
a user side, with access to Zoe Service, you use E(zoe).getTerms()
.
const { brands, issuers, maths, terms } = zcf.getTerms()
# zcf.getZoeService()
- Returns: ZoeService
This is the only way to get the user-facing Zoe Service API to the contract code as well.
// Making an offer to another contract instance in the contract.
const zoeService = zcf.getZoeService();
E(zoeService).offer(creatorInvitation, proposal, paymentKeywordRecord);
# zcf.assertUniqueKeyword(keyword)
keyword
{String}
- Returns: Undefined
Checks if a keyword is valid and not already used as a brand
in this instance
(i.e. unique)
and could be used as a new brand
to make an issuer
. Throws an appropriate error if it's not
a valid keyword, or is not unique.
zcf.assertUniqueKeyword(keyword);
# zcf.reallocate(seats)
seats
{ZCFSeats[]}
(at least two)- Returns:
{void}
zcf.reallocate()
commits the staged allocations for each of its seat arguments,
making their staged allocations their current allocations. zcf.reallocate()
then
transfers the assets escrowed in Zoe from one seat to another. Importantly, the assets
stay escrowed, with only the internal Zoe accounting of each seat's allocation changed.
There must be at least two ZCFSeats
in the array argument. Every ZCFSeat
with a staged allocation must be included in the argument array or an error
is thrown. If any seat in the argument array does not have a staged allocation,
an error is thrown.
On commit, the staged allocations become the seats' current allocations and the staged allocations are deleted.
Note: reallocate()
is an atomic operation. To enforce offer safety,
it will never abort part way through. It will completely succeed or it will
fail before any seats have their current allocation changed.
The reallocation only succeeds if it:
- Conserves rights (the specified
amounts
have the same total value as the current total amount) - Is 'offer-safe' for all parties involved.
The reallocation is partial, only applying to the seats
in the
argument array. By induction, if rights conservation and
offer safety hold before, they hold after a safe reallocation.
This is true even though we only re-validate for seats
whose
allocations change. A reallocation can only effect offer safety for
those seats
, and since rights are conserved for the change, overall
rights are unchanged.
reallocate()
throws this error:
reallocating must be done over two or more seats
sellerSeat.incrementBy(buyerSeat.decrementBy({ Money: providedMoney }));
buyerSeat.incrementBy(sellerSeat.decrementBy({ Items: wantedItems }));
zcf.reallocate(buyerSeat, sellerSeat);
← Zoe ZoeHelpers →