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(postalSvc): deliver payment using address #2

Merged
merged 3 commits into from
Dec 28, 2023
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
62 changes: 62 additions & 0 deletions contract/src/postalSvc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// @ts-check
import { E, Far } from '@endo/far';
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();
console.log('postalSvc 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 =>
Promise.resolve(pmtP).then(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 };
};
106 changes: 106 additions & 0 deletions contract/src/start-postalSvc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**
* @file core eval script* to start the postalSvc contract.
*
* * see test-gimix-proposal.js to make a script from this file.
*
* The `permit` export specifies the corresponding permit.
*/
// @ts-check

import { E, Far } from '@endo/far';

const { Fail } = assert;

const trace = (...args) => console.log('start-postalSvc', ...args);

const fail = msg => {
throw Error(msg);
};

/**
* ref https://github.com/Agoric/agoric-sdk/issues/8408#issuecomment-1741445458
*
* @param {ERef<import('@agoric/vats').NameAdmin>} namesByAddressAdmin
*/
const fixHub = async namesByAddressAdmin => {
/** @type {import('@agoric/vats').NameHub} */
const hub = Far('Hub work-around', {
lookup: async (addr, ...rest) => {
await E(namesByAddressAdmin).reserve(addr);
const addressAdmin = await E(namesByAddressAdmin).lookupAdmin(addr);
assert(addressAdmin, 'no admin???');
const addressHub = E(addressAdmin).readonly();
if (rest.length === 0) return addressHub;
await E(addressAdmin).reserve(rest[0]);
return E(addressHub).lookup(...rest);
},
has: _key => Fail`key space not well defined`,
entries: () => Fail`enumeration not supported`,
values: () => Fail`enumeration not supported`,
keys: () => Fail`enumeration not supported`,
});
return hub;
};

/**
* @param {BootstrapPowers} powers
* @param {{ options?: { postalSvc: {
* bundleID: string;
* }}}} config
*/
export const startPostalSvc = async (powers, config) => {
const {
consume: { zoe, namesByAddressAdmin },
installation: {
// @ts-expect-error not statically known at genesis
produce: { postalSvc: produceInstallation },
},
instance: {
// @ts-expect-error not statically known at genesis
produce: { postalSvc: produceInstance },
},
} = powers;
const { bundleID = fail(`no bundleID; try test-gimix-proposal.js?`) } =
config.options?.postalSvc ?? {};

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

const namesByAddress = await fixHub(namesByAddressAdmin);

const [IST, Invitation] = await Promise.all([
E(zoe).getFeeIssuer(),
E(zoe).getInvitationIssuer(),
]);
const { instance } = await E(zoe).startInstance(
installation,
{ IST, Invitation },
Copy link
Contributor

Choose a reason for hiding this comment

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

creatorFacet method to add issuers?
governed API to add issuers?
#8547

It seems like this may be a common requirement. I wonder if it's possible for a contract to have a dynamic list of issuers from a nameHub. Could maybe be added to StandardTerms from zoe or specified in the startInstance function.

Copy link
Contributor

Choose a reason for hiding this comment

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

I also wonder if this is a better approach here, saving issuers on the fly. Tying postalSvc to a NameHub like agoricNames might preclude wider usage.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Contracts usually add issuers that they are confident that they can rely on, so letting clients unilaterally add issuers is usually a no-no. But in this case, maybe it's ok, since we're just relaying assets that the parties choose to use?

{ namesByAddress },
);
produceInstance.resolve(instance);

trace('postalSvc started');
};

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

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

// script completion value
startPostalSvc;
124 changes: 124 additions & 0 deletions contract/test/test-postalSvc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// @ts-check
// XXX what's the state-of-the-art in ava setup?
// eslint-disable-next-line import/order
import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js';

import { createRequire } from 'module';

import { E, Far } from '@endo/far';
import { unsafeMakeBundleCache } from '@agoric/swingset-vat/tools/bundleTool.js';
import { makeNameHubKit, makePromiseSpace } from '@agoric/vats';
import { makeWellKnownSpaces } from '@agoric/vats/src/core/utils.js';
import { AmountMath } from '@agoric/ertp/src/amountMath.js';
import { makeFakeVatAdmin } from '@agoric/zoe/tools/fakeVatAdmin.js';
import { makeZoeKitForTest } from '@agoric/zoe/tools/setup-zoe.js';
import { startPostalSvc } from '../src/start-postalSvc.js';

/** @type {import('ava').TestFn<Awaited<ReturnType<makeTestContext>>>} */
const test = anyTest;

const myRequire = createRequire(import.meta.url);

const assets = {
postalSvc: myRequire.resolve('../src/postalSvc.js'),
};

const makeTestContext = async t => {
const bundleCache = await unsafeMakeBundleCache('bundles/');

return { bundleCache };
};

test.before(async t => (t.context = await makeTestContext(t)));

const bootstrap = async log => {
const { produce, consume } = makePromiseSpace();

const { admin, vatAdminState } = makeFakeVatAdmin();
const { zoeService: zoe, feeMintAccess } = makeZoeKitForTest(admin);

const { nameHub: agoricNames, nameAdmin: agoricNamesAdmin } =
makeNameHubKit();
const spaces = await makeWellKnownSpaces(agoricNamesAdmin, log, [
'installation',
'instance',
]);

const { nameAdmin: namesByAddressAdmin } = makeNameHubKit();

produce.zoe.resolve(zoe);
produce.feeMintAccess.resolve(feeMintAccess);
produce.agoricNames.resolve(agoricNames);
produce.namesByAddressAdmin.resolve(namesByAddressAdmin);

/** @type {BootstrapPowers}} */
// @ts-expect-error mock
const powers = { produce, consume, ...spaces };

return { powers, vatAdminState };
};

test('deliver payment using address', async t => {
t.log('bootstrap');
const { powers, vatAdminState } = await bootstrap(t.log);

const { bundleCache } = t.context;
const bundle = await bundleCache.load(assets.postalSvc, 'postalSvc');
const bundleID = `b1-${bundle.endoZipBase64Sha512}`;
t.log('publish bundle', bundleID.slice(0, 8));
vatAdminState.installBundle(bundleID, bundle);

await startPostalSvc(powers, {
options: { postalSvc: { bundleID } },
});

const { agoricNames, zoe, namesByAddressAdmin } = powers.consume;

const instance = await E(agoricNames).lookup('instance', 'postalSvc');

const addr1 = 'agoric1receiver';

const rxd = [];
const depositFacet = Far('DepositFacet', {
/** @param {Payment} pmt */
receive: async pmt => {
rxd.push(pmt);
// XXX should return amount of pmt
},
});

const my = makeNameHubKit();
my.nameAdmin.update('depositFacet', depositFacet);
await E(namesByAddressAdmin).update(addr1, my.nameHub, my.nameAdmin);

const { issuers, brands } = await E(zoe).getTerms(instance);
const postalSvc = E(zoe).getPublicFacet(instance);
const purse = await E(issuers.IST).makeEmptyPurse();

const pmt1 = await E(purse).withdraw(AmountMath.make(brands.IST, 0n));

// XXX should test that return value is amount
t.log('send IST with public facet to', addr1);
await E(postalSvc).sendTo(addr1, pmt1);
t.deepEqual(rxd, [pmt1]);

{
const Payment = AmountMath.make(brands.IST, 0n);
const pmt2 = await E(postalSvc).makeSendInvitation(addr1);
const pmt3 = await E(purse).withdraw(Payment);
const Invitation = await E(issuers.Invitation).getAmountOf(pmt2);
const proposal = { give: { Payment, Invitation } };
t.log('make offer to send IST, Invitation to', addr1);
const seat = E(zoe).offer(
E(postalSvc).makeSendInvitation(addr1),
proposal,
{ Payment: pmt3, Invitation: pmt2 },
);
// XXX test is overly sensitive to order?
const result = await E(seat).getOfferResult();
t.is(result, 'sent Invitation, Payment');
t.deepEqual(rxd, [pmt1, pmt2, pmt3]);
const done = await E(seat).getPayouts();
}
});
test.todo('partial failure: send N+1 payments where >= 1 delivery fails');