-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #11 from Agoric/dc-pay-contract
feat: contract to pay someone
- Loading branch information
Showing
9 changed files
with
648 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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`, | ||
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
/** @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 | ||
* @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); | ||
}; |
Oops, something went wrong.