Skip to content

Commit

Permalink
[Zoe] Zoe Release v0.3.0 (#685)
Browse files Browse the repository at this point in the history
* feat: add keywords to Zoe
  • Loading branch information
katelynsills authored Mar 25, 2020
1 parent 2c293ba commit 0f050cc
Show file tree
Hide file tree
Showing 44 changed files with 4,428 additions and 4,233 deletions.
92 changes: 92 additions & 0 deletions packages/zoe/NEWS.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,96 @@
User-visible changes in @agoric/zoe:

## Release 0.3.0 (25-Mar-2020)

"OfferRules", or the declarative statement portion of an offer, have
been renamed to "proposal". The structure of a proposal has changed to:

```js
{
give: { Asset: moola(4 )},
want: { Price: simoleans(15) },
exit: { afterDeadline: {
timer,
deadline: 100,
}}
}
```

(* `moola` is an alias for `moolaAmountMath.make`, and likewise for
`simoleans`.)

There are no longer any keys such as 'payoutRules' or 'exitRule'. Most
importantly, we no longer rely on the specific order of payoutRules
arrays and payment/payout arrays to be the same. In fact, we can even
make a partial proposal that will be filled in.

`Asset` and `Price` in the above example are called "keywords". Each
contract has its own specific keywords. For instance, an auction
might have "Asset" and "Bid". Keywords are unique identifiers per
contract, that tie together the proposal, payments to be escrowed, and
payouts to the user.

Users should submit their payments using keywords:

``` const payments = { Asset: moolaPayment }; ```

And, users will receive their payouts with keywords as the keys of a
payout object:

``` moolaPurse.deposit(payout.Asset); ```

In summary, the arrays that were part of the previous API have been
replaced with records keyed by keywords.

Below is a summary of the changes:

Changes to the Zoe Service (User-facing) API:
* `makeInstance` now takes in three arguments: `installationHandle`,
`issuerKeywordRecord`, and `terms`. `issuerKeywordRecord` should have strings
as keys (called `keyword`s), and issuers as values (called `args`).
For instance, `{Asset: moolaIssuer, Price: simoleanIssuer }`. `Terms`
no longer needs an `issuers` property. Instead, `terms` should be used for
any contract-specific parameters, such as the number of bids an
auction should wait for before closing.
* OfferRules (now proposal) have the new structure mentioned above
* Payments to be escrowed must be an object keyed by `keywords` rather
than an array
* Payouts from Zoe will be in the form of a promise that resolves to a
object keyed by keywords where the values are promises for
payments
* We have added three new methods to the Zoe Service API: `getOffer`,
`getOffers`, and `isOfferActive`. Note that `getOffer` and
`getOffers` will throw if the offer is not found, whereas
`isOfferActive` is safe to check whether an offer is still active or
not.

Changes to the Contract Facet API and contract helpers:
* The `userFlow` helpers have been renamed to `zoeHelpers`
* `hasValidPayoutRules` is no longer a helper function due to the
change in the proposal structure
* Instead, we have three new helpers: `assertKeywords` which can be
used to make sure that the keywords on instantiation match what
the contract expects, `rejectIfNotProposal` which rejects an
offer if the proposal have the wrong structure, and
`checkIfProposal` which checks if the proposal match the
expected structure and returns true or false.
* `getAmountMathForIssuers` and `getBrandsForIssuers` no longer exist
* `getAmountMaths` is added. It takes in a `issuerKeywordRecord`
record that maps keywords to issuers.
* `getInstanceRecord` is added. It allows the contracts to get access
to their keywords, issuers and other "instanceRecord" information from
Zoe.
* The instanceRecord now has properties: `issuerKeywordRecord` (a record with
`keyword` keys and issuer values) and `keywords` (an array of
strings). The keywords in `issuerKeywordRecord` and the `keywords` array
are the same.
* In the offerRecord, amounts are no longer an array. Instead they are
an `amountKeywordRecord`, an object with `keywords` as keys and `amount`
values.
* Reallocate now takes an array of offerHandles/inviteHandles and an
array of the above `amountKeywordRecord`s keyed on keywords.
* AddNewIssuer now requires a keyword to be provided alongside the issuer.

## Release 0.2.1 (3-Feb-2020)

Updates ERTP dependency to v0.3.0 and adds dependencies on new
Expand Down
129 changes: 129 additions & 0 deletions packages/zoe/src/cleanProposal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import harden from '@agoric/harden';
import { assert, details } from '@agoric/assert';
import { mustBeComparable } from '@agoric/same-structure';

import { arrayToObj } from './objArrayConversion';

// cleanProposal checks the keys and values of the proposal, including
// the keys and values of the internal objects. The proposal may have
// the following keys: `give`, `want`, and `exit`. These keys may be
// omitted in the `proposal` argument passed to cleanProposal, but
// anything other than these keys is not allowed. The values of `give`
// and `want` must be "amountKeywordRecords", meaning that the keys
// must be keywords and the values must be amounts. The value of
// `exit`, if present, must be a record of one of the following forms:
// `{ waived: null }` `{ onDemand: null }` `{ afterDeadline: { timer
// :Timer, deadline :Number } }

// Assert that the keys of record, if present, are in expectedKeys.
// Return the keys after asserting this.
const checkKeys = (expectedKeys, record) => {
// Assert that keys, if present, match expectedKeys.
const keys = Object.getOwnPropertyNames(record);
keys.forEach(key => {
assert.typeof(key, 'string');
assert(
expectedKeys.includes(key),
details`key ${key} was not an expected key`,
);
});
// assert that there are no symbol properties.
assert(
Object.getOwnPropertySymbols(record).length === 0,
details`no symbol properties allowed`,
);
return keys;
};

const coerceAmountKeywordRecordValues = (
amountMathKeywordRecord,
validatedKeywords,
allegedAmountKeywordRecord,
) => {
// Check that each value can be coerced using the amountMath indexed
// by keyword. `AmountMath.coerce` throws if coercion fails.
const coercedAmounts = validatedKeywords.map(keyword =>
amountMathKeywordRecord[keyword].coerce(
allegedAmountKeywordRecord[keyword],
),
);

// Recreate the amountKeywordRecord with coercedAmounts.
return arrayToObj(coercedAmounts, validatedKeywords);
};

export const coerceAmountKeywordRecord = (
amountMathKeywordRecord,
keywords,
allegedAmountKeywordRecord,
) => {
const validatedKeywords = checkKeys(keywords, allegedAmountKeywordRecord);
return coerceAmountKeywordRecordValues(
amountMathKeywordRecord,
validatedKeywords,
allegedAmountKeywordRecord,
);
};

export const cleanProposal = (keywords, amountMathKeywordRecord, proposal) => {
const expectedRootKeys = ['want', 'give', 'exit'];
mustBeComparable(proposal);
checkKeys(expectedRootKeys, proposal);

// We fill in the default values if the keys are undefined.
let { want = harden({}), give = harden({}) } = proposal;
const { exit = harden({ onDemand: null }) } = proposal;

want = coerceAmountKeywordRecord(amountMathKeywordRecord, keywords, want);
give = coerceAmountKeywordRecord(amountMathKeywordRecord, keywords, give);

// Check exit
assert(
Object.getOwnPropertyNames(exit).length === 1,
details`exit ${proposal.exit} should only have one key`,
);
// We expect the single exit key to be one of the following:
const expectedExitKeys = ['onDemand', 'afterDeadline', 'waived'];
const [exitKey] = checkKeys(expectedExitKeys, exit);
if (exitKey === 'onDemand' || exitKey === 'waived') {
assert(
exit[exitKey] === null,
`exit value must be null for key ${exitKey}`,
);
}
if (exitKey === 'afterDeadline') {
const expectedAfterDeadlineKeys = ['timer', 'deadline'];
checkKeys(expectedAfterDeadlineKeys, exit.afterDeadline);
// timers must have a 'setWakeup' function which takes a deadline
// and an object as arguments.
// TODO: document timer interface
// /~https://github.com/Agoric/agoric-sdk/issues/751
// TODO: how to check methods on presences?
}

const hasPropDefined = (obj, prop) => obj[prop] !== undefined;

// Create an unfrozen version of 'want' in case we need to add
// properties.
const wantObj = { ...want };

keywords.forEach(keyword => {
// check that keyword is not in both 'want' and 'give'.
const wantHas = hasPropDefined(wantObj, keyword);
const giveHas = hasPropDefined(give, keyword);
assert(
!(wantHas && giveHas),
details`a keyword cannot be in both 'want' and 'give'`,
);
// If keyword is in neither, fill in with a 'want' of empty.
if (!(wantHas || giveHas)) {
wantObj[keyword] = amountMathKeywordRecord[keyword].getEmpty();
}
});

return harden({
want: wantObj,
give,
exit,
});
};
22 changes: 11 additions & 11 deletions packages/zoe/src/contracts/atomicSwap.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
/* eslint-disable no-use-before-define */
import harden from '@agoric/harden';

import { makeHelpers } from './helpers/userFlow';
import { makeZoeHelpers } from './helpers/zoeHelpers';

export const makeContract = harden((zoe, terms) => {
const { issuers } = terms;
const { rejectOffer, swap, hasValidPayoutRules } = makeHelpers(zoe, issuers);
export const makeContract = harden(zoe => {
const { swap, assertKeywords, rejectIfNotProposal } = makeZoeHelpers(zoe);
assertKeywords(harden(['Asset', 'Price']));

const makeMatchingInvite = firstInviteHandle => {
const seat = harden({
matchOffer: () => swap(firstInviteHandle, inviteHandle),
});
const {
proposal: { want, give },
} = zoe.getOffer(firstInviteHandle);
const { invite, inviteHandle } = zoe.makeInvite(seat, {
offerMadeRules: zoe.getOffer(firstInviteHandle).payoutRules,
asset: give.Asset,
price: want.Price,
seatDesc: 'matchOffer',
});
return invite;
Expand All @@ -21,11 +25,8 @@ export const makeContract = harden((zoe, terms) => {
const makeFirstOfferInvite = () => {
const seat = harden({
makeFirstOffer: () => {
if (
!hasValidPayoutRules(['offerAtMost', 'wantAtLeast'], inviteHandle)
) {
throw rejectOffer(inviteHandle);
}
const expected = harden({ give: ['Asset'], want: ['Price'] });
rejectIfNotProposal(inviteHandle, expected);
return makeMatchingInvite(inviteHandle);
},
});
Expand All @@ -37,6 +38,5 @@ export const makeContract = harden((zoe, terms) => {

return harden({
invite: makeFirstOfferInvite(),
terms,
});
});
3 changes: 1 addition & 2 deletions packages/zoe/src/contracts/automaticRefund.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import harden from '@agoric/harden';
* sophisticated logic and interfaces.
* @param {contractFacet} zoe - the contract facet of zoe
*/
export const makeContract = harden((zoe, terms) => {
export const makeContract = harden(zoe => {
let offersCount = 0;
const makeSeatInvite = () => {
const seat = harden({
Expand All @@ -30,6 +30,5 @@ export const makeContract = harden((zoe, terms) => {
getOffersCount: () => offersCount,
makeInvite: makeSeatInvite,
},
terms,
});
});
Loading

0 comments on commit 0f050cc

Please sign in to comment.