Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: contract to pay someone #11

Merged
merged 20 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions contract/src/postal-service.contract.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// @ts-check
import { E, Far } from '@endo/far';
import { M, mustMatch } from '@endo/patterns';
import { withdrawFromSeat } from '@agoric/zoe/src/contractSupport/zoeHelpers.js';

const { keys, values } = Object;

/**
* @typedef {object} PostalSvcTerms
* @property {import('@agoric/vats').NameHub} namesByAddress
*/

/** @param {ZCF<PostalSvcTerms>} zcf */
export const start = zcf => {
const { namesByAddress, issuers } = zcf.getTerms();
mustMatch(namesByAddress, M.remotable('namesByAddress'));
console.log('postal-service issuers', Object.keys(issuers));

/**
* @param {string} addr
* @returns {ERef<DepositFacet>}
*/
const getDepositFacet = addr => {
assert.typeof(addr, 'string');
return E(namesByAddress).lookup(addr, 'depositFacet');
};

/**
* @param {string} addr
* @param {Payment} pmt
*/
const sendTo = (addr, pmt) => E(getDepositFacet(addr)).receive(pmt);

/** @param {string} recipient */
const makeSendInvitation = recipient => {
assert.typeof(recipient, 'string');

/** @type {OfferHandler} */
const handleSend = async seat => {
const { give } = seat.getProposal();
const depositFacet = await getDepositFacet(recipient);
const payouts = await withdrawFromSeat(zcf, seat, give);

// XXX partial failure? return payments?
await Promise.all(
values(payouts).map(pmtP =>
E.when(pmtP, pmt => E(depositFacet).receive(pmt)),
),
);
seat.exit();
return `sent ${keys(payouts).join(', ')}`;
};

return zcf.makeInvitation(handleSend, 'send');
};

const publicFacet = Far('postalSvc', {
lookup: (...path) => E(namesByAddress).lookup(...path),
getDepositFacet,
sendTo,
makeSendInvitation,
});
return { publicFacet };
};
91 changes: 91 additions & 0 deletions contract/src/postal-service.proposal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* @file core eval script* to start the postalService contract.
*
* The `permit` export specifies the corresponding permit.
*/
// @ts-check

import { E } from '@endo/far';
import { fixHub } from './fixHub.js';

const { Fail } = assert;

/**
* @typedef { typeof import('./postal-service.contract.js').start } PostalServiceFn
*
* @typedef {{
* produce: { postalServiceKit: Producer<unknown> },
* installation: {
* consume: { postalService: Promise<Installation<PostalServiceFn>> },
* produce: { postalService: Producer<Installation<PostalServiceFn>> },
* }
* instance: {
* consume: { postalService: Promise<StartedInstanceKit<PostalServiceFn>['instance']> },
* produce: { postalService: Producer<StartedInstanceKit<PostalServiceFn>['instance']> },
* }
* }} PostalServicePowers
*/

/**
* @param {BootstrapPowers} powers
* @param {{ options?: { postalService: {
* bundleID: string;
* issuerNames?: string[];
* }}}} [config]
*/
export const startPostalService = async (powers, config) => {
/** @type { BootstrapPowers & PostalServicePowers} */
// @ts-expect-error bootstrap powers evolve with BLD staker governance
const postalPowers = powers;
const {
consume: { zoe, namesByAddressAdmin, agoricNames },
installation: {
produce: { postalService: produceInstallation },
},
instance: {
produce: { postalService: produceInstance },
},
} = postalPowers;
const {
bundleID = Fail`no bundleID`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? What does this do when passed to installBundleID()?

Why does bundllng want separate lines?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The design in #16 is that there are 2 cases:

  1. in an ava tests, the caller supplies the bundleID in the config arg.
  2. for deployment, this line gets replaced by bundleID = ${JSON.stringify(b1-${bundle.endoZipBase64Sha512})},. See configureBundleID

For this PR, I punted the comment.

issuerNames = ['IST', 'Invitation', 'BLD', 'ATOM'],
} = config?.options?.postalService ?? {};

/** @type {Installation<PostalServiceFn>} */
const installation = await E(zoe).installBundleID(bundleID);
produceInstallation.resolve(installation);

const namesByAddress = await fixHub(namesByAddressAdmin);

// XXX ATOM isn't available via consume.issuer.ATOM. Odd.
const issuers = Object.fromEntries(
issuerNames.map(n => [n, E(agoricNames).lookup('issuer', n)]),
);
const { instance } = await E(zoe).startInstance(installation, issuers, {
namesByAddress,
});
produceInstance.resolve(instance);

console.log('postalService started');
};

export const manifest = /** @type {const} */ ({
[startPostalService.name]: {
consume: {
agoricNames: true,
namesByAddress: true,
namesByAddressAdmin: true,
zoe: true,
},
installation: {
produce: { postalService: true },
},
instance: {
produce: { postalService: true },
},
},
});

export const permit = Object.values(manifest)[0];

export const main = startPostalService;
19 changes: 12 additions & 7 deletions contract/test/boot-tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,28 +108,28 @@ export const mockBootstrapPowers = async (
};

/**
* @param {import('ava').ExecutionContext} t
* @param {BundleCache} bundleCache
* @param {Record<string, string>} bundleRoots
* @param {InstallBundle} installBundle
* @param {(...args: unknown[]) => void} log
*
* @typedef {(id: string, bundle: CachedBundle, name: string) => Promise<void>} InstallBundle
* @typedef {Awaited<ReturnType<import('@endo/bundle-source/cache.js').makeNodeBundleCache>>} BundleCache
* @typedef {{ moduleFormat: 'endoZipBase64', endoZipBase64Sha512: string }} CachedBundle
*/
export const installBundles = async (
t,
bundleCache,
bundleRoots,
installBundle,
log = console.log,
) => {
/** @type {Record<string, CachedBundle>} */
const bundles = {};
await null;
for (const [name, rootModulePath] of Object.entries(bundleRoots)) {
const bundle = await bundleCache.load(rootModulePath, name);
const bundleID = getBundleId(bundle);
t.log('publish bundle', name, bundleID.slice(0, 8));
log('publish bundle', name, bundleID.slice(0, 8));
await installBundle(bundleID, bundle, name);
bundles[name] = bundle;
}
Expand All @@ -143,10 +143,10 @@ export const bootAndInstallBundles = async (t, bundleRoots) => {
const { vatAdminState } = powersKit;

const bundles = await installBundles(
t,
t.context.bundleCache,
bundleRoots,
(bundleID, bundle, _name) => vatAdminState.installBundle(bundleID, bundle),
t.log,
);
return { ...powersKit, bundles };
};
Expand Down Expand Up @@ -199,7 +199,12 @@ export const makeMockTools = async (t, bundleCache) => {
);

let pid = 0;
const runCoreEval = async ({ behavior, config }) => {
const runCoreEval = async ({
behavior,
config,
entryFile: _e,
name: _todo,
}) => {
if (!behavior) throw Error('TODO: run core eval without live behavior');
await behavior(powers, config);
pid += 1;
Expand All @@ -226,8 +231,8 @@ export const makeMockTools = async (t, bundleCache) => {

return {
makeQueryTool,
installBundles: bundleRoots =>
installBundles(t, bundleCache, bundleRoots, installBundle),
installBundles: (bundleRoots, log) =>
installBundles(bundleCache, bundleRoots, installBundle, log),
runCoreEval,
provisionSmartWallet: async (addr, balances) => {
const it = await walletFactory.makeSmartWallet(addr);
Expand Down
169 changes: 169 additions & 0 deletions contract/test/market-actors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// @ts-check
import { E, getInterfaceOf } from '@endo/far';
import { AmountMath, AssetKind } from '@agoric/ertp/src/amountMath.js';
import { allValues, mapValues } from '../src/objectTools.js';
import { seatLike } from './wallet-tools.js';
import {
makeNameProxy,
makeAgoricNames,
} from './ui-kit-goals/name-service-client.js';

const { entries, fromEntries, keys } = Object;

/**
* @typedef {{
* brand: Record<string, Promise<Brand>> & { timer: unknown }
* issuer: Record<string, Promise<Issuer>>
* instance: Record<string, Promise<Instance>>
* installation: Record<string, Promise<Installation>>
* }} WellKnown
*/

/**
* @typedef {{
* assetKind: Map<Brand, AssetKind>
* }} WellKnownKinds
*/

/**
* @param {import('ava').ExecutionContext} t
* @param {{
* wallet: import('./wallet-tools.js').MockWallet;
* queryTool: Pick<import('./ui-kit-goals/queryKit.js').QueryTool, 'queryData'>;
* }} mine
* @param {{
* rxAddr: string,
* toSend: AmountKeywordRecord;
* }} shared
*/
export const payerPete = async (
t,
{ wallet, queryTool },
{ rxAddr, toSend },
) => {
const hub = await makeAgoricNames(queryTool);
/** @type {WellKnown} */
const agoricNames = makeNameProxy(hub);

const instance = await agoricNames.instance.postalService;

t.log('Pete offers to send to', rxAddr, 'via contract', instance);
Chris-Hibbert marked this conversation as resolved.
Show resolved Hide resolved
/** @type {import('@agoric/smart-wallet/src/offers.js').OfferSpec} */
const sendOffer = {
id: 'peteSend1',
invitationSpec: {
source: 'contract',
instance,
publicInvitationMaker: 'makeSendInvitation',
invitationArgs: [rxAddr],
},
proposal: { give: toSend },
};
t.snapshot(sendOffer, 'client sends offer');
const updates = await E(wallet.offers).executeOffer(sendOffer);

const seat = seatLike(updates);
const payouts = await E(seat).getPayoutAmounts();
for (const [kwd, amt] of entries(payouts)) {
const { brand } = amt;
const kind = AssetKind.NAT; // TODO: handle non-fungible amounts
t.log('Pete payout should be empty', kwd, amt);
t.deepEqual(amt, AmountMath.makeEmpty(brand, kind));
}
};

const trackDeposits = async (t, initial, purseUpdates, toSend) =>
allValues(
fromEntries(
entries(initial).map(([name, _update]) => {
const amtP = purseUpdates[name].next().then(u => {
const expected = AmountMath.add(initial[name], toSend[name]);
t.log('updated balance', name, u.value);
t.deepEqual(u.value, expected);
return u.value;
});
return [name, amtP];
}),
),
);

/**
* Rose expects to receive `shared.toSend` amounts.
* She expects initial balances to be empty;
* and relies on `wellKnown.assetKind` to make an empty amount from a brand.
*
* @param {import('ava').ExecutionContext} t
Chris-Hibbert marked this conversation as resolved.
Show resolved Hide resolved
* @param {{ wallet: import('./wallet-tools.js').MockWallet, }} mine
* @param {{ toSend: AmountKeywordRecord }} shared
*/
export const receiverRose = async (t, { wallet }, { toSend }) => {
console.time('rose');
console.timeLog('rose', 'before notifiers');
const purseNotifier = mapValues(toSend, amt =>
wallet.peek.purseUpdates(amt.brand),
);
console.timeLog('rose', 'after notifiers; before initial');

const initial = await allValues(
mapValues(purseNotifier, pn => pn.next().then(u => u.value)),
);
console.timeLog('rose', 'got initial', initial);
t.log('Rose initial', initial);
t.deepEqual(keys(initial), keys(toSend));

const done = await trackDeposits(t, initial, purseNotifier, toSend);
t.log('Rose got balance updates', keys(done));
t.deepEqual(keys(done), keys(toSend));
};

/**
* Rex expects to receive `shared.toSend` amounts.
* Rex doesn't check his initial balances
*
* @param {import('ava').ExecutionContext} t
* @param {{ wallet: import('./wallet-tools.js').MockWallet, }} mine
* @param {{ toSend: AmountKeywordRecord }} shared
*/
export const receiverRex = async (t, { wallet }, { toSend }) => {
const purseUpdates = await allValues(
mapValues(toSend, amt => E(wallet.peek).purseUpdates(amt.brand)),
);

const initial = await allValues(
mapValues(purseUpdates, up =>
E(up)
.next()
.then(u => u.value),
),
);

const done = await trackDeposits(t, initial, purseUpdates, toSend);

t.log('Rex got balance updates', keys(done));
t.deepEqual(keys(done), keys(toSend));
};

export const senderContract = async (
t,
{ zoe, terms: { postalService: instance, destAddr: addr1 } },
) => {
const iIssuer = await E(zoe).getInvitationIssuer();
const iBrand = await E(iIssuer).getBrand();
const postalService = E(zoe).getPublicFacet(instance);
const purse = await E(iIssuer).makeEmptyPurse();

const noInvitations = AmountMath.make(iBrand, harden([]));
const pmt1 = await E(purse).withdraw(noInvitations);

t.log(
'senderContract: E(',
getInterfaceOf(await postalService),
').sendTo(',
addr1,
',',
noInvitations,
')',
);
const sent = await E(postalService).sendTo(addr1, pmt1);
t.deepEqual(sent, noInvitations);
};
Loading
Loading