Skip to content

Commit 26412c4

Browse files
authored
Merge pull request #11 from Agoric/dc-pay-contract
feat: contract to pay someone
2 parents c49ac22 + 5e0fa9a commit 26412c4

File tree

9 files changed

+648
-7
lines changed

9 files changed

+648
-7
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// @ts-check
2+
import { E, Far } from '@endo/far';
3+
import { M, mustMatch } from '@endo/patterns';
4+
import { withdrawFromSeat } from '@agoric/zoe/src/contractSupport/zoeHelpers.js';
5+
6+
const { keys, values } = Object;
7+
8+
/**
9+
* @typedef {object} PostalSvcTerms
10+
* @property {import('@agoric/vats').NameHub} namesByAddress
11+
*/
12+
13+
/** @param {ZCF<PostalSvcTerms>} zcf */
14+
export const start = zcf => {
15+
const { namesByAddress, issuers } = zcf.getTerms();
16+
mustMatch(namesByAddress, M.remotable('namesByAddress'));
17+
console.log('postal-service issuers', Object.keys(issuers));
18+
19+
/**
20+
* @param {string} addr
21+
* @returns {ERef<DepositFacet>}
22+
*/
23+
const getDepositFacet = addr => {
24+
assert.typeof(addr, 'string');
25+
return E(namesByAddress).lookup(addr, 'depositFacet');
26+
};
27+
28+
/**
29+
* @param {string} addr
30+
* @param {Payment} pmt
31+
*/
32+
const sendTo = (addr, pmt) => E(getDepositFacet(addr)).receive(pmt);
33+
34+
/** @param {string} recipient */
35+
const makeSendInvitation = recipient => {
36+
assert.typeof(recipient, 'string');
37+
38+
/** @type {OfferHandler} */
39+
const handleSend = async seat => {
40+
const { give } = seat.getProposal();
41+
const depositFacet = await getDepositFacet(recipient);
42+
const payouts = await withdrawFromSeat(zcf, seat, give);
43+
44+
// XXX partial failure? return payments?
45+
await Promise.all(
46+
values(payouts).map(pmtP =>
47+
E.when(pmtP, pmt => E(depositFacet).receive(pmt)),
48+
),
49+
);
50+
seat.exit();
51+
return `sent ${keys(payouts).join(', ')}`;
52+
};
53+
54+
return zcf.makeInvitation(handleSend, 'send');
55+
};
56+
57+
const publicFacet = Far('postalSvc', {
58+
lookup: (...path) => E(namesByAddress).lookup(...path),
59+
getDepositFacet,
60+
sendTo,
61+
makeSendInvitation,
62+
});
63+
return { publicFacet };
64+
};
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/**
2+
* @file core eval script* to start the postalService contract.
3+
*
4+
* The `permit` export specifies the corresponding permit.
5+
*/
6+
// @ts-check
7+
8+
import { E } from '@endo/far';
9+
import { fixHub } from './fixHub.js';
10+
11+
const { Fail } = assert;
12+
13+
/**
14+
* @typedef { typeof import('./postal-service.contract.js').start } PostalServiceFn
15+
*
16+
* @typedef {{
17+
* produce: { postalServiceKit: Producer<unknown> },
18+
* installation: {
19+
* consume: { postalService: Promise<Installation<PostalServiceFn>> },
20+
* produce: { postalService: Producer<Installation<PostalServiceFn>> },
21+
* }
22+
* instance: {
23+
* consume: { postalService: Promise<StartedInstanceKit<PostalServiceFn>['instance']> },
24+
* produce: { postalService: Producer<StartedInstanceKit<PostalServiceFn>['instance']> },
25+
* }
26+
* }} PostalServicePowers
27+
*/
28+
29+
/**
30+
* @param {BootstrapPowers} powers
31+
* @param {{ options?: { postalService: {
32+
* bundleID: string;
33+
* issuerNames?: string[];
34+
* }}}} [config]
35+
*/
36+
export const startPostalService = async (powers, config) => {
37+
/** @type { BootstrapPowers & PostalServicePowers} */
38+
// @ts-expect-error bootstrap powers evolve with BLD staker governance
39+
const postalPowers = powers;
40+
const {
41+
consume: { zoe, namesByAddressAdmin, agoricNames },
42+
installation: {
43+
produce: { postalService: produceInstallation },
44+
},
45+
instance: {
46+
produce: { postalService: produceInstance },
47+
},
48+
} = postalPowers;
49+
const {
50+
bundleID = Fail`no bundleID`,
51+
issuerNames = ['IST', 'Invitation', 'BLD', 'ATOM'],
52+
} = config?.options?.postalService ?? {};
53+
54+
/** @type {Installation<PostalServiceFn>} */
55+
const installation = await E(zoe).installBundleID(bundleID);
56+
produceInstallation.resolve(installation);
57+
58+
const namesByAddress = await fixHub(namesByAddressAdmin);
59+
60+
// XXX ATOM isn't available via consume.issuer.ATOM. Odd.
61+
const issuers = Object.fromEntries(
62+
issuerNames.map(n => [n, E(agoricNames).lookup('issuer', n)]),
63+
);
64+
const { instance } = await E(zoe).startInstance(installation, issuers, {
65+
namesByAddress,
66+
});
67+
produceInstance.resolve(instance);
68+
69+
console.log('postalService started');
70+
};
71+
72+
export const manifest = /** @type {const} */ ({
73+
[startPostalService.name]: {
74+
consume: {
75+
agoricNames: true,
76+
namesByAddress: true,
77+
namesByAddressAdmin: true,
78+
zoe: true,
79+
},
80+
installation: {
81+
produce: { postalService: true },
82+
},
83+
instance: {
84+
produce: { postalService: true },
85+
},
86+
},
87+
});
88+
89+
export const permit = Object.values(manifest)[0];
90+
91+
export const main = startPostalService;

contract/test/boot-tools.js

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,28 +108,28 @@ export const mockBootstrapPowers = async (
108108
};
109109

110110
/**
111-
* @param {import('ava').ExecutionContext} t
112111
* @param {BundleCache} bundleCache
113112
* @param {Record<string, string>} bundleRoots
114113
* @param {InstallBundle} installBundle
114+
* @param {(...args: unknown[]) => void} log
115115
*
116116
* @typedef {(id: string, bundle: CachedBundle, name: string) => Promise<void>} InstallBundle
117117
* @typedef {Awaited<ReturnType<import('@endo/bundle-source/cache.js').makeNodeBundleCache>>} BundleCache
118118
* @typedef {{ moduleFormat: 'endoZipBase64', endoZipBase64Sha512: string }} CachedBundle
119119
*/
120120
export const installBundles = async (
121-
t,
122121
bundleCache,
123122
bundleRoots,
124123
installBundle,
124+
log = console.log,
125125
) => {
126126
/** @type {Record<string, CachedBundle>} */
127127
const bundles = {};
128128
await null;
129129
for (const [name, rootModulePath] of Object.entries(bundleRoots)) {
130130
const bundle = await bundleCache.load(rootModulePath, name);
131131
const bundleID = getBundleId(bundle);
132-
t.log('publish bundle', name, bundleID.slice(0, 8));
132+
log('publish bundle', name, bundleID.slice(0, 8));
133133
await installBundle(bundleID, bundle, name);
134134
bundles[name] = bundle;
135135
}
@@ -143,10 +143,10 @@ export const bootAndInstallBundles = async (t, bundleRoots) => {
143143
const { vatAdminState } = powersKit;
144144

145145
const bundles = await installBundles(
146-
t,
147146
t.context.bundleCache,
148147
bundleRoots,
149148
(bundleID, bundle, _name) => vatAdminState.installBundle(bundleID, bundle),
149+
t.log,
150150
);
151151
return { ...powersKit, bundles };
152152
};
@@ -199,7 +199,12 @@ export const makeMockTools = async (t, bundleCache) => {
199199
);
200200

201201
let pid = 0;
202-
const runCoreEval = async ({ behavior, config }) => {
202+
const runCoreEval = async ({
203+
behavior,
204+
config,
205+
entryFile: _e,
206+
name: _todo,
207+
}) => {
203208
if (!behavior) throw Error('TODO: run core eval without live behavior');
204209
await behavior(powers, config);
205210
pid += 1;
@@ -226,8 +231,8 @@ export const makeMockTools = async (t, bundleCache) => {
226231

227232
return {
228233
makeQueryTool,
229-
installBundles: bundleRoots =>
230-
installBundles(t, bundleCache, bundleRoots, installBundle),
234+
installBundles: (bundleRoots, log) =>
235+
installBundles(bundleCache, bundleRoots, installBundle, log),
231236
runCoreEval,
232237
provisionSmartWallet: async (addr, balances) => {
233238
const it = await walletFactory.makeSmartWallet(addr);

contract/test/market-actors.js

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
// @ts-check
2+
import { E, getInterfaceOf } from '@endo/far';
3+
import { AmountMath, AssetKind } from '@agoric/ertp/src/amountMath.js';
4+
import { allValues, mapValues } from '../src/objectTools.js';
5+
import { seatLike } from './wallet-tools.js';
6+
import {
7+
makeNameProxy,
8+
makeAgoricNames,
9+
} from './ui-kit-goals/name-service-client.js';
10+
11+
const { entries, fromEntries, keys } = Object;
12+
13+
/**
14+
* @typedef {{
15+
* brand: Record<string, Promise<Brand>> & { timer: unknown }
16+
* issuer: Record<string, Promise<Issuer>>
17+
* instance: Record<string, Promise<Instance>>
18+
* installation: Record<string, Promise<Installation>>
19+
* }} WellKnown
20+
*/
21+
22+
/**
23+
* @typedef {{
24+
* assetKind: Map<Brand, AssetKind>
25+
* }} WellKnownKinds
26+
*/
27+
28+
/**
29+
* @param {import('ava').ExecutionContext} t
30+
* @param {{
31+
* wallet: import('./wallet-tools.js').MockWallet;
32+
* queryTool: Pick<import('./ui-kit-goals/queryKit.js').QueryTool, 'queryData'>;
33+
* }} mine
34+
* @param {{
35+
* rxAddr: string,
36+
* toSend: AmountKeywordRecord;
37+
* }} shared
38+
*/
39+
export const payerPete = async (
40+
t,
41+
{ wallet, queryTool },
42+
{ rxAddr, toSend },
43+
) => {
44+
const hub = await makeAgoricNames(queryTool);
45+
/** @type {WellKnown} */
46+
const agoricNames = makeNameProxy(hub);
47+
48+
const instance = await agoricNames.instance.postalService;
49+
50+
t.log('Pete offers to send to', rxAddr, 'via contract', instance);
51+
/** @type {import('@agoric/smart-wallet/src/offers.js').OfferSpec} */
52+
const sendOffer = {
53+
id: 'peteSend1',
54+
invitationSpec: {
55+
source: 'contract',
56+
instance,
57+
publicInvitationMaker: 'makeSendInvitation',
58+
invitationArgs: [rxAddr],
59+
},
60+
proposal: { give: toSend },
61+
};
62+
t.snapshot(sendOffer, 'client sends offer');
63+
const updates = await E(wallet.offers).executeOffer(sendOffer);
64+
65+
const seat = seatLike(updates);
66+
const payouts = await E(seat).getPayoutAmounts();
67+
for (const [kwd, amt] of entries(payouts)) {
68+
const { brand } = amt;
69+
const kind = AssetKind.NAT; // TODO: handle non-fungible amounts
70+
t.log('Pete payout should be empty', kwd, amt);
71+
t.deepEqual(amt, AmountMath.makeEmpty(brand, kind));
72+
}
73+
};
74+
75+
const trackDeposits = async (t, initial, purseUpdates, toSend) =>
76+
allValues(
77+
fromEntries(
78+
entries(initial).map(([name, _update]) => {
79+
const amtP = purseUpdates[name].next().then(u => {
80+
const expected = AmountMath.add(initial[name], toSend[name]);
81+
t.log('updated balance', name, u.value);
82+
t.deepEqual(u.value, expected);
83+
return u.value;
84+
});
85+
return [name, amtP];
86+
}),
87+
),
88+
);
89+
90+
/**
91+
* Rose expects to receive `shared.toSend` amounts.
92+
* She expects initial balances to be empty;
93+
* and relies on `wellKnown.assetKind` to make an empty amount from a brand.
94+
*
95+
* @param {import('ava').ExecutionContext} t
96+
* @param {{ wallet: import('./wallet-tools.js').MockWallet, }} mine
97+
* @param {{ toSend: AmountKeywordRecord }} shared
98+
*/
99+
export const receiverRose = async (t, { wallet }, { toSend }) => {
100+
console.time('rose');
101+
console.timeLog('rose', 'before notifiers');
102+
const purseNotifier = mapValues(toSend, amt =>
103+
wallet.peek.purseUpdates(amt.brand),
104+
);
105+
console.timeLog('rose', 'after notifiers; before initial');
106+
107+
const initial = await allValues(
108+
mapValues(purseNotifier, pn => pn.next().then(u => u.value)),
109+
);
110+
console.timeLog('rose', 'got initial', initial);
111+
t.log('Rose initial', initial);
112+
t.deepEqual(keys(initial), keys(toSend));
113+
114+
const done = await trackDeposits(t, initial, purseNotifier, toSend);
115+
t.log('Rose got balance updates', keys(done));
116+
t.deepEqual(keys(done), keys(toSend));
117+
};
118+
119+
/**
120+
* Rex expects to receive `shared.toSend` amounts.
121+
* Rex doesn't check his initial balances
122+
*
123+
* @param {import('ava').ExecutionContext} t
124+
* @param {{ wallet: import('./wallet-tools.js').MockWallet, }} mine
125+
* @param {{ toSend: AmountKeywordRecord }} shared
126+
*/
127+
export const receiverRex = async (t, { wallet }, { toSend }) => {
128+
const purseUpdates = await allValues(
129+
mapValues(toSend, amt => E(wallet.peek).purseUpdates(amt.brand)),
130+
);
131+
132+
const initial = await allValues(
133+
mapValues(purseUpdates, up =>
134+
E(up)
135+
.next()
136+
.then(u => u.value),
137+
),
138+
);
139+
140+
const done = await trackDeposits(t, initial, purseUpdates, toSend);
141+
142+
t.log('Rex got balance updates', keys(done));
143+
t.deepEqual(keys(done), keys(toSend));
144+
};
145+
146+
export const senderContract = async (
147+
t,
148+
{ zoe, terms: { postalService: instance, destAddr: addr1 } },
149+
) => {
150+
const iIssuer = await E(zoe).getInvitationIssuer();
151+
const iBrand = await E(iIssuer).getBrand();
152+
const postalService = E(zoe).getPublicFacet(instance);
153+
const purse = await E(iIssuer).makeEmptyPurse();
154+
155+
const noInvitations = AmountMath.make(iBrand, harden([]));
156+
const pmt1 = await E(purse).withdraw(noInvitations);
157+
158+
t.log(
159+
'senderContract: E(',
160+
getInterfaceOf(await postalService),
161+
').sendTo(',
162+
addr1,
163+
',',
164+
noInvitations,
165+
')',
166+
);
167+
const sent = await E(postalService).sendTo(addr1, pmt1);
168+
t.deepEqual(sent, noInvitations);
169+
};

0 commit comments

Comments
 (0)