Skip to content

Commit

Permalink
fix: minting allocations in zcf (#1382)
Browse files Browse the repository at this point in the history
  • Loading branch information
erights authored and michaelfig committed Aug 9, 2020
1 parent 8984c39 commit 017c85d
Show file tree
Hide file tree
Showing 10 changed files with 293 additions and 98 deletions.
2 changes: 1 addition & 1 deletion packages/ERTP/src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@
/**
* @callback MakeIssuerKit
* @param {string} allegedName
* @param {string} mathHelperName
* @param {MathHelpersName} mathHelperName
* @returns {IssuerKit<BrandName>}
*
* The allegedName is useful for debugging and double-checking
Expand Down
3 changes: 2 additions & 1 deletion packages/eventual-send/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ export function makeHandledPromise(Promise) {
* https://en.wikipedia.org/wiki/Disjoint-set_data_structure
*
* @param {*} target Any value.
* @returns {*} If the target was a HandledPromise, the most-resolved parent of it, otherwise the target.
* @returns {*} If the target was a HandledPromise, the most-resolved parent
* of it, otherwise the target.
*/
function shorten(target) {
let p = target;
Expand Down
89 changes: 85 additions & 4 deletions packages/zoe/src/contractFacet/contractFacet.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ import { assert, details } from '@agoric/assert';
import { E } from '@agoric/eventual-send';
import makeWeakStore from '@agoric/weak-store';

import makeAmountMath from '@agoric/ertp/src/amountMath';
import { areRightsConserved } from './rightsConservation';
import { makeIssuerTable } from '../issuerTable';
import { assertKeywordName, getKeywords } from '../zoeService/cleanProposal';
import { evalContractBundle } from './evalContractCode';
import { makeSeatAdmin } from './seat';
import { makeExitObj } from './exit';
import { objectMap } from '../objArrayConversion';

import '../../exported';
import '../internal-types';
Expand All @@ -35,6 +37,8 @@ export function buildRootObject() {
const getAmountMath = brand => issuerTable.get(brand).amountMath;

const invitationHandleToHandler = makeWeakStore('invitationHandle');

/** @type WeakStore<ZCFSeat,ZCFSeatAdmin> */
const seatToZCFSeatAdmin = makeWeakStore('seat');

const issuers = Object.values(instanceRecord.issuerKeywordRecord);
Expand All @@ -44,7 +48,83 @@ export function buildRootObject() {

await getPromiseForIssuerRecords(issuers);

const allSeatStagings = new Set();
const allSeatStagings = new WeakSet();

/** @type MakeZCFMint */
const makeZCFMint = async (keyword, mathHelperName = 'nat') => {
const zoeMintP = E(zoeInstanceAdmin).makeZoeMint(keyword, mathHelperName);
const mintyIssuerRecord = await E(zoeMintP).getIssuerRecord();
// AWAIT
const { brand: mintyBrand } = mintyIssuerRecord;
const mintyAmountMath = makeAmountMath(mintyBrand, mathHelperName);
// TODO some zcf-side registration of the new issuer

/** @type ZCFMint */
const zcfMint = harden({
getIssuerRecord: () => {
return mintyIssuerRecord;
},
mintGains: (gains, zcfSeat = undefined) => {
assert(
zcfSeat !== undefined,
details`On demand seat creation not yet implemented`,
);
let totalToMint = mintyAmountMath.getEmpty();
const oldAllocation = zcfSeat.getCurrentAllocation();
const updates = objectMap(gains, ([seatKeyword, amountToAdd]) => {
totalToMint = mintyAmountMath.add(totalToMint, amountToAdd);
const oldAmount = oldAllocation[seatKeyword];
// oldAmount being absent is equivalent to empty.
const newAmount = oldAmount
? mintyAmountMath.add(oldAmount, amountToAdd)
: amountToAdd;
return [seatKeyword, newAmount];
});
const newAllocation = harden({
...oldAllocation,
...updates,
});
const zcfSeatAdmin = seatToZCFSeatAdmin.get(zcfSeat);
// verifies offer safety
const seatStaging = zcfSeat.stage(newAllocation);
// No effects above. Commit point. Must mint and commit atomically.
// (Not really. If we minted only, no one would ever get those
// invisibly-minted assets.)
E(zoeMintP).mintGains(totalToMint);
zcfSeatAdmin.commit(seatStaging);
return zcfSeat;
},
burnLosses: (losses, zcfSeat) => {
let totalToBurn = mintyAmountMath.getEmpty();
const oldAllocation = zcfSeat.getCurrentAllocation();
const updates = objectMap(
losses,
([seatKeyword, amountToSubtract]) => {
totalToBurn = mintyAmountMath.add(totalToBurn, amountToSubtract);
const oldAmount = oldAllocation[seatKeyword];
const newAmount = mintyAmountMath.subtract(
oldAmount,
amountToSubtract,
);
return [seatKeyword, newAmount];
},
);
const newAllocation = harden({
...oldAllocation,
...updates,
});
const zcfSeatAdmin = seatToZCFSeatAdmin.get(zcfSeat);
// verifies offer safety
const seatStaging = zcfSeat.stage(newAllocation);
// No effects above. Commit point. Must commit and burn atomically.
// (Not really. If we committed only, no one would ever get the
// unburned assets.)
zcfSeatAdmin.commit(seatStaging);
E(zoeMintP).burnLosses(totalToBurn);
},
});
return zcfMint;
};

/** @type ContractFacet */
const zcf = {
Expand Down Expand Up @@ -123,9 +203,8 @@ export function buildRootObject() {
'string',
details`invitations must have a description string: ${description}`,
);
/** @type {InvitationHandle} */
const invitationHandle = {};
harden(invitationHandle);

const invitationHandle = /** @type {InvitationHandle} */ (harden({}));
invitationHandleToHandler.init(invitationHandle, offerHandler);
/** @type {Promise<Payment<'ZoeInvitation'>>} */
const invitationP = E(zoeInstanceAdmin).makeInvitation(
Expand All @@ -145,6 +224,7 @@ export function buildRootObject() {
getBrandForIssuer: issuer =>
issuerTable.getIssuerRecordByIssuer(issuer).brand,
getAmountMath,
makeZCFMint,
};
harden(zcf);

Expand All @@ -161,6 +241,7 @@ export function buildRootObject() {
);
seatToZCFSeatAdmin.init(zcfSeat, zcfSeatAdmin);
const offerHandler = invitationHandleToHandler.get(invitationHandle);
// @ts-ignore
const offerResultP = E(offerHandler)(zcfSeat).catch(err => {
zcfSeat.exit();
throw err;
Expand Down
107 changes: 47 additions & 60 deletions packages/zoe/src/contracts/mintPayments.js
Original file line number Diff line number Diff line change
@@ -1,75 +1,62 @@
/* eslint-disable no-use-before-define */
// @ts-check

import makeIssuerKit from '@agoric/ertp';
import { escrowAndAllocateTo } from '../contractSupport';

import '../../exported';

/**
* This is a very simple contract that creates a new issuer and mints payments
* from it, in order to give an example of how that can be done. This contract
* sends new tokens to anyone who requests them.
* sends new tokens to anyone who has an invitation.
*
* Offer safety is not enforced here: the expectation is that most contracts
* that want to do something similar would use the ability to mint new payments
* internally rather than sharing that ability widely as this one does.
* The expectation is that most contracts that want to do something similar
* would use the ability to mint new payments internally rather than sharing
* that ability widely as this one does.
*
* makeInstance returns an invitation that, when exercised, provides 1000 of the
* new tokens. await E(publicAPI).makeInvite() returns an invitation that accepts an
* empty offer and provides 1000 tokens.
* To pay others in tokens, the creator of the instance can make
* invitations for them, which when used to make an offer, will payout
* the specified amount of tokens.
*
* @param {ContractFacet} zcf
* @type {ContractStartFn}
*/
const start = (zcf, _terms) => {
// Create the internal token mint for a fungible digital asset
const { issuer, mint, amountMath } = makeIssuerKit('tokens');

// We need to tell Zoe about this issuer and add a keyword for the
// issuer. Let's call this the 'Token' issuer.
return zcf.saveIssuer(issuer, 'Token').then(() => {
// We need to wait for the promise to resolve (meaning that Zoe
// has done the work of adding a new issuer).
const mintPayment = (extent = 1000) => seat => {
const amount = amountMath.make(extent);
const payment = mint.mintPayment(amount);

// Let's use a helper function which escrows the payment with
// Zoe, and reallocates to the recipientHandle.
return escrowAndAllocateTo(
zcf,
seat,
{ Token: amount },
{ Token: payment },
).then(() => {
// Exit the seat so the user gets a payout
seat.exit();

// Since the user is getting the payout through Zoe, we can
// return anything here. Let's return some helpful instructions.
return 'Offer completed. You should receive a payment from Zoe';
});
};

const creatorFacet = {
// The creator of the instance can send invitations to anyone
// they wish to.
makeInvitation: extent =>
zcf.makeInvitation(mintPayment(extent), 'mint a payment'),
getTokenIssuer: () => issuer,
};

const publicFacet = {
// Make the token issuer public. Note that only the mint can
// make new digital assets. The issuer is ok to make public.
getTokenIssuer: () => issuer,
};

// Return the creatorFacet to the creator, so they can make
// invitations for others to get payments of tokens. Publish the
// publicFacet.
return harden({ creatorFacet, publicFacet });
});
const start = async (zcf, _terms) => {
// Create the internal token mint for a fungible digital asset. Note
// that 'Tokens' is both the keyword and the allegedName.
const zcfMint = await zcf.makeZCFMint('Tokens');
// AWAIT

// Now ZCF has saved the issuer, brand, and local amountMath so that they
// can be accessed synchronously.
const { amountMath, issuer } = zcfMint.getIssuerRecord();

const mintPayment = (extent = 1000) => seat => {
const amount = amountMath.make(extent);
// Synchronously mint and allocate amount to seat.
zcfMint.mintGains({ Token: amount }, seat);
// Exit the seat so that the user gets a payout.
seat.exit();
// Since the user is getting the payout through Zoe, we can
// return anything here. Let's return some helpful instructions.
return 'Offer completed. You should receive a payment from Zoe';
};

const creatorFacet = {
// The creator of the instance can send invitations to anyone
// they wish to.
makeInvitation: extent =>
zcf.makeInvitation(mintPayment(extent), 'mint a payment'),
getTokenIssuer: () => issuer,
};

const publicFacet = {
// Make the token issuer public. Note that only the mint can
// make new digital assets. The issuer is ok to make public.
getTokenIssuer: () => issuer,
};

// Return the creatorFacet to the creator, so they can make
// invitations for others to get payments of tokens. Publish the
// publicFacet.
return harden({ creatorFacet, publicFacet });
};

harden(start);
Expand Down
46 changes: 34 additions & 12 deletions packages/zoe/src/internal-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@
/**
* @typedef {Object} ZoeSeatAdmin
* @property {() => void} exit - exit seat
* @property {(replacementAllocation: Allocation) => void} replaceAllocation - replace the
* currentAllocation with this allocation
* @property {(replacementAllocation: Allocation) => void} replaceAllocation
* - replace the currentAllocation with this allocation
*/

/**
Expand All @@ -65,9 +65,10 @@
/**
* Make the ZCF seat and seat admin
* @callback MakeSeatAdmin
* @param {Set<SeatStaging>} allSeatStagings - a set of valid
* @param {WeakSet<SeatStaging>} allSeatStagings - a set of valid
* seatStagings where allocations have been checked for offerSafety
* @param {ZoeSeatAdmin} zoeSeatAdmin - a presence from Zoe such that ZCF can tell Zoe
* @param {ZoeSeatAdmin} zoeSeatAdmin
* - a presence from Zoe such that ZCF can tell Zoe
* about seat events
* @param {SeatData} seatData - pass-by-copy data to use to make the seat
* @param {(brand: Brand) => AmountMath} getAmountMath
Expand All @@ -82,8 +83,10 @@

/**
* @typedef {Object} InstanceAdmin
* @property {(invitationHandle: InvitationHandle, zoeSeatAdmin:
* ZoeSeatAdmin, seatData: SeatData) => Promise<AddSeatResult>} addZoeSeatAdmin
* @property {(invitationHandle: InvitationHandle,
* zoeSeatAdmin: ZoeSeatAdmin,
* seatData: SeatData,
* ) => Promise<AddSeatResult>} addZoeSeatAdmin
* @property {(zoeSeatAdmin: ZoeSeatAdmin) => void} removeZoeSeatAdmin
* @property {() => Instance} getInstance
* @property {() => PublicFacet} getPublicFacet
Expand All @@ -94,17 +97,36 @@

/**
* @typedef {Object} AddSeatObj
* @property {(invitationHandle: InvitationHandle, zoeSeatAdmin:
* ZoeSeatAdmin, seatData: SeatData) => AddSeatResult} addSeat
* @property {(invitationHandle: InvitationHandle,
* zoeSeatAdmin: ZoeSeatAdmin,
* seatData: SeatData,
* ) => AddSeatResult} addSeat
*/

/**
* @typedef {Object} ZoeInstanceAdmin
* @property {(invitationHandle: InvitationHandle, description:
* string, customProperties?: {}) => Payment<'ZoeInvitation'>}
* makeInvitation
* @property {(invitationHandle: InvitationHandle,
* description: string,
* customProperties?: {},
* ) => Payment<'ZoeInvitation'>} makeInvitation
* @property {() => void} shutdown
* @property {(issuerP: Issuer|Promise<Issuer>, keyword: Keyword) => void} saveIssuer
* @property {(issuerP: ERef<Issuer>, keyword: Keyword) => void} saveIssuer
*
* @property {MakeZoeMint} makeZoeMint
*/

/**
* @callback MakeZoeMint
* @param {Keyword} keyword
* @param {MathHelpersName=} mathHelperName
* @returns {ZoeMint}
*/

/**
* @typedef {Object} ZoeMint
* @property {() => IssuerRecord} getIssuerRecord
* @property {(totalToMint: Amount) => void} mintGains
* @property {(totalToBurn: Amount) => void} burnLosses
*/

/**
Expand Down
Loading

0 comments on commit 017c85d

Please sign in to comment.