Skip to content

Commit

Permalink
refactor(zoe): strengthen types around object conversions
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelfig committed Jul 27, 2020
1 parent 9af7903 commit fe20aa4
Show file tree
Hide file tree
Showing 8 changed files with 100 additions and 82 deletions.
18 changes: 14 additions & 4 deletions packages/zoe/src/contractFacet.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { isOfferSafe } from './offerSafety';
import { areRightsConserved } from './rightsConservation';
import { assertKeywordName, cleanProposal, getKeywords } from './cleanProposal';
import { makeContractTables } from './state';
import { filterObj, filterFillAmounts } from './objArrayConversion';
import { filterObj, filterFillAmounts, tuple } from './objArrayConversion';
import { evalContractBundle } from './evalContractCode';

import '../exported';
Expand All @@ -25,14 +25,22 @@ import './internal-types';
* @returns {{ startContract: StartContract }} The returned instance
*/
export function buildRootObject(_vatPowers) {
const visibleInstanceRecordFields = [
'instanceHandle',
// Need to make this variable a tuple type, since technically
// it could be mutated before we pass it to filterObj.
//
// If we want to avoid this type magic, just supply it as the
// verbatim argument of filterObj.
const visibleInstanceRecordFields = tuple(
'handle',
'installationHandle',
'publicAPI',
'terms',
'issuerKeywordRecord',
'brandKeywordRecord',
];
);
/**
* @param {InstanceRecord} instanceRecord
*/
const visibleInstanceRecord = instanceRecord =>
filterObj(instanceRecord, visibleInstanceRecordFields);

Expand All @@ -45,8 +53,10 @@ export function buildRootObject(_vatPowers) {
assert(offerTable.has(offerHandle), details`Offer is not active`),
);

/** @param {OfferRecord} offerRecord */
const removeAmountsAndNotifier = offerRecord =>
filterObj(offerRecord, ['handle', 'instanceHandle', 'proposal']);
/** @param {IssuerRecord} issuerRecord */
const removePurse = issuerRecord =>
filterObj(issuerRecord, ['issuer', 'brand', 'amountMath']);

Expand Down
3 changes: 1 addition & 2 deletions packages/zoe/src/contracts/mintAndSellNFT.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,7 @@ const makeContract = zcf => {
const tokenAmount = tokenAmountMath.make(
harden(
Array(count)
// @ts-ignore
.fill()
.fill(undefined)
.map((_, i) => {
const tokenNumber = i + 1;
return {
Expand Down
42 changes: 22 additions & 20 deletions packages/zoe/src/contracts/multipoolAutoswap.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
/* eslint-disable no-use-before-define */
import makeIssuerKit from '@agoric/ertp';
import { assert, details } from '@agoric/assert';
import { E } from '@agoric/eventual-send';

import { makeTable, makeValidateProperties } from '../table';
import { assertKeywordName } from '../cleanProposal';
Expand Down Expand Up @@ -74,25 +75,27 @@ const makeContract = zcf => {
// tokenIssuer, liquidityMint, liquidityIssuer, tokenKeyword,
// liquidityKeyword, liquidityTokenSupply
const liquidityTable = makeTable(
makeValidateProperties(
harden([
'poolHandle',
'tokenIssuer',
'tokenBrand',
'liquidityMint',
'liquidityIssuer',
'liquidityBrand',
'tokenKeyword',
'liquidityKeyword',
'liquidityTokenSupply',
]),
),
makeValidateProperties([
'poolHandle',
'tokenIssuer',
'tokenBrand',
'liquidityMint',
'liquidityIssuer',
'liquidityBrand',
'tokenKeyword',
'liquidityKeyword',
'liquidityTokenSupply',
]),
'tokenBrand',
);

// Allows users to add new liquidity pools. `newTokenIssuer` and
// `newTokenKeyword` must not have been already used
const addPool = (newTokenIssuer, newTokenKeyword) => {
/**
* Allows users to add new liquidity pools. `newTokenIssuer` and
* `newTokenKeyword` must not have been already used
* @param {Issuer} newTokenIssuer
* @param {Keyword} newTokenKeyword
*/
const addPool = async (newTokenIssuer, newTokenKeyword) => {
assertKeywordName(newTokenKeyword);
const { brandKeywordRecord } = zcf.getInstanceRecord();
const keywords = Object.keys(brandKeywordRecord);
Expand All @@ -101,9 +104,9 @@ const makeContract = zcf => {
!keywords.includes(newTokenKeyword),
details`newTokenKeyword must be unique`,
);
// TODO: handle newTokenIssuer as a potential promise
const newTokenBrand = await E(newTokenIssuer).getBrand();
assert(
!brands.includes(newTokenIssuer.brand),
!brands.includes(newTokenBrand),
details`newTokenIssuer must not be already present`,
);
const newLiquidityKeyword = `${newTokenKeyword}Liquidity`;
Expand All @@ -117,8 +120,7 @@ const makeContract = zcf => {
brand: liquidityBrand,
} = makeIssuerKit(newLiquidityKeyword);

/** @type {Promise<[IssuerRecord, OfferHandle, IssuerRecord]>} */
// @ts-ignore
/** @type {Promise<[Omit<IssuerRecord,'purse'>, OfferHandle, Omit<IssuerRecord,'purse'>]>} */
const promiseAll = Promise.all([
zcf.addNewIssuer(newTokenIssuer, newTokenKeyword),
makeEmptyOffer(),
Expand Down
37 changes: 21 additions & 16 deletions packages/zoe/src/objArrayConversion.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import { assert, details, q } from '@agoric/assert';

/** @type {<T extends string[]>(...args: T) => T} */
export const tuple = (...args) => args;

/**
* @template T
* @template U
* @param {T[]} array
* @param {string[]} keywords
* @param {U[]} keys
*/
export const arrayToObj = (array, keywords) => {
export const arrayToObj = (array, keys) => {
assert(
array.length === keywords.length,
details`array and keywords must be of equal length`,
array.length === keys.length,
details`array and keys must be of equal length`,
);
/** @type {{[Keyword: string]: T}} */
/** @type {{[Keyword: U]: T}} */
const obj = {};
keywords.forEach((keyword, i) => (obj[keyword] = array[i]));
keys.forEach((key, i) => (obj[key] = array[i]));
return obj;
};

Expand All @@ -38,21 +42,22 @@ export const assertSubset = (whole, part) => {
};

/**
* Return a new object with only the keys in subsetKeywords.
* `obj` must have values for all the `subsetKeywords`.
* Return a new object with only the keys in subsetKeys.
* `obj` must have values for all the `subsetKeys`.
* @template T
* @param {Object} obj
* @param {Keyword[]} subsetKeywords
* @returns {T}
* @template {(keyof T)[]} U
* @param {T} obj
* @param {U} subsetKeys
* @returns {Pick<T,U[number]>}
*/
export const filterObj = (obj, subsetKeywords) => {
export const filterObj = (obj, subsetKeys) => {
const newObj = {};
subsetKeywords.forEach(keyword => {
subsetKeys.forEach(key => {
assert(
obj[keyword] !== undefined,
details`obj[keyword] must be defined for keyword ${q(keyword)}`,
obj[key] !== undefined,
details`obj[key] must be defined for keyword ${q(key)}`,
);
newObj[keyword] = obj[keyword];
newObj[key] = obj[key];
});
return newObj;
};
Expand Down
28 changes: 13 additions & 15 deletions packages/zoe/src/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ const makeInstallationTable = () => {
/**
* @type {Validator<InstallationRecord>}
*/
const validateSomewhat = makeValidateProperties(harden(['bundle']));
return makeTable(validateSomewhat, 'installationHandle', () => ({}));
const validateSomewhat = makeValidateProperties(['bundle']);
return makeTable(validateSomewhat, 'installationHandle');
};

// Instance Table key: instanceHandle
Expand All @@ -40,17 +40,15 @@ const makeInstanceTable = () => {
*
* @type {Validator<InstanceRecord & PrivateInstanceRecord>}
*/
const validateSomewhat = makeValidateProperties(
harden([
'installationHandle',
'publicAPI',
'terms',
'issuerKeywordRecord',
'brandKeywordRecord',
'zcfForZoe',
'offerHandles',
]),
);
const validateSomewhat = makeValidateProperties([
'installationHandle',
'publicAPI',
'terms',
'issuerKeywordRecord',
'brandKeywordRecord',
'zcfForZoe',
'offerHandles',
]);

const makeCustomMethods = table => {
const customMethods = harden({
Expand Down Expand Up @@ -188,8 +186,8 @@ const makeIssuerTable = (withPurses = true) => {
*/
const validateSomewhat = makeValidateProperties(
withPurses
? harden(['brand', 'issuer', 'purse', 'amountMath'])
: harden(['brand', 'issuer', 'amountMath']),
? ['brand', 'issuer', 'purse', 'amountMath']
: ['brand', 'issuer', 'amountMath'],
);

const makeCustomMethods = table => {
Expand Down
48 changes: 26 additions & 22 deletions packages/zoe/src/table.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import { assert, details } from '@agoric/assert';
import '../exported';
import './internal-types';

// This definition is used to ensure the proper typing of
// makeCustomMethodsFn.
const DEFAULT_CUSTOM_METHODS = () => ({});
/**
* This definition is used to ensure the proper typing of
* makeCustomMethodsFn.
* @2type {_ => {}}
*/
const DEFAULT_CUSTOM_METHODS = _ => ({});

/**
* Create an opaque handle object.
Expand All @@ -22,22 +25,23 @@ export const makeHandle = handleType => {
};

/**
* @template T,U
* @param {(record: any) => record is T} validateFn
* @param {string} [keyDebugName='Handle'] the debug name for the table key
* @param {(table: Table<T>) => U} makeCustomMethodsFn
* @return {Table<T> & U}
* @template {{}} T
* @template U
* @param {(record: any) => record is U} validateFn
* @param {string} [handleDebugName='Handle'] the debug name for the table key
* @param {(table: Table<U>) => T} [makeCustomMethodsFn=DEFAULT_CUSTOM_METHODS]
* @return {Table<U> & T}
*/
export const makeTable = (
validateFn,
keyDebugName = 'Handle',
handleDebugName = 'Handle',
makeCustomMethodsFn = DEFAULT_CUSTOM_METHODS,
) => {
// The WeakMap that stores the records
/**
* @type {WeakStore<{},T>}
*/
const handleToRecord = makeWeakStore(keyDebugName);
const handleToRecord = makeWeakStore(handleDebugName);

/** @type {Table<T>} */
const table = harden({
Expand Down Expand Up @@ -76,27 +80,27 @@ export const makeTable = (

/**
* @template T
* @param {string[]} param0
* @returns {Validator<T>}
* @template {(keyof T)[]} U
* @param {U} expectedProperties
* @returns {Validator<Record<U[number]|'handle',any>>}
*/
export const makeValidateProperties = ([...expectedProperties]) => {
// add handle to expected properties
expectedProperties.push('handle');
// Sorts in-place
expectedProperties.sort();
harden(expectedProperties);
export const makeValidateProperties = expectedProperties => {
// add handle to properties to check
const checkSet = new Set(expectedProperties);
checkSet.add('handle');
const checkProperties = harden([...checkSet.values()].sort());
return obj => {
const actualProperties = Object.getOwnPropertyNames(obj);
actualProperties.sort();
assert(
actualProperties.length === expectedProperties.length,
actualProperties.length === checkProperties.length,
details`the actual properties (${actualProperties}) did not match the \
expected properties (${expectedProperties})`,
expected properties (${checkProperties})`,
);
for (let i = 0; i < actualProperties.length; i += 1) {
assert(
expectedProperties[i] === actualProperties[i],
details`property '${expectedProperties[i]}' did not equal actual property '${actualProperties[i]}'`,
checkProperties[i] === actualProperties[i],
details`property '${checkProperties[i]}' did not equal actual property '${actualProperties[i]}'`,
);
}
return true;
Expand Down
2 changes: 1 addition & 1 deletion packages/zoe/src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@
* when the issuer is added and ready.
* @param {Promise<Issuer>|Issuer} issuerP Promise for issuer
* @param {Keyword} keyword Keyword for added issuer
* @returns {Promise<IssuerRecord>} Issuer is added and ready
* @returns {Promise<Omit<IssuerRecord,'purse'>>} Issuer is added and ready
*
* @typedef {Record<string,function>} PublicAPI
*
Expand Down
4 changes: 2 additions & 2 deletions packages/zoe/test/unitTests/test-objArrayConversion.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ test('arrayToObj', t => {
const keywords2 = ['X', 'Y', 'Z'];
t.throws(
() => arrayToObj(array, keywords2),
/Error: array and keywords must be of equal length/,
/Error: array and keys must be of equal length/,
`unequal length should throw`,
);

const array2 = [4, 5, 2];
t.throws(
() => arrayToObj(array2, keywords),
/Error: array and keywords must be of equal length/,
/Error: array and keys must be of equal length/,
`unequal length should throw`,
);
} catch (e) {
Expand Down

0 comments on commit fe20aa4

Please sign in to comment.