Skip to content

Commit dcc9ba3

Browse files
authored
feat: allow sparse keywords (#812)
Zoe currently has the concept of keywords which are used as indexes in place of the array indexes that we had previously. The keywords are used to match proposal elements, payments, amounts in the offerRecord within Zoe, and payouts to the user. Keywords are per contract, and are currently objective: they are the same for all users of a contract. We expect that future contracts (specifically multipool Autoswap #391 ) will have many keywords, potentially hundreds. Therefore, we want to ensure that the keywordRecords (records using keywords as keys) throughout Zoe are sparse, much sparser than all the keywords for a contract.
1 parent 29b5399 commit dcc9ba3

14 files changed

+457
-325
lines changed

packages/zoe/src/cleanProposal.js

+79-64
Original file line numberDiff line numberDiff line change
@@ -2,89 +2,109 @@ import harden from '@agoric/harden';
22
import { assert, details } from '@agoric/assert';
33
import { mustBeComparable } from '@agoric/same-structure';
44

5-
import { arrayToObj } from './objArrayConversion';
5+
import { arrayToObj, assertSubset } from './objArrayConversion';
66

7-
// cleanProposal checks the keys and values of the proposal, including
8-
// the keys and values of the internal objects. The proposal may have
9-
// the following keys: `give`, `want`, and `exit`. These keys may be
10-
// omitted in the `proposal` argument passed to cleanProposal, but
11-
// anything other than these keys is not allowed. The values of `give`
12-
// and `want` must be "amountKeywordRecords", meaning that the keys
13-
// must be keywords and the values must be amounts. The value of
14-
// `exit`, if present, must be a record of one of the following forms:
15-
// `{ waived: null }` `{ onDemand: null }` `{ afterDeadline: { timer
16-
// :Timer, deadline :Number } }
7+
export const assertCapASCII = keyword => {
8+
assert.typeof(keyword, 'string');
9+
const firstCapASCII = /^[A-Z][a-zA-Z0-9_$]*$/;
10+
assert(
11+
firstCapASCII.test(keyword),
12+
details`keyword ${keyword} must be ascii and must start with a capital letter.`,
13+
);
14+
};
1715

18-
// Assert that the keys of record, if present, are in expectedKeys.
19-
// Return the keys after asserting this.
20-
const checkKeys = (expectedKeys, record) => {
21-
// Assert that keys, if present, match expectedKeys.
16+
// Assert that the keys of `record` are all in `allowedKeys`. If a key
17+
// of `record` is not in `allowedKeys`, throw an error. If a key in
18+
// `allowedKeys` is not a key of record, we do not throw an error.
19+
const assertKeysAllowed = (allowedKeys, record) => {
2220
const keys = Object.getOwnPropertyNames(record);
23-
keys.forEach(key => {
24-
assert.typeof(key, 'string');
25-
assert(
26-
expectedKeys.includes(key),
27-
details`key ${key} was not an expected key`,
28-
);
29-
});
21+
assertSubset(allowedKeys, keys);
3022
// assert that there are no symbol properties.
3123
assert(
3224
Object.getOwnPropertySymbols(record).length === 0,
3325
details`no symbol properties allowed`,
3426
);
35-
return keys;
3627
};
3728

38-
const coerceAmountKeywordRecordValues = (
29+
const cleanKeys = (allowedKeys, record) => {
30+
assertKeysAllowed(allowedKeys, record);
31+
return Object.getOwnPropertyNames(record);
32+
};
33+
34+
export const getKeywords = keywordRecord =>
35+
harden(Object.getOwnPropertyNames(keywordRecord));
36+
37+
export const coerceAmountKeywordRecord = (
3938
amountMathKeywordRecord,
40-
validatedKeywords,
39+
allKeywords,
4140
allegedAmountKeywordRecord,
4241
) => {
42+
const sparseKeywords = cleanKeys(allKeywords, allegedAmountKeywordRecord);
4343
// Check that each value can be coerced using the amountMath indexed
4444
// by keyword. `AmountMath.coerce` throws if coercion fails.
45-
const coercedAmounts = validatedKeywords.map(keyword =>
45+
const coercedAmounts = sparseKeywords.map(keyword =>
4646
amountMathKeywordRecord[keyword].coerce(
4747
allegedAmountKeywordRecord[keyword],
4848
),
4949
);
5050

5151
// Recreate the amountKeywordRecord with coercedAmounts.
52-
return arrayToObj(coercedAmounts, validatedKeywords);
52+
return arrayToObj(coercedAmounts, sparseKeywords);
5353
};
5454

55-
export const coerceAmountKeywordRecord = (
56-
amountMathKeywordRecord,
57-
keywords,
58-
allegedAmountKeywordRecord,
59-
) => {
60-
const validatedKeywords = checkKeys(keywords, allegedAmountKeywordRecord);
61-
return coerceAmountKeywordRecordValues(
62-
amountMathKeywordRecord,
63-
validatedKeywords,
64-
allegedAmountKeywordRecord,
55+
export const cleanKeywords = keywordRecord => {
56+
// `getOwnPropertyNames` returns all the non-symbol properties
57+
// (both enumerable and non-enumerable).
58+
const keywords = Object.getOwnPropertyNames(keywordRecord);
59+
60+
// Insist that there are no symbol properties.
61+
assert(
62+
Object.getOwnPropertySymbols(keywordRecord).length === 0,
63+
details`no symbol properties allowed`,
6564
);
65+
66+
// Assert all key characters are ascii and keys start with a
67+
// capital letter.
68+
keywords.forEach(assertCapASCII);
69+
70+
return keywords;
6671
};
6772

68-
export const cleanProposal = (keywords, amountMathKeywordRecord, proposal) => {
69-
const expectedRootKeys = ['want', 'give', 'exit'];
73+
// cleanProposal checks the keys and values of the proposal, including
74+
// the keys and values of the internal objects. The proposal may have
75+
// the following keys: `give`, `want`, and `exit`. These keys may be
76+
// omitted in the `proposal` argument passed to cleanProposal, but
77+
// anything other than these keys is not allowed. The values of `give`
78+
// and `want` must be "amountKeywordRecords", meaning that the keys
79+
// must be keywords and the values must be amounts. The value of
80+
// `exit`, if present, must be a record of one of the following forms:
81+
// `{ waived: null }` `{ onDemand: null }` `{ afterDeadline: { timer
82+
// :Timer, deadline :Number } }
83+
export const cleanProposal = (
84+
issuerKeywordRecord,
85+
amountMathKeywordRecord,
86+
proposal,
87+
) => {
88+
const rootKeysAllowed = ['want', 'give', 'exit'];
7089
mustBeComparable(proposal);
71-
checkKeys(expectedRootKeys, proposal);
90+
assertKeysAllowed(rootKeysAllowed, proposal);
7291

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

77-
want = coerceAmountKeywordRecord(amountMathKeywordRecord, keywords, want);
78-
give = coerceAmountKeywordRecord(amountMathKeywordRecord, keywords, give);
96+
const allKeywords = getKeywords(issuerKeywordRecord);
97+
want = coerceAmountKeywordRecord(amountMathKeywordRecord, allKeywords, want);
98+
give = coerceAmountKeywordRecord(amountMathKeywordRecord, allKeywords, give);
7999

80100
// Check exit
81101
assert(
82102
Object.getOwnPropertyNames(exit).length === 1,
83103
details`exit ${proposal.exit} should only have one key`,
84104
);
85105
// We expect the single exit key to be one of the following:
86-
const expectedExitKeys = ['onDemand', 'afterDeadline', 'waived'];
87-
const [exitKey] = checkKeys(expectedExitKeys, exit);
106+
const allowedExitKeys = ['onDemand', 'afterDeadline', 'waived'];
107+
const [exitKey] = cleanKeys(allowedExitKeys, exit);
88108
if (exitKey === 'onDemand' || exitKey === 'waived') {
89109
assert(
90110
exit[exitKey] === null,
@@ -93,37 +113,32 @@ export const cleanProposal = (keywords, amountMathKeywordRecord, proposal) => {
93113
}
94114
if (exitKey === 'afterDeadline') {
95115
const expectedAfterDeadlineKeys = ['timer', 'deadline'];
96-
checkKeys(expectedAfterDeadlineKeys, exit.afterDeadline);
116+
assertKeysAllowed(expectedAfterDeadlineKeys, exit.afterDeadline);
117+
assert(
118+
exit.afterDeadline.timer !== undefined,
119+
details`timer must be defined`,
120+
);
121+
assert(
122+
exit.afterDeadline.deadline !== undefined,
123+
details`deadline must be defined`,
124+
);
97125
// timers must have a 'setWakeup' function which takes a deadline
98126
// and an object as arguments.
99127
// TODO: document timer interface
100128
// https://github.com/Agoric/agoric-sdk/issues/751
101129
// TODO: how to check methods on presences?
102130
}
103131

104-
const hasPropDefined = (obj, prop) => obj[prop] !== undefined;
132+
// check that keyword is not in both 'want' and 'give'.
133+
const wantKeywordSet = new Set(Object.getOwnPropertyNames(want));
134+
const giveKeywords = Object.getOwnPropertyNames(give);
105135

106-
// Create an unfrozen version of 'want' in case we need to add
107-
// properties.
108-
const wantObj = { ...want };
109-
110-
keywords.forEach(keyword => {
111-
// check that keyword is not in both 'want' and 'give'.
112-
const wantHas = hasPropDefined(wantObj, keyword);
113-
const giveHas = hasPropDefined(give, keyword);
136+
giveKeywords.forEach(keyword => {
114137
assert(
115-
!(wantHas && giveHas),
138+
!wantKeywordSet.has(keyword),
116139
details`a keyword cannot be in both 'want' and 'give'`,
117140
);
118-
// If keyword is in neither, fill in with a 'want' of empty.
119-
if (!(wantHas || giveHas)) {
120-
wantObj[keyword] = amountMathKeywordRecord[keyword].getEmpty();
121-
}
122141
});
123142

124-
return harden({
125-
want: wantObj,
126-
give,
127-
exit,
128-
});
143+
return harden({ want, give, exit });
129144
};

packages/zoe/src/contracts/autoswap.js

+29-28
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ export const makeContract = harden(zoe => {
1818
let liqTokenSupply = 0;
1919

2020
return zoe.addNewIssuer(liquidityIssuer, 'Liquidity').then(() => {
21-
const { issuerKeywordRecord } = zoe.getInstanceRecord();
22-
const amountMaths = zoe.getAmountMaths(issuerKeywordRecord);
21+
const amountMaths = zoe.getAmountMaths(
22+
harden(['TokenA', 'TokenB', 'Liquidity']),
23+
);
2324
Object.values(amountMaths).forEach(amountMath =>
2425
assert(
2526
amountMath.getMathHelpersName() === 'nat',
@@ -39,19 +40,19 @@ export const makeContract = harden(zoe => {
3940
} = makeConstProductBC(zoe);
4041

4142
return makeEmptyOffer().then(poolHandle => {
42-
const getPoolAmounts = () => zoe.getOffer(poolHandle).amounts;
43+
const getPoolAllocation = () => zoe.getCurrentAllocation(poolHandle);
4344

4445
const makeInvite = () => {
4546
const seat = harden({
4647
swap: () => {
4748
const { proposal } = zoe.getOffer(inviteHandle);
4849
const giveTokenA = harden({
4950
give: ['TokenA'],
50-
want: ['TokenB', 'Liquidity'],
51+
want: ['TokenB'],
5152
});
5253
const giveTokenB = harden({
5354
give: ['TokenB'],
54-
want: ['TokenA', 'Liquidity'],
55+
want: ['TokenA'],
5556
});
5657
let giveKeyword;
5758
let wantKeyword;
@@ -64,23 +65,23 @@ export const makeContract = harden(zoe => {
6465
} else {
6566
return rejectOffer(inviteHandle);
6667
}
67-
if (!amountMaths.Liquidity.isEmpty(proposal.want.Liquidity)) {
68+
if (proposal.want.Liquidity !== undefined) {
6869
rejectOffer(
6970
inviteHandle,
7071
`A Liquidity amount should not be present in a swap`,
7172
);
7273
}
7374

74-
const poolAmounts = getPoolAmounts();
75+
const poolAllocation = getPoolAllocation();
7576
const {
7677
outputExtent,
7778
newInputReserve,
7879
newOutputReserve,
7980
} = getPrice(
8081
harden({
8182
inputExtent: proposal.give[giveKeyword].extent,
82-
inputReserve: poolAmounts[giveKeyword].extent,
83-
outputReserve: poolAmounts[wantKeyword].extent,
83+
inputReserve: poolAllocation[giveKeyword].extent,
84+
outputReserve: poolAllocation[wantKeyword].extent,
8485
}),
8586
);
8687
const amountOut = amountMaths[wantKeyword].make(outputExtent);
@@ -97,7 +98,7 @@ export const makeContract = harden(zoe => {
9798
newUserAmounts[giveKeyword] = amountMaths[giveKeyword].getEmpty();
9899
newUserAmounts[wantKeyword] = amountOut;
99100

100-
const newPoolAmounts = { Liquidity: poolAmounts.Liquidity };
101+
const newPoolAmounts = { Liquidity: poolAllocation.Liquidity };
101102
newPoolAmounts[giveKeyword] = amountMaths[giveKeyword].make(
102103
newInputReserve,
103104
);
@@ -119,8 +120,8 @@ export const makeContract = harden(zoe => {
119120
});
120121
rejectIfNotProposal(inviteHandle, expected);
121122

122-
const userAmounts = zoe.getOffer(inviteHandle).amounts;
123-
const poolAmounts = getPoolAmounts();
123+
const userAllocation = zoe.getCurrentAllocation(inviteHandle);
124+
const poolAllocation = getPoolAllocation();
124125

125126
// Calculate how many liquidity tokens we should be minting.
126127
// Calculations are based on the extents represented by TokenA.
@@ -130,8 +131,8 @@ export const makeContract = harden(zoe => {
130131
const liquidityExtentOut = calcLiqExtentToMint(
131132
harden({
132133
liqTokenSupply,
133-
inputExtent: userAmounts.TokenA.extent,
134-
inputReserve: poolAmounts.TokenA.extent,
134+
inputExtent: userAllocation.TokenA.extent,
135+
inputReserve: poolAllocation.TokenA.extent,
135136
}),
136137
);
137138

@@ -160,9 +161,9 @@ export const makeContract = harden(zoe => {
160161
amountMaths[key].add(obj1[key], obj2[key]);
161162

162163
const newPoolAmounts = harden({
163-
TokenA: add('TokenA', userAmounts, poolAmounts),
164-
TokenB: add('TokenB', userAmounts, poolAmounts),
165-
Liquidity: poolAmounts.Liquidity,
164+
TokenA: add('TokenA', userAllocation, poolAllocation),
165+
TokenB: add('TokenB', userAllocation, poolAllocation),
166+
Liquidity: poolAllocation.Liquidity,
166167
});
167168

168169
const newUserAmounts = harden({
@@ -192,30 +193,30 @@ export const makeContract = harden(zoe => {
192193
});
193194
rejectIfNotProposal(inviteHandle, expected);
194195

195-
const userAmounts = zoe.getOffer(inviteHandle).amounts;
196-
const liquidityExtentIn = userAmounts.Liquidity.extent;
196+
const userAllocation = zoe.getCurrentAllocation(inviteHandle);
197+
const liquidityExtentIn = userAllocation.Liquidity.extent;
197198

198-
const poolAmounts = getPoolAmounts();
199+
const poolAllocation = getPoolAllocation();
199200

200201
const newUserAmounts = calcAmountsToRemove(
201202
harden({
202203
liqTokenSupply,
203-
poolAmounts,
204+
poolAllocation,
204205
liquidityExtentIn,
205206
}),
206207
);
207208

208209
const newPoolAmounts = harden({
209210
TokenA: amountMaths.TokenA.subtract(
210-
poolAmounts.TokenA,
211+
poolAllocation.TokenA,
211212
newUserAmounts.TokenA,
212213
),
213214
TokenB: amountMaths.TokenB.subtract(
214-
poolAmounts.TokenB,
215+
poolAllocation.TokenB,
215216
newUserAmounts.TokenB,
216217
),
217218
Liquidity: amountMaths.Liquidity.add(
218-
poolAmounts.Liquidity,
219+
poolAllocation.Liquidity,
219220
amountMaths.Liquidity.make(liquidityExtentIn),
220221
),
221222
});
@@ -259,10 +260,10 @@ export const makeContract = harden(zoe => {
259260
const inputExtent = amountMaths[inKeyword].getExtent(
260261
amountInObj[inKeyword],
261262
);
262-
const poolAmounts = getPoolAmounts();
263-
const inputReserve = poolAmounts[inKeyword].extent;
263+
const poolAllocation = getPoolAllocation();
264+
const inputReserve = poolAllocation[inKeyword].extent;
264265
const outKeyword = inKeyword === 'TokenA' ? 'TokenB' : 'TokenA';
265-
const outputReserve = poolAmounts[outKeyword].extent;
266+
const outputReserve = poolAllocation[outKeyword].extent;
266267
const { outputExtent } = getPrice(
267268
harden({
268269
inputExtent,
@@ -273,7 +274,7 @@ export const makeContract = harden(zoe => {
273274
return amountMaths[outKeyword].make(outputExtent);
274275
},
275276
getLiquidityIssuer: () => liquidityIssuer,
276-
getPoolAmounts,
277+
getPoolAllocation,
277278
makeInvite,
278279
},
279280
});

packages/zoe/src/contracts/helpers/auctions.js

+3-4
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,17 @@ export const closeAuction = (
2929
zoe,
3030
{ auctionLogicFn, sellerInviteHandle, allBidHandles },
3131
) => {
32-
const { issuerKeywordRecord } = zoe.getInstanceRecord();
3332
const { Bid: bidAmountMath, Asset: assetAmountMath } = zoe.getAmountMaths(
34-
issuerKeywordRecord,
33+
harden(['Bid', 'Asset']),
3534
);
3635

3736
// Filter out any inactive bids
3837
const { active: activeBidHandles } = zoe.getOfferStatuses(
3938
harden(allBidHandles),
4039
);
4140

42-
const getBids = offerRecord => offerRecord.amounts.Bid;
43-
const bids = zoe.getOffers(activeBidHandles).map(getBids);
41+
const getBids = amountsKeywordRecord => amountsKeywordRecord.Bid;
42+
const bids = zoe.getCurrentAllocations(activeBidHandles).map(getBids);
4443
const assetAmount = zoe.getOffer(sellerInviteHandle).proposal.give.Asset;
4544

4645
const {

0 commit comments

Comments
 (0)