diff --git a/package.json b/package.json index 540ccbf2518f..05bd86e76f30 100644 --- a/package.json +++ b/package.json @@ -65,5 +65,8 @@ "lint-check": "yarn workspaces run lint-check", "test": "yarn workspaces run test", "build": "yarn workspaces run build" + }, + "dependencies": { + "@agoric/store": "^0.1.2" } } diff --git a/packages/zoe/src/state.js b/packages/zoe/src/state.js index be592cc59f12..7986f2a9ec82 100644 --- a/packages/zoe/src/state.js +++ b/packages/zoe/src/state.js @@ -96,66 +96,79 @@ const makeOfferTable = () => { const makePayoutMap = makeStore; // Issuer Table -// Columns: issuer | brand | purse | amountMath +// Columns: brand | issuer | purse | amountMath +// +// The IssuerTable is keyed by brand, but the Issuer is required in order for +// getPromiseForIssuerRecord() to initialize the records. When +// getPromiseForIssuerRecord is called and the record doesn't exist, it stores a +// promise for the record in issuersInProgress, and then builds the record. It +// updates the tables when done. const makeIssuerTable = () => { // TODO: make sure this validate function protects against malicious - // misshapen objects rather than just a general check. + // misshapen objects rather than just a general check. const validateSomewhat = makeValidateProperties( - harden(['issuer', 'brand', 'purse', 'amountMath']), + harden(['brand', 'issuer', 'purse', 'amountMath']), ); const makeCustomMethods = table => { const issuersInProgress = makeStore(); + const issuerToBrand = makeStore(); + + // We can't be sure we can build the table entry soon enough that the first + // caller will get the actual data, so we start by saving a promise in the + // inProgress table, and once we have the Issuer, build the record, fill in + // the table, and resolve the promise. + function buildTableEntryAndPlaceHolder(issuer) { + // remote calls which immediately return a promise + const mathHelpersNameP = E(issuer).getMathHelpersName(); + const brandP = E(issuer).getBrand(); + const purseP = E(issuer).makeEmptyPurse(); + + // a promise for a synchronously accessible record + const synchronousRecordP = Promise.all([ + brandP, + mathHelpersNameP, + purseP, + ]).then(([brand, mathHelpersName, purse]) => { + const amountMath = makeAmountMath(brand, mathHelpersName); + const issuerRecord = { + brand, + issuer, + purse, + amountMath, + }; + table.create(issuerRecord, brand); + issuerToBrand.init(issuer, brand); + issuersInProgress.delete(issuer); + return table.get(brand); + }); + issuersInProgress.init(issuer, synchronousRecordP); + return synchronousRecordP; + } const customMethods = harden({ - getPurseKeywordRecord: issuerKeywordRecord => { - const purseKeywordRecord = {}; - Object.keys(issuerKeywordRecord).forEach(keyword => { - purseKeywordRecord[keyword] = table.get( - issuerKeywordRecord[keyword], - ).purse; - }); - return harden(purseKeywordRecord); - }, - - // `issuerP` may be a promise, presence, or local object + // `issuerP` may be a promise, presence, or local object. If there's + // already a record, or already a promise for a record, return it. + // Otherwise wrap a promise around building the record so we can return + // the promise until we build the record. getPromiseForIssuerRecord: issuerP => { return Promise.resolve(issuerP).then(issuer => { - if (!table.has(issuer)) { - if (issuersInProgress.has(issuer)) { - // a promise which resolves to the issuer record - return issuersInProgress.get(issuer); - } - // remote calls which immediately return a promise - const mathHelpersNameP = E(issuer).getMathHelpersName(); - const brandP = E(issuer).getBrand(); - const purseP = E(issuer).makeEmptyPurse(); - - // a promise for a synchronously accessible record - const synchronousRecordP = Promise.all([ - brandP, - mathHelpersNameP, - purseP, - ]).then(([brand, mathHelpersName, purse]) => { - const amountMath = makeAmountMath(brand, mathHelpersName); - const issuerRecord = { - issuer, - brand, - purse, - amountMath, - }; - table.create(issuerRecord, issuer); - issuersInProgress.delete(issuer); - return table.get(issuer); - }); - issuersInProgress.init(issuer, synchronousRecordP); - return synchronousRecordP; + if (issuerToBrand.has(issuer)) { + // we always initialize table and issuerToBrand together + return table.get(issuerToBrand.get(issuer)); + // eslint-disable-next-line no-else-return + } else if (issuersInProgress.has(issuer)) { + // a promise which resolves to the issuer record + return issuersInProgress.get(issuer); + // eslint-disable-next-line no-else-return + } else { + return buildTableEntryAndPlaceHolder(issuer); } - return table.get(issuer); }); }, getPromiseForIssuerRecords: issuerPs => Promise.all(issuerPs.map(customMethods.getPromiseForIssuerRecord)), + brandFromIssuer: issuer => issuerToBrand.get(issuer), }); return customMethods; }; diff --git a/packages/zoe/src/zoe.js b/packages/zoe/src/zoe.js index 1523860c8fa7..30a954648c57 100644 --- a/packages/zoe/src/zoe.js +++ b/packages/zoe/src/zoe.js @@ -336,6 +336,8 @@ const makeZoe = (additionalEndowments = {}) => { issuerTable, } = makeTables(); + const getAmountMathForBrand = brand => issuerTable.get(brand).amountMath; + /** * @param {InstanceHandle} instanceHandle * @param {OfferHandle[]} offerHandles @@ -347,36 +349,33 @@ const makeZoe = (additionalEndowments = {}) => { } const offerRecords = offerTable.getOffers(offerHandles); - const { issuerKeywordRecord } = instanceTable.get(instanceHandle); - // Remove the offers from the offerTable so that they are no // longer active. offerTable.deleteOffers(offerHandles); // Resolve the payout promises with promises for the payouts - const pursePKeywordRecord = issuerTable.getPurseKeywordRecord( - issuerKeywordRecord, - ); for (const offerRecord of offerRecords) { const payout = {}; Object.keys(offerRecord.currentAllocation).forEach(keyword => { - payout[keyword] = E(pursePKeywordRecord[keyword]).withdraw( - offerRecord.currentAllocation[keyword], - ); + const payoutAmount = offerRecord.currentAllocation[keyword]; + const { purse } = issuerTable.get(payoutAmount.brand); + payout[keyword] = E(purse).withdraw(payoutAmount); }); harden(payout); payoutMap.get(offerRecord.handle).resolve(payout); } }; + // presumes global keywords const getAmountMaths = (instanceHandle, sparseKeywords) => { const amountMathKeywordRecord = /** @type {Object.} */ ({}); const { issuerKeywordRecord } = instanceTable.get(instanceHandle); + // this method presumes that issuers have all been retrieved by this point sparseKeywords.forEach(keyword => { - const issuer = issuerKeywordRecord[keyword]; - amountMathKeywordRecord[keyword] = issuerTable.get(issuer).amountMath; + const brand = issuerTable.brandFromIssuer(issuerKeywordRecord[keyword]); + amountMathKeywordRecord[keyword] = issuerTable.get(brand).amountMath; }); - return harden(amountMathKeywordRecord); + return amountMathKeywordRecord; }; const removePurse = issuerRecord => @@ -517,7 +516,7 @@ const makeZoe = (additionalEndowments = {}) => { makePotentialReallocation(offerHandle, newAllocations[i]), ); - // 3) save the reallocation + // 3. Save the reallocations. offerTable.updateAmounts(offerHandles, reallocations); }, @@ -616,7 +615,9 @@ const makeZoe = (additionalEndowments = {}) => { ); }, getInstanceRecord: () => instanceTable.get(instanceHandle), - getIssuerRecord: issuer => removePurse(issuerTable.get(issuer)), + getAmountMathForBrand, + getIssuerRecord: issuer => + removePurse(issuerTable.get(issuerTable.brandFromIssuer(issuer))), }); return contractFacet; }; @@ -783,7 +784,7 @@ const makeZoe = (additionalEndowments = {}) => { getKeywords(issuerKeywordRecord), ); - proposal = cleanProposal( + const cleanedProposal = cleanProposal( issuerKeywordRecord, amountMathKeywordRecord, proposal, @@ -791,8 +792,8 @@ const makeZoe = (additionalEndowments = {}) => { // Promise flow: // issuer -> purse -> deposit payment -> offerHook -> payout - const giveKeywords = Object.getOwnPropertyNames(proposal.give); - const wantKeywords = Object.getOwnPropertyNames(proposal.want); + const giveKeywords = Object.getOwnPropertyNames(cleanedProposal.give); + const wantKeywords = Object.getOwnPropertyNames(cleanedProposal.want); const userKeywords = harden([...giveKeywords, ...wantKeywords]); const paymentDepositedPs = userKeywords.map(keyword => { const issuer = issuerKeywordRecord[keyword]; @@ -805,9 +806,9 @@ const makeZoe = (additionalEndowments = {}) => { return E(purse) .deposit( paymentKeywordRecord[keyword], - proposal.give[keyword], + cleanedProposal.give[keyword], ) - .then(_ => proposal.give[keyword]); + .then(_ => cleanedProposal.give[keyword]); } // If any other payments are included, they are ignored. return Promise.resolve( @@ -820,7 +821,7 @@ const makeZoe = (additionalEndowments = {}) => { const notifierRec = produceNotifier(); const offerImmutableRecord = { instanceHandle, - proposal, + proposal: cleanedProposal, currentAllocation: arrayToObj(amountsArray, userKeywords), notifier: notifierRec.notifier, updater: notifierRec.updater, @@ -841,7 +842,7 @@ const makeZoe = (additionalEndowments = {}) => { payout: payoutMap.get(offerHandle).promise, outcome: outcomeP, }; - const { exit } = proposal; + const { exit } = cleanedProposal; const [exitKind] = Object.getOwnPropertyNames(exit); // Automatically cancel on deadline. if (exitKind === 'afterDeadline') { diff --git a/packages/zoe/test/unitTests/contractSupport/test-zoeHelpers.js b/packages/zoe/test/unitTests/contractSupport/test-zoeHelpers.js index e0fc7a4a2b50..b58271358939 100644 --- a/packages/zoe/test/unitTests/contractSupport/test-zoeHelpers.js +++ b/packages/zoe/test/unitTests/contractSupport/test-zoeHelpers.js @@ -448,7 +448,7 @@ test('ZoeHelpers canTradeWith', t => { test('ZoeHelpers swap ok', t => { t.plan(4); - const { moolaR, simoleanR, moola, simoleans } = setup(); + const { moolaR, simoleanR, moola, simoleans, amountMaths } = setup(); const leftOfferHandle = harden({}); const rightOfferHandle = harden({}); const cantTradeRightOfferHandle = harden({}); @@ -465,6 +465,7 @@ test('ZoeHelpers swap ok', t => { }, keywords: ['Asset', 'Price'], }), + getAmountMathForBrand: brand => amountMaths.get(brand.getAllegedName()), getAmountMaths: () => harden({ Asset: moolaR.amountMath, Price: simoleanR.amountMath }), getZoeService: () => {},