@agoric/wallet-backend 0.13.5 → 0.13.6-dev-800c2bb.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,387 +0,0 @@
1
- // @ts-check
2
- import { makeScalarMap } from '@agoric/store';
3
- import { Far, makeMarshal, Remotable } from '@endo/marshal';
4
- import { HandledPromise } from '@endo/eventual-send'; // TODO: convince tsc this isn't needed
5
-
6
- const { details: X, quote: q } = assert;
7
-
8
- /**
9
- * For a value with a known id in the board, we can use
10
- * that board id as a slot to preserve identity when marshaling.
11
- *
12
- * @typedef {`board${Digits}`} BoardId
13
- */
14
-
15
- /**
16
- * @param {unknown} specimen
17
- * @returns {specimen is BoardId}
18
- */
19
- const isBoardId = specimen => {
20
- return typeof specimen === 'string' && !!specimen.match(/^board[^:]/);
21
- };
22
-
23
- /**
24
- * When marshaling a purse, payment, etc. we partition the slots
25
- * using prefixes.
26
- *
27
- * @template {Record<string, IdTable<*,*>>} T
28
- * @typedef {`${string & keyof T}:${Digits}`} WalletSlot<T>
29
- */
30
- /**
31
- * @template {string} K
32
- * @typedef {`${K}:${Digits}`} KindSlot<K>
33
- */
34
-
35
- /**
36
- * @template {Record<string, IdTable<*,*>>} T
37
- * @param {T} _tables
38
- * @param {string & keyof T} kind
39
- * @param {number} id
40
- * @returns {WalletSlot<T>}
41
- */
42
- const makeWalletSlot = (_tables, kind, id) => {
43
- const digits = /** @type {Digits} */ (`${id}`);
44
- return `${kind}:${digits}`;
45
- };
46
-
47
- /**
48
- * @template {Record<string, IdTable<*,*>>} T
49
- * @param {T} record
50
- * @param {(value: string, index: number, obj: string[]) => boolean} predicate
51
- * @returns {string & keyof T | undefined}
52
- */
53
- const findKey = (record, predicate) => {
54
- const key = Object.keys(record).find(predicate);
55
- return key;
56
- };
57
-
58
- /**
59
- * @template {Record<string, IdTable<*,*>>} T
60
- * @param {T} tables
61
- * @param {string} slot
62
- * @returns {{ kind: undefined | string & keyof T, id: number }}
63
- */
64
- const parseWalletSlot = (tables, slot) => {
65
- const kind = findKey(tables, k => slot.startsWith(`${k}:`));
66
- const id = kind ? Number(slot.slice(kind.length + 1)) : NaN;
67
- return { kind, id };
68
- };
69
-
70
- /**
71
- * Since KindSlots always include a colon and BoardIds never do,
72
- * we an mix them without confusion.
73
- *
74
- * @template {Record<string, IdTable<*,*>>} T
75
- * @typedef {WalletSlot<T> | BoardId} MixedSlot<T>
76
- */
77
- /**
78
- * @typedef {`1` | `12` | `123`} Digits - 1 or more digits.
79
- * NOTE: the typescript definition here is more restrictive than
80
- * actual usage.
81
- */
82
-
83
- /**
84
- * @template Slot
85
- * @template Val
86
- *
87
- * @typedef {{
88
- * bySlot: MapStore<Slot, Val>,
89
- * byVal: MapStore<Val, Slot>,
90
- * }} IdTable<Value>
91
- */
92
-
93
- /**
94
- * @template Slot
95
- * @template Val
96
- * @param {IdTable<Slot, Val>} table
97
- * @param {Slot} slot
98
- * @param {Val} val
99
- */
100
- const initSlotVal = (table, slot, val) => {
101
- table.bySlot.init(slot, val);
102
- table.byVal.init(val, slot);
103
- };
104
-
105
- /**
106
- * Make context for exporting wallet data where brands etc. can be recognized by boardId.
107
- * Export for use outside the smart wallet.
108
- *
109
- * When serializing wallet state for, there's a tension between
110
- *
111
- * - keeping purses etc. closely held
112
- * - recognizing identity of brands also referenced in the state of contracts such as the AMM
113
- *
114
- * `makeMarshal()` is parameterized by the type of slots. Here we use a disjoint union of
115
- * - board ids for widely shared objects
116
- * - kind:seq ids for closely held objects; for example purse:123
117
- */
118
- export const makeExportContext = () => {
119
- const walletObjects = {
120
- /** @type {IdTable<number, Purse>} */
121
- purse: {
122
- bySlot: makeScalarMap(),
123
- byVal: makeScalarMap(),
124
- },
125
- /** @type {IdTable<number, Payment>} */
126
- payment: {
127
- bySlot: makeScalarMap(),
128
- byVal: makeScalarMap(),
129
- },
130
- // TODO: offer, contact, dapp
131
- /** @type {IdTable<number, unknown>} */
132
- unknown: {
133
- bySlot: makeScalarMap(),
134
- byVal: makeScalarMap(),
135
- },
136
- };
137
- /** @type {IdTable<BoardId, unknown>} */
138
- const boardObjects = {
139
- bySlot: makeScalarMap(),
140
- byVal: makeScalarMap(),
141
- };
142
-
143
- /**
144
- * Look up the slot in mappings from published data
145
- * else try walletObjects that we have seen.
146
- *
147
- * @throws if not found (a slotToVal function typically
148
- * conjures a new identity)
149
- *
150
- * @param {MixedSlot<typeof walletObjects>} slot
151
- * @param {string} _iface
152
- */
153
- const slotToVal = (slot, _iface) => {
154
- if (isBoardId(slot) && boardObjects.bySlot.has(slot)) {
155
- return boardObjects.bySlot.get(slot);
156
- }
157
- const { kind, id } = parseWalletSlot(walletObjects, slot);
158
- assert(kind, X`bad slot kind: ${slot}`);
159
- const val = walletObjects[kind].bySlot.get(id); // or throw
160
- return val;
161
- };
162
-
163
- let unknownNonce = 0;
164
-
165
- /**
166
- * @param {unknown} val
167
- * @returns {MixedSlot<typeof walletObjects>}
168
- */
169
- const valToSlot = val => {
170
- if (boardObjects.byVal.has(val)) {
171
- return boardObjects.byVal.get(val);
172
- }
173
- const kind = findKey(walletObjects, k => walletObjects[k].byVal.has(val));
174
- if (kind) {
175
- // @ts-expect-error has(val) above ensures val has the right type
176
- const id = walletObjects[kind].byVal.get(val);
177
- return makeWalletSlot(walletObjects, kind, id);
178
- }
179
- unknownNonce += 1;
180
- const slot = makeWalletSlot(walletObjects, 'unknown', unknownNonce);
181
- initSlotVal(walletObjects.unknown, unknownNonce, val);
182
- return slot;
183
- };
184
-
185
- /**
186
- * @template V
187
- * @param {string & keyof typeof walletObjects} kind
188
- * @param {IdTable<number, V>} table
189
- */
190
- const makeSaver = (kind, table) => {
191
- let nonce = 0;
192
- /** @param {V} val */
193
- const saver = val => {
194
- nonce += 1;
195
- initSlotVal(table, nonce, val);
196
- };
197
- return saver;
198
- };
199
-
200
- return harden({
201
- savePurseActions: makeSaver('purse', walletObjects.purse),
202
- savePaymentActions: makeSaver('payment', walletObjects.payment),
203
- /**
204
- * @param {number} id
205
- * @param {Purse} purse
206
- */
207
- initPurseId: (id, purse) => {
208
- initSlotVal(walletObjects.purse, id, purse);
209
- },
210
- purseEntries: walletObjects.purse.bySlot.entries,
211
- /**
212
- * @param {BoardId} id
213
- * @param {unknown} val
214
- */
215
- initBoardId: (id, val) => {
216
- initSlotVal(boardObjects, id, val);
217
- },
218
- /**
219
- * @param {BoardId} id
220
- * @param {unknown} val
221
- */
222
- ensureBoardId: (id, val) => {
223
- if (boardObjects.byVal.has(val)) {
224
- assert.equal(boardObjects.byVal.get(val), id);
225
- return;
226
- }
227
- initSlotVal(boardObjects, id, val);
228
- },
229
- ...makeMarshal(valToSlot, slotToVal),
230
- });
231
- };
232
-
233
- const defaultMakePresence = iface => {
234
- const severed = `SEVERED: ${iface.replace(/^Alleged: /, '')}`;
235
- return Far(severed, {});
236
- };
237
-
238
- /**
239
- * Make context for marshalling wallet or board data.
240
- * To be imported into the client, which never exports objects.
241
- *
242
- * @param {(iface: string) => unknown} [makePresence]
243
- */
244
- export const makeImportContext = (makePresence = defaultMakePresence) => {
245
- const walletObjects = {
246
- /** @type {IdTable<number, unknown>} */
247
- purse: {
248
- bySlot: makeScalarMap(),
249
- byVal: makeScalarMap(),
250
- },
251
- /** @type {IdTable<number, unknown>} */
252
- payment: {
253
- bySlot: makeScalarMap(),
254
- byVal: makeScalarMap(),
255
- },
256
- /** @type {IdTable<number, unknown>} */
257
- unknown: {
258
- bySlot: makeScalarMap(),
259
- byVal: makeScalarMap(),
260
- },
261
- };
262
- /** @type {IdTable<BoardId, unknown>} */
263
- const boardObjects = {
264
- bySlot: makeScalarMap(),
265
- byVal: makeScalarMap(),
266
- };
267
-
268
- /**
269
- * @template Slot
270
- * @template Val
271
- * @param {IdTable<Slot, Val>} table
272
- * @param {Slot} slot
273
- * @param {string} iface
274
- */
275
- const provideVal = (table, slot, iface) => {
276
- if (table.bySlot.has(slot)) {
277
- return table.bySlot.get(slot);
278
- }
279
- const val = makePresence(iface);
280
- initSlotVal(table, slot, val);
281
- return val;
282
- };
283
-
284
- const slotToVal = {
285
- /**
286
- * @param {string} slot
287
- * @param {string} iface
288
- */
289
- fromBoard: (slot, iface) => {
290
- assert(isBoardId(slot), X`bad board slot ${q(slot)}`);
291
- return provideVal(boardObjects, slot, iface);
292
- },
293
-
294
- /**
295
- * @param {string} slot
296
- * @param {string} iface
297
- */
298
- fromMyWallet: (slot, iface) => {
299
- if (!slot) {
300
- // Empty or null slots are neither in the wallet nor the board.
301
- return makePresence(`${slot}`);
302
- }
303
- const { kind, id } = parseWalletSlot(walletObjects, slot);
304
- return kind
305
- ? provideVal(walletObjects[kind], id, iface)
306
- : slotToVal.fromBoard(slot, iface);
307
- },
308
- };
309
-
310
- const valToSlot = {
311
- fromBoard: val => boardObjects.byVal.get(val),
312
- /** @param {unknown} val */
313
- fromMyWallet: val => {
314
- const kind = findKey(walletObjects, k => walletObjects[k].byVal.has(val));
315
- assert(kind, X`cannot serialize unregistered ${val}`);
316
-
317
- const id = walletObjects[kind].byVal.get(val);
318
- return makeWalletSlot(walletObjects, kind, id);
319
- },
320
- };
321
-
322
- const marshal = {
323
- fromBoard: makeMarshal(valToSlot.fromBoard, slotToVal.fromBoard, {
324
- marshalName: 'fromBoard',
325
- }),
326
- fromMyWallet: makeMarshal(valToSlot.fromMyWallet, slotToVal.fromMyWallet, {
327
- marshalName: 'fromMyWallet',
328
- }),
329
- };
330
-
331
- return harden({
332
- /**
333
- * @param {BoardId} id
334
- * @param {unknown} val
335
- */
336
- initBoardId: (id, val) => {
337
- initSlotVal(boardObjects, id, val);
338
- },
339
- /**
340
- * @param {BoardId} id
341
- * @param {unknown} val
342
- */
343
- ensureBoardId: (id, val) => {
344
- if (boardObjects.byVal.has(val)) {
345
- assert.equal(boardObjects.byVal.get(val), id);
346
- return;
347
- }
348
- initSlotVal(boardObjects, id, val);
349
- },
350
- fromMyWallet: Far('wallet marshaller', { ...marshal.fromMyWallet }),
351
- fromBoard: Far('board marshaller', { ...marshal.fromBoard }),
352
- });
353
- };
354
-
355
- /**
356
- * @param {string} iface
357
- * @param {{
358
- * applyMethod: (target: unknown, method: string | symbol, args: unknown[]) => void,
359
- * applyFunction: (target: unknown, args: unknown[]) => void,
360
- * }} handler
361
- */
362
- const makePresence = (iface, handler) => {
363
- let obj;
364
- // eslint-disable-next-line no-new
365
- new HandledPromise((resolve, reject, resolveWithPresence) => {
366
- obj = resolveWithPresence(handler);
367
- });
368
- assert(obj);
369
- return Remotable(iface, undefined, obj);
370
- };
371
-
372
- /**
373
- * @param {string} iface
374
- * @param {(parts: unknown[]) => void} log
375
- */
376
- export const makeLoggingPresence = (iface, log) => {
377
- /** @type {any} */ // TODO: solve types puzzle
378
- const it = makePresence(iface, {
379
- applyMethod: (target, method, args) => {
380
- log(harden(['applyMethod', target, method, args]));
381
- },
382
- applyFunction: (target, args) => {
383
- log(harden(['applyFunction', target, args]));
384
- },
385
- });
386
- return it;
387
- };
@@ -1,65 +0,0 @@
1
- // @ts-check
2
- import '@agoric/zoe/exported.js';
3
- import { AmountMath } from '@agoric/ertp';
4
- import { E } from '@endo/eventual-send';
5
- import { Far } from '@endo/marshal';
6
-
7
- /** @param {ZCF} zcf */
8
- export const start = async zcf => {
9
- const mint = await zcf.makeZCFMint('Attestation');
10
- const { brand } = await mint.getIssuerRecord();
11
- const callHistory = {
12
- /** @type Array<string> */
13
- wantProposals: [],
14
- /** @type Array<string> */
15
- wantAllocations: [],
16
- /** @type Array<string> */
17
- returnProposals: [],
18
- /** @type Array<string> */
19
- returnAllocations: [],
20
- };
21
-
22
- const mintAttestation = ({ value }) => {
23
- const { userSeat, zcfSeat } = zcf.makeEmptySeatKit();
24
- mint.mintGains({ Attestation: AmountMath.make(brand, value) }, zcfSeat);
25
- zcfSeat.exit();
26
- return E(userSeat).getPayout('Attestation');
27
- };
28
-
29
- const handleWantAtt = seat => {
30
- const proposal = seat.getProposal();
31
- callHistory.wantProposals.push(proposal);
32
- callHistory.wantAllocations.push(seat.getCurrentAllocation());
33
- mint.mintGains(
34
- { Attestation: AmountMath.make(brand, proposal.want.Attestation.value) },
35
- seat,
36
- );
37
- seat.exit();
38
- return 'good job';
39
- };
40
-
41
- const handleReturnAtt = seat => {
42
- const proposal = seat.getProposal();
43
- callHistory.returnProposals.push(proposal);
44
- callHistory.returnAllocations.push(seat.getCurrentAllocation());
45
- seat.exit();
46
- return 'thank you for that';
47
- };
48
-
49
- const makeWantAttInvitation = () =>
50
- zcf.makeInvitation(handleWantAtt, 'WantAtt');
51
-
52
- const makeReturnAttInvitation = () =>
53
- zcf.makeInvitation(handleReturnAtt, 'ReturnAtt');
54
-
55
- const getCallHistory = () => callHistory;
56
-
57
- const publicFacet = Far('attestation public facet', {
58
- mintAttestation,
59
- makeWantAttInvitation,
60
- makeReturnAttInvitation,
61
- getCallHistory,
62
- });
63
-
64
- return harden({ publicFacet });
65
- };
@@ -1,184 +0,0 @@
1
- // @ts-check
2
- import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js';
3
- // eslint-disable-next-line import/no-extraneous-dependencies
4
- import { Far } from '@endo/far';
5
- import { makeBoard } from '@agoric/vats/src/lib-board.js';
6
- import { makeHandle } from '@agoric/zoe/src/makeHandle.js';
7
- import {
8
- makeExportContext,
9
- makeImportContext,
10
- } from '../src/marshal-contexts.js';
11
-
12
- /** @param {ReturnType<typeof makeBoard>} board */
13
- const makeAMM = board => {
14
- const atom = Far('ATOM brand', {});
15
- const pub = board.getPublishingMarshaller();
16
- return harden({ getMetrics: () => pub.serialize(harden([atom])) });
17
- };
18
-
19
- /** @param {ReturnType<typeof makeBoard>} board */
20
- const makeOnChainWallet = board => {
21
- const context = makeExportContext();
22
- let brand;
23
- return harden({
24
- suggestIssuer: (name, boardId) => {
25
- brand = board.getValue(boardId);
26
- const purse = Far(`${name} purse`, {
27
- getCurrentAmount: () => harden({ brand, value: 100 }),
28
- });
29
- // only for private brands
30
- // context.initBrandId(boardId, brand);
31
- context.initBoardId(boardId, brand);
32
- // @ts-expect-error mock purse
33
- context.initPurseId(name, purse); // TODO: strong id rather than name?
34
- },
35
- publish: () =>
36
- context.serialize(
37
- harden(
38
- [...context.purseEntries()].map(([name, purse], id) => ({
39
- meta: { id },
40
- // displayInfo
41
- // brandBoardId,
42
- purse,
43
- brand,
44
- currentAmount: purse.getCurrentAmount(),
45
- // actions
46
- brandPetname: name,
47
- pursePetname: `${name} purse`,
48
- })),
49
- ),
50
- ),
51
- });
52
- };
53
-
54
- test('makeImportContext preserves identity across AMM and wallet', t => {
55
- const context = makeImportContext();
56
-
57
- const board = makeBoard(0, { prefix: 'board' });
58
- const amm = makeAMM(board);
59
- const ammMetricsCapData = amm.getMetrics();
60
-
61
- t.deepEqual(ammMetricsCapData, {
62
- body: JSON.stringify([
63
- { '@qclass': 'slot', iface: 'Alleged: ATOM brand', index: 0 },
64
- ]),
65
- slots: ['board011'],
66
- });
67
-
68
- /** @type {Brand[]} */
69
- const [b1] = context.fromBoard.unserialize(ammMetricsCapData);
70
- /** @type {Brand[]} */
71
- const [b2] = context.fromBoard.unserialize(amm.getMetrics());
72
- t.is(b1, b2, 'unserialization twice from same source');
73
-
74
- const myWallet = makeOnChainWallet(board);
75
- myWallet.suggestIssuer('ATOM', 'board011');
76
- const walletCapData = myWallet.publish();
77
- t.deepEqual(walletCapData, {
78
- body: JSON.stringify([
79
- {
80
- brand: { '@qclass': 'slot', iface: 'Alleged: ATOM brand', index: 0 },
81
- brandPetname: 'ATOM',
82
- currentAmount: { brand: { '@qclass': 'slot', index: 0 }, value: 100 },
83
- meta: { id: 0 },
84
- purse: { '@qclass': 'slot', iface: 'Alleged: ATOM purse', index: 1 },
85
- pursePetname: 'ATOM purse',
86
- },
87
- ]),
88
- slots: ['board011', 'purse:ATOM'],
89
- });
90
-
91
- const walletState = context.fromMyWallet.unserialize(walletCapData);
92
- t.is(walletState[0].brand, b1, 'unserialization across sources');
93
-
94
- t.throws(
95
- () => context.fromBoard.unserialize(walletCapData),
96
- { message: /bad board slot/ },
97
- 'AMM cannot refer to purses',
98
- );
99
-
100
- t.throws(
101
- () => context.fromMyWallet.serialize(Far('widget', {})),
102
- {
103
- message: /unregistered/,
104
- },
105
- 'import context never exports unregistered objects',
106
- );
107
- });
108
-
109
- test('ensureBoardId allows re-registration; initBoardId does not', t => {
110
- const brandM = Far('Moola brand', {});
111
- const brandS = Far('Semolean brand', {});
112
-
113
- const context = makeExportContext();
114
- context.initBoardId('board1', brandM);
115
- t.throws(() => context.initBoardId('board1', brandM));
116
- context.ensureBoardId('board1', brandM);
117
- t.throws(() => context.ensureBoardId('board12', brandM));
118
- context.ensureBoardId('board12', brandS);
119
- t.throws(() => context.initBoardId('board12', brandM));
120
- });
121
-
122
- test('makeExportContext.serialize handles unregistered identites', t => {
123
- const brand = Far('Zoe invitation brand', {});
124
- const instance = Far('amm instance', {});
125
- const invitationAmount = harden({ brand, value: [{ instance }] });
126
-
127
- const context = makeExportContext();
128
- context.initBoardId('board1', brand);
129
- const actual = context.serialize(invitationAmount);
130
-
131
- t.deepEqual(actual, {
132
- body: JSON.stringify({
133
- brand: {
134
- '@qclass': 'slot',
135
- iface: 'Alleged: Zoe invitation brand',
136
- index: 0,
137
- },
138
- value: [
139
- {
140
- instance: {
141
- '@qclass': 'slot',
142
- iface: 'Alleged: amm instance',
143
- index: 1,
144
- },
145
- },
146
- ],
147
- }),
148
- slots: ['board1', 'unknown:1'],
149
- });
150
-
151
- t.deepEqual(context.unserialize(actual), invitationAmount);
152
-
153
- const myPayment = /** @type {Payment} */ (
154
- Far('payment', { getAllegedBrand: () => assert.fail('no impl') })
155
- );
156
- context.savePaymentActions(myPayment);
157
- const cap2 = context.serialize(myPayment);
158
- t.deepEqual(cap2, {
159
- body: JSON.stringify({
160
- '@qclass': 'slot',
161
- iface: 'Alleged: payment',
162
- index: 0,
163
- }),
164
- slots: ['payment:1'],
165
- });
166
- t.deepEqual(context.unserialize(cap2), myPayment);
167
- });
168
-
169
- test('fromBoard.serialize requires board ids', t => {
170
- const context = makeImportContext();
171
-
172
- const unpassable = harden({
173
- instance: makeHandle('Instance'),
174
- });
175
- t.throws(() => context.fromBoard.serialize(unpassable), {
176
- message: '"key" not found: "[Alleged: InstanceHandle]"',
177
- });
178
-
179
- context.ensureBoardId('board123', unpassable.instance);
180
- t.deepEqual(context.fromBoard.serialize(unpassable), {
181
- body: '{"instance":{"@qclass":"slot","iface":"Alleged: InstanceHandle","index":0}}',
182
- slots: ['board123'],
183
- });
184
- });