From 4a14455e10f1e3807fd6633594c86a0f60026393 Mon Sep 17 00:00:00 2001 From: Kate Sills Date: Tue, 30 Jun 2020 11:11:42 -0700 Subject: [PATCH] feat(zoe): Zoe release 0.7.0 (#1143) ## Release v0.7.0 (29-June-2020) Zoe Service changes: * Instead of `zoe.makeInstance` returning an invite only, it now returns a record of `{ invite, instanceRecord }` such that information like the `instanceHandle` can be obtained directly from the instanceRecord. * `installationHandle`, the identifier for the code that is used to create a new running contract instance, is added to the extent of invites for contracts so that interested parties can easily check whether their invite is using the code they expect. * `brandKeywordRecord` is added as a property of `instanceRecord` alongside `issuerKeywordRecord`. `brandKeywordRecord` is an object with keyword keys and brand values. * `zoe.makeContract` now only accepts a single argument (`bundle`) and the old module format will error. Zoe Contract Facet (zcf) changes: * `zcf.reallocate` no longer accepts a third argument `sparseKeywords` and no longer expects the keywords for different offers to be the same. Within reallocate, the offer safety check compares the user's proposal to the user's allocation, and the rights conservation check adds up the amounts by brand to ensure the totals are the same. Neither of these checks requires that the keywords for the allocations be the same for different offers. * `zcf.getCurrentAllocation` and `zcf.getCurrentAllocations` no longer take sparseKeywords as a parameter. Instead, brandKeywordRecord is an optional parameter. If omitted, amounts are returned only for brands for which an allocation currently exists. * `zcf.getInstanceRecord()` no longer takes a parameter * `brandKeywordRecord` is added as a property of `instanceRecord` alongside `issuerKeywordRecord`. `brandKeywordRecord` is an object with keyword keys and brand values. * `zcf.getAmountMaths` has been subsumed by `zcf.getAmountMath` which takes a single brand parameter. * `zcf.getBrandForIssuer` has been added. It synchronously returns the brand for a given issuer already known to Zoe. * `zoe.getInstance`, which was deprecated earlier, has been removed. * `cancelObj` and `cancelObj.cancel()`, which were deprecated earlier, have been removed. Changes for Zoe contract developers: * Zoe contracts are now expected to return only an invite as the result of `makeContract`. If the contracts want to have a `publicAPI`, they can do so through `zcf.initPublicAPI`. * Contracts can allow different offers to use different keywords for the same issuers. For example, publicAuction, the second price auction contract, uses 'Asset' and 'Ask' for the seller keywords and 'Asset' and 'Bid' for the buyer keywords. Built-in Zoe contract changes: * We added more comments to the start of the built-in Zoe contracts. * The operaTickets contract has been split into two contracts: a generic `sellItems` contract that sells fungible or nonfungible items at a set price for money, and a generic `mintAndSellNFT` contract that mints NFT tokens and then immediately creates a new `sellItems` instance to sell them. The original operaTicket tests are able to use these contracts. * The `getCurrentPrice` helper in `bondingCurves.js` has been renamed to `getInputPrice` and now only returns the `outputExtent`. * A new built-in contract was added: barter-exchange.js. Barter Exchange takes offers for trading arbitrary goods for one another. * Autoswap now has different keywords for different actions that can be taken. For example, a swap should have the keywords 'In' and 'Out'. * Multipool Autoswap has new keywords for different actions that can be taken as well. For example, adding liquidity has the keywords: 'SecondaryToken' and 'CentralToken' and returns a payout with keyword `Liquidity`. * In Public Auction, the seller keywords are 'Asset' and 'Ask' and the buyer keywords are 'Asset' and 'Bid'. ZoeHelpers changes: Some helpers were removed, and others were added. The built-in Zoe contracts were rewritten to take advantage of these new helpers. * `satisfies` was added. It checks whether an allocation would satisfy a single offer's wants if that was the allocation passed to `reallocate`. * `isOfferSafe` was added. It checks whether an allocation for a particular offer would satisfy offer safety. Any allocation that returns true under `satisfy` will also return true under `isOfferSafe`. (`isOfferSafe` is equivalent of `satisfies` || gives a refund). * `trade` was added. `Trade` performs a trade between two offers given a declarative description of what each side loses and gains. * `Swap` remains but has slightly different behavior: any surplus in a trade remains with the original offer * `canTradeWith` was removed and subsumed by `satisfies`. * `inviteAnOffer` was already deprecated and was removed. * `assertNatMathHelpers` was added, which checks whether a particular keyword is associated with an issuer with natMathHelpers. ERTP changes: * `purse.deposit()` now returns the amount of the deposit, rather than the purse's new balance. * A deposit-only facet was added to purses, and can be created by calling `makeDepositFacet` on any purse. --- packages/ERTP/NEWS.md | 7 + packages/ERTP/src/issuer.js | 3 +- .../ERTP/test/unitTests/test-issuerObj.js | 62 +- packages/cosmic-swingset/test/test-home.js | 2 +- .../test/unitTests/test-lib-wallet.js | 222 +++- packages/store/src/store.js | 19 +- packages/zoe/NEWS.md | 108 ++ packages/zoe/package.json | 1 + packages/zoe/src/cleanProposal.js | 33 +- packages/zoe/src/contractSupport/auctions.js | 20 +- .../zoe/src/contractSupport/bondingCurves.js | 20 +- packages/zoe/src/contractSupport/index.js | 2 +- .../zoe/src/contractSupport/zoeHelpers.js | 291 +++-- packages/zoe/src/contracts/atomicSwap.js | 84 +- packages/zoe/src/contracts/automaticRefund.js | 49 +- packages/zoe/src/contracts/autoswap.js | 498 ++++---- packages/zoe/src/contracts/barterExchange.js | 134 ++ packages/zoe/src/contracts/coveredCall.js | 125 +- packages/zoe/src/contracts/mintAndSellNFT.js | 115 ++ packages/zoe/src/contracts/mintPayments.js | 120 +- .../zoe/src/contracts/multipoolAutoswap.js | 1093 ++++++++--------- .../zoe/src/contracts/operaConcertTicket.js | 173 --- packages/zoe/src/contracts/publicAuction.js | 235 ++-- packages/zoe/src/contracts/sellItems.js | 129 ++ packages/zoe/src/contracts/simpleExchange.js | 261 ++-- packages/zoe/src/objArrayConversion.js | 78 +- packages/zoe/src/offerSafety.js | 130 +- packages/zoe/src/rightsConservation.js | 111 +- packages/zoe/src/state.js | 24 +- packages/zoe/src/zoe.js | 378 +++--- .../swingsetTests/zoe-metering/bootstrap.js | 9 +- .../zoe-metering/infiniteTestLoop.js | 14 +- .../zoe-metering/testBuiltins.js | 14 +- .../zoe/test/swingsetTests/zoe/bootstrap.js | 4 + .../zoe/test/swingsetTests/zoe/test-zoe.js | 33 +- .../zoe/test/swingsetTests/zoe/vat-alice.js | 101 +- .../zoe/test/swingsetTests/zoe/vat-bob.js | 91 +- .../zoe/test/swingsetTests/zoe/vat-carol.js | 2 +- .../zoe/test/swingsetTests/zoe/vat-dave.js | 2 +- .../contractSupport/test-bondingCurves.js | 62 +- .../contractSupport/test-zoeHelpers.js | 775 ++++++------ .../unitTests/contracts/brokenAutoRefund.js | 18 +- .../zoe/test/unitTests/contracts/grifter.js | 12 +- .../unitTests/contracts/test-atomicSwap.js | 123 +- .../contracts/test-automaticRefund.js | 28 +- .../test/unitTests/contracts/test-autoswap.js | 113 +- .../test/unitTests/contracts/test-barter.js | 145 +++ .../contracts/test-brokenContract.js | 1 - .../unitTests/contracts/test-coveredCall.js | 14 +- .../test/unitTests/contracts/test-grifter.js | 5 +- .../unitTests/contracts/test-mintPayments.js | 18 +- .../contracts/test-multipoolAutoswap.js | 90 +- .../contracts/test-operaConcertTicket.js | 457 ------- .../unitTests/contracts/test-publicAuction.js | 71 +- .../unitTests/contracts/test-sellTickets.js | 563 +++++++++ .../contracts/test-simpleExchange.js | 47 +- .../zoe/test/unitTests/test-cleanProposal.js | 97 +- .../zoe/test/unitTests/test-offerSafety.js | 151 +-- .../test/unitTests/test-rightsConservation.js | 64 +- 59 files changed, 4358 insertions(+), 3293 deletions(-) create mode 100644 packages/zoe/src/contracts/barterExchange.js create mode 100644 packages/zoe/src/contracts/mintAndSellNFT.js delete mode 100644 packages/zoe/src/contracts/operaConcertTicket.js create mode 100644 packages/zoe/src/contracts/sellItems.js create mode 100644 packages/zoe/test/unitTests/contracts/test-barter.js delete mode 100644 packages/zoe/test/unitTests/contracts/test-operaConcertTicket.js create mode 100644 packages/zoe/test/unitTests/contracts/test-sellTickets.js diff --git a/packages/ERTP/NEWS.md b/packages/ERTP/NEWS.md index 49cb97974d1..7775568a56e 100644 --- a/packages/ERTP/NEWS.md +++ b/packages/ERTP/NEWS.md @@ -1,5 +1,12 @@ User-visible changes in ERTP: +## Release v0.6.0 (29-June-2020) + +* `purse.deposit()` now returns the amount of the deposit, rather than + the purse's new balance. +* A deposit-only facet was added to purses, and can be created by + calling `makeDepositFacet` on any purse. + ## Release v0.5.0 (26-Mar-2020) Changed most ERTP methods to now accept promises for payments. The diff --git a/packages/ERTP/src/issuer.js b/packages/ERTP/src/issuer.js index d515d6f8160..7972517c7cb 100644 --- a/packages/ERTP/src/issuer.js +++ b/packages/ERTP/src/issuer.js @@ -276,7 +276,7 @@ function produceIssuer(allegedName, mathHelpersName = 'nat') { }), ); assert(payments.length === 0, 'no payments should be returned'); - return newPurseBalance; + return srcPaymentBalance; }, withdraw: amount => { amount = amountMath.coerce(amount); @@ -295,6 +295,7 @@ function produceIssuer(allegedName, mathHelpersName = 'nat') { }, getCurrentAmount: () => purseLedger.get(purse), getAllegedBrand: () => brand, + makeDepositFacet: () => harden({ receive: purse.deposit }), }); return purse; }; diff --git a/packages/ERTP/test/unitTests/test-issuerObj.js b/packages/ERTP/test/unitTests/test-issuerObj.js index 7c95c1147fe..0edb4081e5e 100644 --- a/packages/ERTP/test/unitTests/test-issuerObj.js +++ b/packages/ERTP/test/unitTests/test-issuerObj.js @@ -101,31 +101,42 @@ test('issuer.makeEmptyPurse', t => { .catch(e => t.assert(false, e)); }); -test('issuer.deposit', t => { - t.plan(2); +test('purse.deposit', async t => { + t.plan(4); const { issuer, mint, amountMath } = produceIssuer('fungible'); + const fungible0 = amountMath.getEmpty(); + const fungible17 = amountMath.make(17); const fungible25 = amountMath.make(25); + const fungibleSum = amountMath.add(fungible17, fungible25); const purse = issuer.makeEmptyPurse(); - const payment = mint.mintPayment(fungible25); - - const checkDeposit = newPurseBalance => { + const payment17 = mint.mintPayment(fungible17); + const payment25 = mint.mintPayment(fungible25); + + const checkDeposit = ( + expectedOldBalance, + expectedNewBalance, + ) => depositResult => { + const delta = amountMath.subtract(expectedNewBalance, expectedOldBalance); t.ok( - amountMath.isEqual(newPurseBalance, fungible25), - `the balance returned is the purse balance`, + amountMath.isEqual(depositResult, delta), + `the balance changes by the deposited amount: ${delta.extent}`, ); t.ok( - amountMath.isEqual(purse.getCurrentAmount(), fungible25), - `the new purse balance is the payment's old balance`, + amountMath.isEqual(purse.getCurrentAmount(), expectedNewBalance), + `the new purse balance ${depositResult.extent} is the expected amount: ${expectedNewBalance.extent}`, ); }; - E(purse) - .deposit(payment, fungible25) - .then(checkDeposit); + await E(purse) + .deposit(payment17, fungible17) + .then(checkDeposit(fungible0, fungible17)); + await E(purse) + .deposit(payment25, fungible25) + .then(checkDeposit(fungible17, fungibleSum)); }); -test('issuer.deposit promise', t => { +test('purse.deposit promise', t => { t.plan(1); const { issuer, mint, amountMath } = produceIssuer('fungible'); const fungible25 = amountMath.make(25); @@ -141,6 +152,31 @@ test('issuer.deposit promise', t => { ); }); +test('purse.makeDepositFacet', t => { + t.plan(2); + const { issuer, mint, amountMath } = produceIssuer('fungible'); + const fungible25 = amountMath.make(25); + + const purse = issuer.makeEmptyPurse(); + const payment = mint.mintPayment(fungible25); + + const checkDeposit = newPurseBalance => { + t.ok( + amountMath.isEqual(newPurseBalance, fungible25), + `the balance returned is the purse balance`, + ); + t.ok( + amountMath.isEqual(purse.getCurrentAmount(), fungible25), + `the new purse balance is the payment's old balance`, + ); + }; + + E(purse) + .makeDepositFacet() + .then(({ receive }) => receive(payment)) + .then(checkDeposit); +}); + test('issuer.burn', t => { t.plan(2); const { issuer, mint, amountMath } = produceIssuer('fungible'); diff --git a/packages/cosmic-swingset/test/test-home.js b/packages/cosmic-swingset/test/test-home.js index 7995a36dd0e..b270215b4d5 100644 --- a/packages/cosmic-swingset/test/test-home.js +++ b/packages/cosmic-swingset/test/test-home.js @@ -119,7 +119,7 @@ test('home.wallet - receive zoe invite', async t => { ); const bundle = await bundleSource(contractRoot); const installationHandle = await E(zoe).install(bundle); - const invite = await E(zoe).makeInstance(installationHandle); + const { invite } = await E(zoe).makeInstance(installationHandle); // Check that the wallet knows about the Zoe invite issuer and starts out // with a default Zoe invite issuer purse. diff --git a/packages/cosmic-swingset/test/unitTests/test-lib-wallet.js b/packages/cosmic-swingset/test/unitTests/test-lib-wallet.js index 979da0f5b34..c3a0c81697e 100644 --- a/packages/cosmic-swingset/test/unitTests/test-lib-wallet.js +++ b/packages/cosmic-swingset/test/unitTests/test-lib-wallet.js @@ -2,22 +2,19 @@ import '@agoric/install-ses'; // calls lockdown() // eslint-disable-next-line import/no-extraneous-dependencies import { test } from 'tape-promise/tape'; import bundleSource from '@agoric/bundle-source'; +import makeAmountMath from '@agoric/ertp/src/amountMath'; import produceIssuer from '@agoric/ertp'; import { makeZoe } from '@agoric/zoe'; import { makeRegistrar } from '@agoric/registrar'; import harden from '@agoric/harden'; -import { makeGetInstanceHandle } from '@agoric/zoe/src/clientSupport'; +import { E } from '@agoric/eventual-send'; import { makeWallet } from '../../lib/ag-solo/vats/lib-wallet'; import { makeBoard } from '../../lib/ag-solo/vats/lib-board'; import { makeMailboxAdmin } from '../../lib/ag-solo/vats/lib-mailbox'; const setupTest = async () => { - const contractRoot = require.resolve( - '@agoric/zoe/src/contracts/automaticRefund', - ); - const bundle = await bundleSource(contractRoot); const pursesStateChangeLog = []; const inboxStateChangeLog = []; const pursesStateChangeHandler = data => { @@ -28,27 +25,52 @@ const setupTest = async () => { }; const moolaBundle = produceIssuer('moola'); + const simoleanBundle = produceIssuer('simolean'); const rpgBundle = produceIssuer('rpg', 'strSet'); const zoe = makeZoe(); const registry = makeRegistrar(); const board = makeBoard(); const mailboxAdmin = makeMailboxAdmin(board); - const installationHandle = await zoe.install(bundle); - - const issuerKeywordRecord = harden({ Contribution: moolaBundle.issuer }); - const invite = await zoe.makeInstance( - installationHandle, - issuerKeywordRecord, + // Create AutomaticRefund instance + const automaticRefundContractRoot = require.resolve( + '@agoric/zoe/src/contracts/automaticRefund', ); - const inviteIssuer = zoe.getInviteIssuer(); - const getInstanceHandle = makeGetInstanceHandle(inviteIssuer); - const instanceHandle = getInstanceHandle(invite); + const automaticRefundBundle = await bundleSource(automaticRefundContractRoot); + const installationHandle = await zoe.install(automaticRefundBundle); + const issuerKeywordRecord = harden({ Contribution: moolaBundle.issuer }); + const { + invite, + instanceRecord: { handle: instanceHandle }, + } = await zoe.makeInstance(installationHandle, issuerKeywordRecord); const instanceRegKey = registry.register( 'automaticRefundInstanceHandle', instanceHandle, ); + // Create Autoswap instance + const autoswapContractRoot = require.resolve( + '@agoric/zoe/src/contracts/autoswap', + ); + const autoswapBundle = await bundleSource(autoswapContractRoot); + const autoswapInstallationHandle = await zoe.install(autoswapBundle); + const autoswapIssuerKeywordRecord = harden({ + TokenA: moolaBundle.issuer, + TokenB: simoleanBundle.issuer, + }); + const { + invite: addLiquidityInvite, + instanceRecord: { handle: autoswapInstanceHandle }, + } = await zoe.makeInstance( + autoswapInstallationHandle, + autoswapIssuerKeywordRecord, + ); + + const autoswapInstanceRegKey = registry.register( + 'autoswapInstanceHandle', + autoswapInstanceHandle, + ); + const wallet = await makeWallet({ zoe, registry, @@ -59,6 +81,7 @@ const setupTest = async () => { }); return { moolaBundle, + simoleanBundle, rpgBundle, zoe, registry, @@ -66,9 +89,11 @@ const setupTest = async () => { board, wallet, invite, + addLiquidityInvite, installationHandle, instanceHandle, instanceRegKey, + autoswapInstanceRegKey, pursesStateChangeLog, inboxStateChangeLog, }; @@ -214,7 +239,7 @@ test('lib-wallet offer methods', async t => { pursePetname: 'Fun budget', extent: 1, issuerPetname: 'moola', - brandRegKey: 'moolabrand_2059', + brandRegKey: 'moolabrand_9794', }, }, exit: { onDemand: null }, @@ -248,10 +273,10 @@ test('lib-wallet offer methods', async t => { t.deepEquals( pursesStateChangeLog, [ - '[{"issuerPetname":"moola","brandRegKey":"moolabrand_2059","pursePetname":"Fun budget","extent":0,"currentAmountSlots":{"body":"{\\"brand\\":{\\"@qclass\\":\\"slot\\",\\"index\\":0},\\"extent\\":0}","slots":[{"kind":"brand","petname":"moola"}]},"currentAmount":{"brand":{"kind":"brand","petname":"moola"},"extent":0}}]', - '[{"issuerPetname":"moola","brandRegKey":"moolabrand_2059","pursePetname":"Fun budget","extent":100,"currentAmountSlots":{"body":"{\\"brand\\":{\\"@qclass\\":\\"slot\\",\\"index\\":0},\\"extent\\":100}","slots":[{"kind":"brand","petname":"moola"}]},"currentAmount":{"brand":{"kind":"brand","petname":"moola"},"extent":100}}]', - '[{"issuerPetname":"moola","brandRegKey":"moolabrand_2059","pursePetname":"Fun budget","extent":99,"currentAmountSlots":{"body":"{\\"brand\\":{\\"@qclass\\":\\"slot\\",\\"index\\":0},\\"extent\\":99}","slots":[{"kind":"brand","petname":"moola"}]},"currentAmount":{"brand":{"kind":"brand","petname":"moola"},"extent":99}}]', - '[{"issuerPetname":"moola","brandRegKey":"moolabrand_2059","pursePetname":"Fun budget","extent":100,"currentAmountSlots":{"body":"{\\"brand\\":{\\"@qclass\\":\\"slot\\",\\"index\\":0},\\"extent\\":100}","slots":[{"kind":"brand","petname":"moola"}]},"currentAmount":{"brand":{"kind":"brand","petname":"moola"},"extent":100}}]', + '[{"issuerPetname":"moola","brandRegKey":"moolabrand_9794","pursePetname":"Fun budget","extent":0,"currentAmountSlots":{"body":"{\\"brand\\":{\\"@qclass\\":\\"slot\\",\\"index\\":0},\\"extent\\":0}","slots":[{"kind":"brand","petname":"moola"}]},"currentAmount":{"brand":{"kind":"brand","petname":"moola"},"extent":0}}]', + '[{"issuerPetname":"moola","brandRegKey":"moolabrand_9794","pursePetname":"Fun budget","extent":100,"currentAmountSlots":{"body":"{\\"brand\\":{\\"@qclass\\":\\"slot\\",\\"index\\":0},\\"extent\\":100}","slots":[{"kind":"brand","petname":"moola"}]},"currentAmount":{"brand":{"kind":"brand","petname":"moola"},"extent":100}}]', + '[{"issuerPetname":"moola","brandRegKey":"moolabrand_9794","pursePetname":"Fun budget","extent":99,"currentAmountSlots":{"body":"{\\"brand\\":{\\"@qclass\\":\\"slot\\",\\"index\\":0},\\"extent\\":99}","slots":[{"kind":"brand","petname":"moola"}]},"currentAmount":{"brand":{"kind":"brand","petname":"moola"},"extent":99}}]', + '[{"issuerPetname":"moola","brandRegKey":"moolabrand_9794","pursePetname":"Fun budget","extent":100,"currentAmountSlots":{"body":"{\\"brand\\":{\\"@qclass\\":\\"slot\\",\\"index\\":0},\\"extent\\":100}","slots":[{"kind":"brand","petname":"moola"}]},"currentAmount":{"brand":{"kind":"brand","petname":"moola"},"extent":100}}]', ], `purses state change log`, ); @@ -259,12 +284,12 @@ test('lib-wallet offer methods', async t => { inboxStateChangeLog, [ '[{"id":"unknown#1588645041696","instanceRegKey":"automaticrefundinstancehandle_3467","hooks":{"publicAPI":{"getInvite":["makeInvite"]}},"proposalTemplate":{"give":{"Contribution":{"pursePetname":"Fun budget","extent":1}},"exit":{"onDemand":null}},"requestContext":{"origin":"unknown"}}]', - '[{"id":"unknown#1588645041696","instanceRegKey":"automaticrefundinstancehandle_3467","hooks":{"publicAPI":{"getInvite":["makeInvite"]}},"proposalTemplate":{"give":{"Contribution":{"pursePetname":"Fun budget","extent":1,"issuerPetname":"moola","brandRegKey":"moolabrand_2059"}},"exit":{"onDemand":null}},"requestContext":{"origin":"unknown"}}]', - '[{"id":"unknown#1588645041696","instanceRegKey":"automaticrefundinstancehandle_3467","hooks":{"publicAPI":{"getInvite":["makeInvite"]}},"proposalTemplate":{"give":{"Contribution":{"pursePetname":"Fun budget","extent":1,"issuerPetname":"moola","brandRegKey":"moolabrand_2059"}},"exit":{"onDemand":null}},"requestContext":{"origin":"unknown"},"status":"pending"}]', - '[{"id":"unknown#1588645041696","instanceRegKey":"automaticrefundinstancehandle_3467","hooks":{"publicAPI":{"getInvite":["makeInvite"]}},"proposalTemplate":{"give":{"Contribution":{"pursePetname":"Fun budget","extent":1,"issuerPetname":"moola","brandRegKey":"moolabrand_2059"}},"exit":{"onDemand":null}},"requestContext":{"origin":"unknown"},"status":"accept"}]', - '[{"id":"unknown#1588645041696","instanceRegKey":"automaticrefundinstancehandle_3467","hooks":{"publicAPI":{"getInvite":["makeInvite"]}},"proposalTemplate":{"give":{"Contribution":{"pursePetname":"Fun budget","extent":1,"issuerPetname":"moola","brandRegKey":"moolabrand_2059"}},"exit":{"onDemand":null}},"requestContext":{"origin":"unknown"},"status":"accept"},{"id":"unknown#1588645230204","instanceRegKey":"automaticrefundinstancehandle_3467","hooks":{"publicAPI":{"getInvite":["makeInvite"]}},"proposalTemplate":{"give":{"Contribution":{"pursePetname":"Fun budget","extent":1}},"exit":{"onDemand":null}},"requestContext":{"origin":"unknown"}}]', - '[{"id":"unknown#1588645041696","instanceRegKey":"automaticrefundinstancehandle_3467","hooks":{"publicAPI":{"getInvite":["makeInvite"]}},"proposalTemplate":{"give":{"Contribution":{"pursePetname":"Fun budget","extent":1,"issuerPetname":"moola","brandRegKey":"moolabrand_2059"}},"exit":{"onDemand":null}},"requestContext":{"origin":"unknown"},"status":"accept"},{"id":"unknown#1588645230204","instanceRegKey":"automaticrefundinstancehandle_3467","hooks":{"publicAPI":{"getInvite":["makeInvite"]}},"proposalTemplate":{"give":{"Contribution":{"pursePetname":"Fun budget","extent":1,"issuerPetname":"moola","brandRegKey":"moolabrand_2059"}},"exit":{"onDemand":null}},"requestContext":{"origin":"unknown"}}]', - '[{"id":"unknown#1588645041696","instanceRegKey":"automaticrefundinstancehandle_3467","hooks":{"publicAPI":{"getInvite":["makeInvite"]}},"proposalTemplate":{"give":{"Contribution":{"pursePetname":"Fun budget","extent":1,"issuerPetname":"moola","brandRegKey":"moolabrand_2059"}},"exit":{"onDemand":null}},"requestContext":{"origin":"unknown"},"status":"accept"},{"id":"unknown#1588645230204","instanceRegKey":"automaticrefundinstancehandle_3467","hooks":{"publicAPI":{"getInvite":["makeInvite"]}},"proposalTemplate":{"give":{"Contribution":{"pursePetname":"Fun budget","extent":1,"issuerPetname":"moola","brandRegKey":"moolabrand_2059"}},"exit":{"onDemand":null}},"requestContext":{"origin":"unknown"},"status":"decline"}]', + '[{"id":"unknown#1588645041696","instanceRegKey":"automaticrefundinstancehandle_3467","hooks":{"publicAPI":{"getInvite":["makeInvite"]}},"proposalTemplate":{"give":{"Contribution":{"pursePetname":"Fun budget","extent":1,"issuerPetname":"moola","brandRegKey":"moolabrand_9794"}},"exit":{"onDemand":null}},"requestContext":{"origin":"unknown"}}]', + '[{"id":"unknown#1588645041696","instanceRegKey":"automaticrefundinstancehandle_3467","hooks":{"publicAPI":{"getInvite":["makeInvite"]}},"proposalTemplate":{"give":{"Contribution":{"pursePetname":"Fun budget","extent":1,"issuerPetname":"moola","brandRegKey":"moolabrand_9794"}},"exit":{"onDemand":null}},"requestContext":{"origin":"unknown"},"status":"pending"}]', + '[{"id":"unknown#1588645041696","instanceRegKey":"automaticrefundinstancehandle_3467","hooks":{"publicAPI":{"getInvite":["makeInvite"]}},"proposalTemplate":{"give":{"Contribution":{"pursePetname":"Fun budget","extent":1,"issuerPetname":"moola","brandRegKey":"moolabrand_9794"}},"exit":{"onDemand":null}},"requestContext":{"origin":"unknown"},"status":"accept"}]', + '[{"id":"unknown#1588645041696","instanceRegKey":"automaticrefundinstancehandle_3467","hooks":{"publicAPI":{"getInvite":["makeInvite"]}},"proposalTemplate":{"give":{"Contribution":{"pursePetname":"Fun budget","extent":1,"issuerPetname":"moola","brandRegKey":"moolabrand_9794"}},"exit":{"onDemand":null}},"requestContext":{"origin":"unknown"},"status":"accept"},{"id":"unknown#1588645230204","instanceRegKey":"automaticrefundinstancehandle_3467","hooks":{"publicAPI":{"getInvite":["makeInvite"]}},"proposalTemplate":{"give":{"Contribution":{"pursePetname":"Fun budget","extent":1}},"exit":{"onDemand":null}},"requestContext":{"origin":"unknown"}}]', + '[{"id":"unknown#1588645041696","instanceRegKey":"automaticrefundinstancehandle_3467","hooks":{"publicAPI":{"getInvite":["makeInvite"]}},"proposalTemplate":{"give":{"Contribution":{"pursePetname":"Fun budget","extent":1,"issuerPetname":"moola","brandRegKey":"moolabrand_9794"}},"exit":{"onDemand":null}},"requestContext":{"origin":"unknown"},"status":"accept"},{"id":"unknown#1588645230204","instanceRegKey":"automaticrefundinstancehandle_3467","hooks":{"publicAPI":{"getInvite":["makeInvite"]}},"proposalTemplate":{"give":{"Contribution":{"pursePetname":"Fun budget","extent":1,"issuerPetname":"moola","brandRegKey":"moolabrand_9794"}},"exit":{"onDemand":null}},"requestContext":{"origin":"unknown"}}]', + '[{"id":"unknown#1588645041696","instanceRegKey":"automaticrefundinstancehandle_3467","hooks":{"publicAPI":{"getInvite":["makeInvite"]}},"proposalTemplate":{"give":{"Contribution":{"pursePetname":"Fun budget","extent":1,"issuerPetname":"moola","brandRegKey":"moolabrand_9794"}},"exit":{"onDemand":null}},"requestContext":{"origin":"unknown"},"status":"accept"},{"id":"unknown#1588645230204","instanceRegKey":"automaticrefundinstancehandle_3467","hooks":{"publicAPI":{"getInvite":["makeInvite"]}},"proposalTemplate":{"give":{"Contribution":{"pursePetname":"Fun budget","extent":1,"issuerPetname":"moola","brandRegKey":"moolabrand_9794"}},"exit":{"onDemand":null}},"requestContext":{"origin":"unknown"},"status":"decline"}]', ], `inbox state change log`, ); @@ -275,3 +300,150 @@ test('lib-wallet offer methods', async t => { t.end(); } }); + +test('lib-wallet addOffer for autoswap swap', async t => { + try { + const { + zoe, + moolaBundle, + simoleanBundle, + wallet, + autoswapInstanceRegKey, + addLiquidityInvite, + registry, + } = await setupTest(); + + const moolaBrandRegKey = registry.register('moolaBrand', moolaBundle.brand); + await wallet.addIssuer('moola', moolaBundle.issuer, moolaBrandRegKey); + await wallet.makeEmptyPurse('moola', 'Fun budget'); + await wallet.deposit( + 'Fun budget', + moolaBundle.mint.mintPayment(moolaBundle.amountMath.make(1000)), + ); + + const simoleanBrandRegKey = registry.register( + 'simoleanBrand', + simoleanBundle.brand, + ); + await wallet.addIssuer( + 'simolean', + simoleanBundle.issuer, + simoleanBrandRegKey, + ); + await wallet.makeEmptyPurse('simolean', 'Nest egg'); + await wallet.deposit( + 'Nest egg', + simoleanBundle.mint.mintPayment(simoleanBundle.amountMath.make(1000)), + ); + + const instanceHandle = await E(registry).get(autoswapInstanceRegKey); + const { publicAPI } = await E(zoe).getInstanceRecord(instanceHandle); + + const liquidityIssuer = await E(publicAPI).getLiquidityIssuer(); + + const getLocalAmountMath = issuer => + Promise.all([ + E(issuer).getBrand(), + E(issuer).getMathHelpersName(), + ]).then(([brand, mathHelpersName]) => + makeAmountMath(brand, mathHelpersName), + ); + const liquidityAmountMath = await getLocalAmountMath(liquidityIssuer); + + // Let's add liquidity using our wallet and the addLiquidityInvite + // we have. + const proposal = harden({ + give: { + TokenA: moolaBundle.amountMath.make(900), + TokenB: simoleanBundle.amountMath.make(500), + }, + want: { + Liquidity: liquidityAmountMath.getEmpty(), + }, + }); + + const pursesArray = await E(wallet).getPurses(); + const purses = new Map(pursesArray); + + const moolaPurse = purses.get('Fun budget'); + const simoleanPurse = purses.get('Nest egg'); + + const moolaPayment = await E(moolaPurse).withdraw(proposal.give.TokenA); + const simoleanPayment = await E(simoleanPurse).withdraw( + proposal.give.TokenB, + ); + + const payments = harden({ + TokenA: moolaPayment, + TokenB: simoleanPayment, + }); + const { outcome: addLiqOutcome } = await E(zoe).offer( + addLiquidityInvite, + proposal, + payments, + ); + await addLiqOutcome; + + const rawId = '1593482020370'; + const id = `unknown#${rawId}`; + + const offer = { + id: rawId, + instanceRegKey: autoswapInstanceRegKey, + hooks: { + publicAPI: { + getInvite: ['makeSwapInvite'], + }, + }, + proposalTemplate: { + give: { + In: { + pursePetname: 'Fun budget', + extent: 30, + }, + }, + want: { + Out: { + pursePetname: 'Nest egg', + extent: 1, + }, + }, + exit: { + onDemand: null, + }, + }, + }; + + const hooks = wallet.hydrateHooks(offer.hooks); + await wallet.addOffer(offer, hooks); + + const { outcome, depositedP } = await wallet.acceptOffer(id); + t.equals( + await outcome, + 'Swap successfully completed.', + `offer was accepted`, + ); + await depositedP; + const offerHandles = wallet.getOfferHandles(harden([id])); + const offerHandle = wallet.getOfferHandle(id); + t.equals( + offerHandle, + offerHandles[0], + `both getOfferHandle(s) methods work`, + ); + t.deepEquals( + await moolaPurse.getCurrentAmount(), + moolaBundle.amountMath.make(70), + `moola purse balance`, + ); + t.deepEquals( + await simoleanPurse.getCurrentAmount(), + simoleanBundle.amountMath.make(516), + `simolean purse balance`, + ); + } catch (e) { + t.isNot(e, e, 'unexpected exception'); + } finally { + t.end(); + } +}); diff --git a/packages/store/src/store.js b/packages/store/src/store.js index 2effb225320..c9a1f62fdef 100644 --- a/packages/store/src/store.js +++ b/packages/store/src/store.js @@ -8,15 +8,16 @@ const harden = /** @type {(x: T) => T} */ (rawHarden); /** * @template K,V - * @typedef {Object} Store A wrapper around a Map - * @property {(key: K) => boolean} has Check if a key exists - * @property {(key: K, value: V) => void} init Initialize the key only if it doesn't already exist - * @property {(key: K) => V?} get Return a value fo the key, or undefined - * @property {(key: K, value: V) => void} set Unconditionally set the key - * @property {(key: K) => void} delete Remove the key - * @property {() => K[]} keys Return an array of keys - * @property {() => V[]} values Return an array of values - * @property {() => [K, V][]} entries Return an array of entries + * @typedef {Object} Store - A wrapper around a Map + * @property {(key: K) => boolean} has - Check if a key exists + * @property {(key: K, value: V) => void} init - Initialize the key only if it doesn't already exist + * @property {(key: K) => V} get - Return a value for the key. Throws + * if not found. + * @property {(key: K, value: V) => void} set - Unconditionally set the key + * @property {(key: K) => void} delete - Remove the key + * @property {() => K[]} keys - Return an array of keys + * @property {() => V[]} values - Return an array of values + * @property {() => [K, V][]} entries - Return an array of entries */ /** diff --git a/packages/zoe/NEWS.md b/packages/zoe/NEWS.md index 88e12bd846d..95ca79bfa03 100644 --- a/packages/zoe/NEWS.md +++ b/packages/zoe/NEWS.md @@ -1,5 +1,113 @@ User-visible changes in @agoric/zoe: +## Release v0.7.0 (29-June-2020) + +Zoe Service changes: + +* Instead of `zoe.makeInstance` returning an invite only, it now + returns a record of `{ invite, instanceRecord }` such that + information like the `instanceHandle` can be obtained directly from + the instanceRecord. +* `installationHandle`, the identifier for the code that is used to + create a new running contract instance, is added to the extent of + invites for contracts so that interested parties can easily check + whether their invite is using the code they expect. +* `brandKeywordRecord` is added as a property of `instanceRecord` + alongside `issuerKeywordRecord`. `brandKeywordRecord` is an object + with keyword keys and brand values. +* `zoe.makeContract` now only accepts a single argument (`bundle`) and + the old module format will error. + + +Zoe Contract Facet (zcf) changes: +* `zcf.reallocate` no longer accepts a third argument `sparseKeywords` + and no longer expects the keywords for different offers to be the + same. Within reallocate, the offer safety check compares the user's + proposal to the user's allocation, and the rights conservation check + adds up the amounts by brand to ensure the totals are the same. + Neither of these checks requires that the keywords for the + allocations be the same for different offers. +* `zcf.getCurrentAllocation` and `zcf.getCurrentAllocations` no longer + take sparseKeywords as a parameter. Instead, brandKeywordRecord is + an optional parameter. If omitted, amounts are returned only for + brands for which an allocation currently exists. +* `zcf.getInstanceRecord()` no longer takes a parameter +* `brandKeywordRecord` is added as a property of `instanceRecord` + alongside `issuerKeywordRecord`. `brandKeywordRecord` is an object + with keyword keys and brand values. +* `zcf.getAmountMaths` has been subsumed by `zcf.getAmountMath` which + takes a single brand parameter. +* `zcf.getBrandForIssuer` has been added. It synchronously returns the + brand for a given issuer already known to Zoe. +* `zoe.getInstance`, which was deprecated earlier, has been removed. +* `cancelObj` and `cancelObj.cancel()`, which were deprecated earlier, + have been removed. + +Changes for Zoe contract developers: +* Zoe contracts are now expected to return only an invite as the + result of `makeContract`. If the contracts want to have a + `publicAPI`, they can do so through `zcf.initPublicAPI`. +* Contracts can allow different offers to use different keywords for + the same issuers. For example, publicAuction, the second price + auction contract, uses 'Asset' and 'Ask' + for the seller keywords and 'Asset' and 'Bid' for the buyer + keywords. + + +Built-in Zoe contract changes: +* We added more comments to the start of the built-in Zoe contracts. +* The operaTickets contract has been split into two contracts: a + generic `sellItems` contract that sells fungible or nonfungible + items at a set price for money, and a generic `mintAndSellNFT` + contract that mints NFT tokens and then immediately creates a new + `sellItems` instance to sell them. The original operaTicket tests + are able to use these contracts. +* The `getCurrentPrice` helper in `bondingCurves.js` has been renamed + to `getInputPrice` and now only returns the `outputExtent`. +* A new built-in contract was added: barter-exchange.js. Barter + Exchange takes offers for trading arbitrary goods for one another. +* Autoswap now has different keywords for different actions that can + be taken. For example, a swap should have the keywords 'In' and + 'Out'. +* Multipool Autoswap has new keywords for different actions that can + be taken as well. For example, adding liquidity has the keywords: + 'SecondaryToken' and 'CentralToken' and returns a payout with + keyword `Liquidity`. +* In Public Auction, the seller keywords are 'Asset' and 'Ask' and the + buyer keywords are 'Asset' and 'Bid'. + + +ZoeHelpers changes: + +Some helpers were removed, and others were added. The built-in Zoe contracts were rewritten to + take advantage of these new helpers. +* `satisfies` was added. It checks whether an allocation would satisfy + a single offer's wants if that was the allocation passed to + `reallocate`. +* `isOfferSafe` was added. It checks whether an + allocation for a particular offer would satisfy offer safety. Any + allocation that returns true under `satisfy` will also return true + under `isOfferSafe`. (`isOfferSafe` is equivalent of `satisfies` || + gives a refund). +* `trade` was added. `Trade` performs a trade between + two offers given a declarative description of what each side loses + and gains. +* `Swap` remains but has slightly different behavior: any + surplus in a trade remains with the original offer +* `canTradeWith` + was removed and subsumed by `satisfies`. +* `inviteAnOffer` was already + deprecated and was removed. +* `assertNatMathHelpers` was added, which + checks whether a particular keyword is associated with an issuer + with natMathHelpers. + +ERTP changes: +* `purse.deposit()` now returns the amount of the deposit, rather than + the purse's new balance. +* A deposit-only facet was added to purses, and can be created by + calling `makeDepositFacet` on any purse. + ## Release v0.6.0 (1-May-2020) * We added `completeObj` with the method `complete` to what is given diff --git a/packages/zoe/package.json b/packages/zoe/package.json index db300bb1f91..178a8765b97 100644 --- a/packages/zoe/package.json +++ b/packages/zoe/package.json @@ -42,6 +42,7 @@ "@agoric/notifier": "^0.1.2", "@agoric/produce-promise": "^0.1.2", "@agoric/same-structure": "^0.0.7", + "@agoric/store": "^0.1.2", "@agoric/transform-metering": "^1.2.5", "@agoric/weak-store": "^0.0.7" }, diff --git a/packages/zoe/src/cleanProposal.js b/packages/zoe/src/cleanProposal.js index 4dd4906d292..3b9a8871234 100644 --- a/packages/zoe/src/cleanProposal.js +++ b/packages/zoe/src/cleanProposal.js @@ -6,7 +6,7 @@ import { arrayToObj, assertSubset } from './objArrayConversion'; // We adopt simple requirements on keywords so that they do not accidentally // conflict with existing property names. -// We require keywords to be strings, ascii identifiers beggining with an +// We require keywords to be strings, ascii identifiers beginning with an // upper case letter, and distinct from the stringified form of any number. // // Of numbers, only NaN and Infinity, when stringified to a property name, @@ -50,21 +50,21 @@ export const getKeywords = keywordRecord => harden(Object.getOwnPropertyNames(keywordRecord)); export const coerceAmountKeywordRecord = ( - amountMathKeywordRecord, - allKeywords, + getAmountMath, allegedAmountKeywordRecord, ) => { - const sparseKeywords = cleanKeys(allKeywords, allegedAmountKeywordRecord); - // Check that each value can be coerced using the amountMath indexed - // by keyword. `AmountMath.coerce` throws if coercion fails. - const coercedAmounts = sparseKeywords.map(keyword => - amountMathKeywordRecord[keyword].coerce( - allegedAmountKeywordRecord[keyword], - ), + const keywords = getKeywords(allegedAmountKeywordRecord); + keywords.forEach(assertKeywordName); + + const amounts = Object.values(allegedAmountKeywordRecord); + // Check that each value can be coerced using the amountMath + // indicated by brand. `AmountMath.coerce` throws if coercion fails. + const coercedAmounts = amounts.map(amount => + getAmountMath(amount.brand).coerce(amount), ); // Recreate the amountKeywordRecord with coercedAmounts. - return arrayToObj(coercedAmounts, sparseKeywords); + return arrayToObj(coercedAmounts, keywords); }; export const cleanKeywords = keywordRecord => { @@ -95,11 +95,7 @@ export const cleanKeywords = keywordRecord => { // `exit`, if present, must be a record of one of the following forms: // `{ waived: null }` `{ onDemand: null }` `{ afterDeadline: { timer // :Timer, deadline :Number } } -export const cleanProposal = ( - issuerKeywordRecord, - amountMathKeywordRecord, - proposal, -) => { +export const cleanProposal = (getAmountMath, proposal) => { const rootKeysAllowed = ['want', 'give', 'exit']; mustBeComparable(proposal); assertKeysAllowed(rootKeysAllowed, proposal); @@ -108,9 +104,8 @@ export const cleanProposal = ( let { want = harden({}), give = harden({}) } = proposal; const { exit = harden({ onDemand: null }) } = proposal; - const allKeywords = getKeywords(issuerKeywordRecord); - want = coerceAmountKeywordRecord(amountMathKeywordRecord, allKeywords, want); - give = coerceAmountKeywordRecord(amountMathKeywordRecord, allKeywords, give); + want = coerceAmountKeywordRecord(getAmountMath, want); + give = coerceAmountKeywordRecord(getAmountMath, give); // Check exit assert( diff --git a/packages/zoe/src/contractSupport/auctions.js b/packages/zoe/src/contractSupport/auctions.js index 7aa46f87f36..a30c144cc4b 100644 --- a/packages/zoe/src/contractSupport/auctions.js +++ b/packages/zoe/src/contractSupport/auctions.js @@ -26,21 +26,21 @@ export const secondPriceLogic = (bidAmountMath, bidOfferHandles, bids) => { }; export const closeAuction = ( - zoe, + zcf, { auctionLogicFn, sellerOfferHandle, allBidHandles }, ) => { - const { Bid: bidAmountMath, Asset: assetAmountMath } = zoe.getAmountMaths( - harden(['Bid', 'Asset']), - ); + const { brandKeywordRecord } = zcf.getInstanceRecord(); + const bidAmountMath = zcf.getAmountMath(brandKeywordRecord.Ask); + const assetAmountMath = zcf.getAmountMath(brandKeywordRecord.Asset); // Filter out any inactive bids - const { active: activeBidHandles } = zoe.getOfferStatuses( + const { active: activeBidHandles } = zcf.getOfferStatuses( harden(allBidHandles), ); const getBids = amountsKeywordRecord => amountsKeywordRecord.Bid; - const bids = zoe.getCurrentAllocations(activeBidHandles).map(getBids); - const assetAmount = zoe.getOffer(sellerOfferHandle).proposal.give.Asset; + const bids = zcf.getCurrentAllocations(activeBidHandles).map(getBids); + const assetAmount = zcf.getOffer(sellerOfferHandle).proposal.give.Asset; const { winnerOfferHandle, winnerBid, price } = auctionLogicFn( bidAmountMath, @@ -52,15 +52,15 @@ export const closeAuction = ( // price paid. const winnerRefund = bidAmountMath.subtract(winnerBid, price); - const newSellerAmounts = { Asset: assetAmountMath.getEmpty(), Bid: price }; + const newSellerAmounts = { Asset: assetAmountMath.getEmpty(), Ask: price }; const newWinnerAmounts = { Asset: assetAmount, Bid: winnerRefund }; // Everyone else gets a refund so their extents remain the // same. - zoe.reallocate( + zcf.reallocate( harden([sellerOfferHandle, winnerOfferHandle]), harden([newSellerAmounts, newWinnerAmounts]), ); const allOfferHandles = harden([sellerOfferHandle, ...activeBidHandles]); - zoe.complete(allOfferHandles); + zcf.complete(allOfferHandles); }; diff --git a/packages/zoe/src/contractSupport/bondingCurves.js b/packages/zoe/src/contractSupport/bondingCurves.js index bc4210855b0..88810b34ead 100644 --- a/packages/zoe/src/contractSupport/bondingCurves.js +++ b/packages/zoe/src/contractSupport/bondingCurves.js @@ -1,5 +1,3 @@ -import harden from '@agoric/harden'; - import { assert, details } from '@agoric/assert'; import { natSafeMath } from './safeMath'; @@ -13,17 +11,19 @@ const { add, subtract, multiply, floorDivide } = natSafeMath; * is valid, getting the current price for an asset on user * request, and to do the actual reallocation after an offer has * been made. - * @param {extent} inputExtent - the extent of the assets sent in - * to be swapped - * @param {extent} inputReserve - the extent in the liquidity + * @param {Object} params + * @param {number} params.inputExtent - the extent of the asset sent + * in to be swapped + * @param {number} params.inputReserve - the extent in the liquidity * pool of the kind of asset sent in - * @param {extent} outputReserve - the extent in the liquidity + * @param {number} params.outputReserve - the extent in the liquidity * pool of the kind of asset to be sent out - * @param {number} feeBasisPoints=30 - the fee taken in + * @param {number} params.feeBasisPoints=30 - the fee taken in * basis points. The default is 0.3% or 30 basis points. The fee is taken from * inputExtent + * @returns {number} outputExtent - the current price, in extent form */ -export const getCurrentPrice = ({ +export const getInputPrice = ({ inputExtent, inputReserve, outputReserve, @@ -35,9 +35,7 @@ export const getCurrentPrice = ({ const denominator = add(multiply(inputReserve, 10000), inputWithFee); const outputExtent = floorDivide(numerator, denominator); - const newOutputReserve = subtract(outputReserve, outputExtent); - const newInputReserve = add(inputReserve, inputExtent); - return harden({ outputExtent, newInputReserve, newOutputReserve }); + return outputExtent; }; function assertDefined(label, value) { diff --git a/packages/zoe/src/contractSupport/index.js b/packages/zoe/src/contractSupport/index.js index 63f6022bcb4..dc7dc977f96 100644 --- a/packages/zoe/src/contractSupport/index.js +++ b/packages/zoe/src/contractSupport/index.js @@ -1,7 +1,7 @@ export { secondPriceLogic, closeAuction } from './auctions'; export { - getCurrentPrice, + getInputPrice, calcLiqExtentToMint, calcExtentToRemove, } from './bondingCurves'; diff --git a/packages/zoe/src/contractSupport/zoeHelpers.js b/packages/zoe/src/contractSupport/zoeHelpers.js index 6f7417098ef..ca56e026448 100644 --- a/packages/zoe/src/contractSupport/zoeHelpers.js +++ b/packages/zoe/src/contractSupport/zoeHelpers.js @@ -2,6 +2,7 @@ import harden from '@agoric/harden'; import { assert, details } from '@agoric/assert'; import { sameStructure } from '@agoric/same-structure'; import { HandledPromise } from '@agoric/eventual-send'; +import { satisfiesWant, isOfferSafe } from '../offerSafety'; /** * @typedef {import('../zoe').OfferHandle} OfferHandle @@ -9,6 +10,10 @@ import { HandledPromise } from '@agoric/eventual-send'; * @typedef {import('../zoe').OfferHook} OfferHook * @typedef {import('../zoe').CustomProperties} CustomProperties * @typedef {import('../zoe').ContractFacet} ContractFacet + * @typedef {import('../zoe').Keyword} Keyword + * @typedef {import('../zoe').AmountKeywordRecord} AmountKeywordRecord + * @typedef {import('../zoe').Amount} Amount + * @typedef {import('../zoe').Payment} Payment */ export const defaultRejectMsg = `The offer was invalid. Please check your refund.`; @@ -56,6 +61,82 @@ export const makeZoeHelpers = (zcf) => { return sameStructure(getKeysSorted(actual), getKeysSorted(expected)); }; + /** + * Given toGains (an AmountKeywordRecord), and allocations (a pair, + * 'to' and 'from', of AmountKeywordRecords), all the entries in + * toGains will be added to 'to'. If fromLosses is defined, all the + * entries in fromLosses are subtracted from 'from'. (If fromLosses + * is not defined, toGains is subtracted from 'from'.) + * + * @param {FromToAllocations} allocations - the 'to' and 'from' + * allocations + * @param {AmountKeywordRecord} toGains - what should be gained in + * the 'to' allocation + * @param {AmountKeywordRecord} fromLosses - what should be lost in + * the 'from' allocation. If not defined, fromLosses is equal to + * toGains. Note that the total amounts should always be equal; it + * is the keywords that might be different. + * @returns {FromToAllocations} allocations - new allocations + * + * @typedef FromToAllocations + * @property {AmountKeywordRecord} from + * @property {AmountKeywordRecord} to + */ + const calcNewAllocations = (allocations, toGains, fromLosses = undefined) => { + if (fromLosses === undefined) { + fromLosses = toGains; + } + + const subtract = (amount, amountToSubtract) => { + const { brand } = amount; + const amountMath = zcf.getAmountMath(brand); + if (amountToSubtract !== undefined) { + return amountMath.subtract(amount, amountToSubtract); + } + return amount; + }; + + const add = (amount, amountToAdd) => { + if (amount && amountToAdd) { + const { brand } = amount; + const amountMath = zcf.getAmountMath(brand); + return amountMath.add(amount, amountToAdd); + } + return amount || amountToAdd; + }; + + const newFromAllocation = Object.fromEntries( + Object.entries(allocations.from).map(([keyword, allocAmount]) => { + return [keyword, subtract(allocAmount, fromLosses[keyword])]; + }), + ); + + const allToKeywords = [ + ...Object.keys(toGains), + ...Object.keys(allocations.to), + ]; + + const newToAllocation = Object.fromEntries( + allToKeywords.map(keyword => [ + keyword, + add(allocations.to[keyword], toGains[keyword]), + ]), + ); + + return harden({ + from: newFromAllocation, + to: newToAllocation, + }); + }; + + const mergeAllocations = (currentAllocation, allocation) => { + const newAllocation = { + ...currentAllocation, + ...allocation, + }; + return newAllocation; + }; + const helpers = harden({ getKeys, assertKeywords: expected => { @@ -88,38 +169,114 @@ export const makeZoeHelpers = (zcf) => { getActiveOffers: handles => zcf.getOffers(zcf.getOfferStatuses(handles).active), rejectOffer, + /** - * Compare two proposals for compatibility. This returns true - * if the left offer would accept whatever the right offer is offering, - * and vice versa. - * - * @param {OfferHandle} leftOfferHandle - * @param {OfferHandle} rightOfferHandle - * @returns boolean + * Check whether an update to currentAllocation satisfies + * proposal.want. Note that this is half of the offer safety + * check; whether the allocation constitutes a refund is not + * checked. Allocation is merged with currentAllocation + * (allocations' values prevailing if the keywords are the same) + * to produce the newAllocation. + * @param {OfferHandle} offerHandle + * @param {allocation} amountKeywordRecord + * @returns {boolean} + */ + satisfies: (offerHandle, allocation) => { + const currentAllocation = zcf.getCurrentAllocation(offerHandle); + const newAllocation = mergeAllocations(currentAllocation, allocation); + const { proposal } = zcf.getOffer(offerHandle); + return satisfiesWant(zcf.getAmountMath, proposal, newAllocation); + }, + + /** + * Check whether an update to currentAllocation satisfies offer + * safety. Note that this is the equivalent of `satisfiesWant` || + * `satisfiesGive`. Allocation is merged with currentAllocation + * (allocations' values prevailing if the keywords are the same) + * to produce the newAllocation. + + * @param {OfferHandle} offerHandle + * @param {AmountKeywordRecord} allocation + * @returns {boolean} + */ + isOfferSafe: (offerHandle, allocation) => { + const currentAllocation = zcf.getCurrentAllocation(offerHandle); + const newAllocation = mergeAllocations(currentAllocation, allocation); + const { proposal } = zcf.getOffer(offerHandle); + return isOfferSafe(zcf.getAmountMath, proposal, newAllocation); + }, + + /** + * Trade between left and right so that left and right end up with + * the declared gains. + * @param {offerHandleGainsLossesRecord} keepLeft + * @param {offerHandleGainsLossesRecord} tryRight + * @returns {undefined | Error} * + * @typedef {object} offerHandleGainsLossesRecord + * @property {OfferHandle} offerHandle + * @property {AmountKeywordRecord} gains - what the offer will + * gain as a result of this trade + * @property {AmountKeywordRecord=} losses - what the offer will + * give up as a result of this trade. Losses is optional, but can + * only be omitted if the keywords for both offers are the same. + * If losses is not defined, the gains of the other offer is + * subtracted. */ - canTradeWith: (leftOfferHandle, rightOfferHandle) => { - const { issuerKeywordRecord } = zcf.getInstanceRecord(); - const keywords = getKeys(issuerKeywordRecord); - const amountMaths = zcf.getAmountMaths(keywords); - const { proposal: left } = zcf.getOffer(leftOfferHandle); - const { proposal: right } = zcf.getOffer(rightOfferHandle); - const satisfied = (want, give) => - keywords.every(keyword => { - if (want[keyword]) { - return amountMaths[keyword].isGTE(give[keyword], want[keyword]); - } - return true; - }); - return ( - satisfied(left.want, right.give) && satisfied(right.want, left.give) + trade: (keepLeft, tryRight) => { + assert( + keepLeft.offerHandle !== tryRight.offerHandle, + details`an offer cannot trade with itself`, ); + let leftAllocation = zcf.getCurrentAllocation(keepLeft.offerHandle); + let rightAllocation = zcf.getCurrentAllocation(tryRight.offerHandle); + + try { + // for all the keywords and amounts in leftGains, transfer from + // right to left + ({ from: rightAllocation, to: leftAllocation } = calcNewAllocations( + { from: rightAllocation, to: leftAllocation }, + keepLeft.gains, + tryRight.losses, + )); + // For all the keywords and amounts in rightGains, transfer from + // left to right + ({ from: leftAllocation, to: rightAllocation } = calcNewAllocations( + { from: leftAllocation, to: rightAllocation }, + tryRight.gains, + keepLeft.losses, + )); + } catch (err) { + return rejectOffer(tryRight.offerHandle); + } + + // Check whether reallocate would error before calling. If + // it would error, reject the right offer and return. + const offerSafeForLeft = helpers.isOfferSafe( + keepLeft.offerHandle, + leftAllocation, + ); + const offerSafeForRight = helpers.isOfferSafe( + tryRight.offerHandle, + rightAllocation, + ); + if (!(offerSafeForLeft && offerSafeForRight)) { + return rejectOffer(tryRight.offerHandle); + } + zcf.reallocate( + [keepLeft.offerHandle, tryRight.offerHandle], + [leftAllocation, rightAllocation], + ); + return undefined; }, + /** * If the two handles can trade, then swap their compatible assets, * marking both offers as complete. * - * TODO: The surplus is dispatched according to some policy TBD. + * The surplus remains with the original offer. For example if + * offer A gives 5 moola and offer B only wants 3 moola, offer A + * retains 2 moola. * * If the keep offer is no longer active (it was already completed), the try * offer will be rejected with a message (provided by 'keepHandleInactiveMsg'). @@ -141,15 +298,19 @@ export const makeZoeHelpers = (zcf) => { if (!zcf.isOfferActive(keepHandle)) { throw helpers.rejectOffer(tryHandle, keepHandleInactiveMsg); } - if (!helpers.canTradeWith(keepHandle, tryHandle)) { - throw helpers.rejectOffer(tryHandle); - } - const keepAmounts = zcf.getCurrentAllocation(keepHandle); - const tryAmounts = zcf.getCurrentAllocation(tryHandle); - // reallocate by switching the amount - const handles = harden([keepHandle, tryHandle]); - zcf.reallocate(handles, harden([tryAmounts, keepAmounts])); - zcf.complete(handles); + + helpers.trade( + { + offerHandle: keepHandle, + gains: zcf.getOffer(keepHandle).proposal.want, + }, + { + offerHandle: tryHandle, + gains: zcf.getOffer(tryHandle).proposal.want, + }, + ); + + zcf.complete([keepHandle, tryHandle]); return defaultAcceptanceMsg; }, @@ -176,20 +337,6 @@ export const makeZoeHelpers = (zcf) => { return offerHook(offerHandle); }, - // TODO DEPRECATED `inviteAnOffer` is deprecated legacy. Remove when we can. - inviteAnOffer: ({ - offerHook = () => {}, - inviteDesc, - customProperties = undefined, - expected = undefined, - }) => { - return zcf.makeInvitation( - expected ? helpers.checkHook(offerHook, expected) : offerHook, - inviteDesc || customProperties.inviteDesc, - customProperties && harden({ customProperties }), - ); - }, - /** * Return a Promise for an OfferHandle. * @@ -199,7 +346,6 @@ export const makeZoeHelpers = (zcf) => { * to manage internal escrowed assets. * * @returns {Promise} - * */ makeEmptyOffer: () => new HandledPromise(resolve => { @@ -209,6 +355,7 @@ export const makeZoeHelpers = (zcf) => { ); zoeService.offer(invite); }), + /** * Escrow a payment with Zoe and reallocate the amount of the * payment to a recipient. @@ -217,17 +364,14 @@ export const makeZoeHelpers = (zcf) => { * @param {Amount} obj.amount * @param {Payment} obj.payment * @param {String} obj.keyword - * @param {Handle} obj.recipientHandle + * @param {OfferHandle} obj.recipientHandle * @returns {Promise} - * */ escrowAndAllocateTo: ({ amount, payment, keyword, recipientHandle }) => { // We will create a temporary offer to be able to escrow our payment // with Zoe. let tempHandle; - const amountMath = zcf.getAmountMaths(harden([keyword]))[keyword]; - // We need to make an invite and store the offerHandle of that // invite for future use. const contractSelfInvite = zcf.makeInvitation( @@ -236,8 +380,9 @@ export const makeZoeHelpers = (zcf) => { ); // To escrow the payment, we must get the Zoe Service facet and // make an offer - const proposal = harden({ give: { [keyword]: amount } }); - const payments = harden({ [keyword]: payment }); + const proposal = harden({ give: { Temp: amount } }); + const payments = harden({ Temp: payment }); + return zcf .getZoeService() .offer(contractSelfInvite, proposal, payments) @@ -246,26 +391,17 @@ export const makeZoeHelpers = (zcf) => { // payment but nothing else. The recipient offer may have any // allocation, so we can't assume the allocation is currently empty for this // keyword. - const [recipientAlloc, tempAlloc] = zcf.getCurrentAllocations( - harden([recipientHandle, tempHandle]), - harden([keyword]), - ); - // Add the tempAlloc for the keyword to the recipientAlloc. - recipientAlloc[keyword] = amountMath.add( - recipientAlloc[keyword], - tempAlloc[keyword], - ); - - // Set the temporary offer allocation to empty. - tempAlloc[keyword] = amountMath.getEmpty(); - - // Actually reallocate the amounts. Note that only the amounts - // for `keyword` are reallocated. - zcf.reallocate( - harden([tempHandle, recipientHandle]), - harden([tempAlloc, recipientAlloc]), - harden([keyword]), + helpers.trade( + { + offerHandle: tempHandle, + gains: {}, + losses: { Temp: amount }, + }, + { + offerHandle: recipientHandle, + gains: { [keyword]: amount }, + }, ); // Complete the temporary offerHandle @@ -275,6 +411,17 @@ export const makeZoeHelpers = (zcf) => { // offer is allocated the value of the payment. }); }, + /* + * Given a brand, assert that the mathHelpers for that issuer + * are 'nat' mathHelpers + */ + assertNatMathHelpers: brand => { + const amountMath = zcf.getAmountMath(brand); + assert( + amountMath.getMathHelpersName() === 'nat', + details`issuer must have natMathHelpers`, + ); + }, }); return helpers; }; diff --git a/packages/zoe/src/contracts/atomicSwap.js b/packages/zoe/src/contracts/atomicSwap.js index 0bec31a38c8..a2043fb4589 100644 --- a/packages/zoe/src/contracts/atomicSwap.js +++ b/packages/zoe/src/contracts/atomicSwap.js @@ -5,41 +5,49 @@ import harden from '@agoric/harden'; // Eventually will be importable from '@agoric/zoe-contract-support' import { makeZoeHelpers } from '../contractSupport'; -/** @typedef {import('../zoe').ContractFacet} ContractFacet */ - -// zcf is the Zoe Contract Facet, i.e. the contract-facing API of Zoe -export const makeContract = harden( - /** @param {ContractFacet} zcf */ zcf => { - const { swap, assertKeywords, checkHook } = makeZoeHelpers(zcf); - assertKeywords(harden(['Asset', 'Price'])); - - const makeMatchingInvite = firstOfferHandle => { - const { - proposal: { want, give }, - } = zcf.getOffer(firstOfferHandle); - - return zcf.makeInvitation( - offerHandle => swap(firstOfferHandle, offerHandle), - 'matchOffer', - harden({ - customProperties: { - asset: give.Asset, - price: want.Price, - }, - }), - ); - }; - - const firstOfferExpected = harden({ - give: { Asset: null }, - want: { Price: null }, - }); - - return harden({ - invite: zcf.makeInvitation( - checkHook(makeMatchingInvite, firstOfferExpected), - 'firstOffer', - ), - }); - }, -); +/** + * Trade one item for another. + * + * The initial offer is { give: { Asset: A }, want: { Price: B } }. + * The outcome from the first offer is an invitation for the second party, + * who should offer { give: { Price: B }, want: { Asset: A } }, with a want + * amount no greater than the original's give, and a give amount at least as + * large as the original's want. + * + * @typedef {import('../zoe').ContractFacet} ContractFacet + * @param {ContractFacet} zcf + */ +const makeContract = zcf => { + const { swap, assertKeywords, checkHook } = makeZoeHelpers(zcf); + assertKeywords(harden(['Asset', 'Price'])); + + const makeMatchingInvite = firstOfferHandle => { + const { + proposal: { want, give }, + } = zcf.getOffer(firstOfferHandle); + + return zcf.makeInvitation( + offerHandle => swap(firstOfferHandle, offerHandle), + 'matchOffer', + harden({ + customProperties: { + asset: give.Asset, + price: want.Price, + }, + }), + ); + }; + + const firstOfferExpected = harden({ + give: { Asset: null }, + want: { Price: null }, + }); + + return zcf.makeInvitation( + checkHook(makeMatchingInvite, firstOfferExpected), + 'firstOffer', + ); +}; + +harden(makeContract); +export { makeContract }; diff --git a/packages/zoe/src/contracts/automaticRefund.js b/packages/zoe/src/contracts/automaticRefund.js index ce62558ef45..154ec0ce435 100644 --- a/packages/zoe/src/contracts/automaticRefund.js +++ b/packages/zoe/src/contracts/automaticRefund.js @@ -1,8 +1,6 @@ // @ts-check import harden from '@agoric/harden'; -/** @typedef {import('../zoe').ContractFacet} ContractFacet */ - /** * This is a very trivial contract to explain and test Zoe. * AutomaticRefund just gives you back what you put in. @@ -11,26 +9,33 @@ import harden from '@agoric/harden'; * contracts will use these same steps, but they will have more * sophisticated logic and interfaces. * - * @type {import('@agoric/zoe').MakeContract} + * Since the contract doesn't attempt any reallocation, the offer can contain + * anything in `give` and `want`. The amount in `give` will be returned, and + * `want` will be ignored. + * + * @typedef {import('../zoe').ContractFacet} ContractFacet + * @param {ContractFacet} zcf */ -export const makeContract = harden( - /** @param {ContractFacet} zcf */ zcf => { - let offersCount = 0; +const makeContract = zcf => { + let offersCount = 0; + + const refundOfferHook = offerHandle => { + offersCount += 1; + zcf.complete(harden([offerHandle])); + return `The offer was accepted`; + }; + const makeRefundInvite = () => + zcf.makeInvitation(refundOfferHook, 'getRefund'); + + zcf.initPublicAPI( + harden({ + getOffersCount: () => offersCount, + makeInvite: makeRefundInvite, + }), + ); - const refundOfferHook = offerHandle => { - offersCount += 1; - zcf.complete(harden([offerHandle])); - return `The offer was accepted`; - }; - const makeRefundInvite = () => - zcf.makeInvitation(refundOfferHook, 'getRefund'); + return makeRefundInvite(); +}; - return harden({ - invite: makeRefundInvite(), - publicAPI: { - getOffersCount: () => offersCount, - makeInvite: makeRefundInvite, - }, - }); - }, -); +harden(makeContract); +export { makeContract }; diff --git a/packages/zoe/src/contracts/autoswap.js b/packages/zoe/src/contracts/autoswap.js index 36400064720..e0bf09686b3 100644 --- a/packages/zoe/src/contracts/autoswap.js +++ b/packages/zoe/src/contracts/autoswap.js @@ -2,319 +2,267 @@ import harden from '@agoric/harden'; import produceIssuer from '@agoric/ertp'; -import { assert, details } from '@agoric/assert'; // Eventually will be importable from '@agoric/zoe-contract-support' import { - getCurrentPrice, + getInputPrice, calcLiqExtentToMint, calcExtentToRemove, makeZoeHelpers, } from '../contractSupport'; -// Autoswap is a rewrite of Uniswap. Please see the documentation for -// more https://agoric.com/documentation/zoe/guide/contracts/autoswap.html - /** + * Autoswap is a rewrite of Uniswap. Please see the documentation for + * more https://agoric.com/documentation/zoe/guide/contracts/autoswap.html + * + * When the contract is instantiated, the two tokens are specified in the + * issuerKeywordRecord. The party that calls makeInstance gets an invitation + * to add liquidity. The same invitation is available by calling + * `publicAPI.makeAddLiquidityInvite()`. Separate invitations are available for + * adding and removing liquidity, and for doing a swap. Other API operations + * support monitoring the price and the size of the liquidity pool. + * * @typedef {import('../zoe').ContractFacet} ContractFacet + * @param {ContractFacet} zcf */ - -// zcf is the Zoe Contract Facet, i.e. the contract-facing API of Zoe -export const makeContract = harden( - /** @param {ContractFacet} zcf */ zcf => { - // Create the liquidity mint and issuer. - const { mint: liquidityMint, issuer: liquidityIssuer } = produceIssuer( - 'liquidity', +const makeContract = zcf => { + // Create the liquidity mint and issuer. + const { + mint: liquidityMint, + issuer: liquidityIssuer, + amountMath: liquidityAmountMath, + } = produceIssuer('liquidity'); + + let liqTokenSupply = 0; + + const { + makeEmptyOffer, + checkHook, + escrowAndAllocateTo, + assertNatMathHelpers, + trade, + } = makeZoeHelpers(zcf); + + return zcf.addNewIssuer(liquidityIssuer, 'Liquidity').then(() => { + const { brandKeywordRecord } = zcf.getInstanceRecord(); + Object.values(brandKeywordRecord).forEach(brand => + assertNatMathHelpers(brand), ); - - let liqTokenSupply = 0; - - const { - checkIfProposal, - rejectOffer, - makeEmptyOffer, - checkHook, - escrowAndAllocateTo, - } = makeZoeHelpers(zcf); - - return zcf.addNewIssuer(liquidityIssuer, 'Liquidity').then(() => { - const amountMaths = zcf.getAmountMaths( - harden(['TokenA', 'TokenB', 'Liquidity']), - ); - Object.values(amountMaths).forEach(amountMath => - assert( - amountMath.getMathHelpersName() === 'nat', - details`issuers must have natMathHelpers`, - ), - ); - - return makeEmptyOffer().then(poolHandle => { - const getPoolAllocation = () => zcf.getCurrentAllocation(poolHandle); - - const swap = (offerHandle, giveKeyword, wantKeyword) => { - const { proposal } = zcf.getOffer(offerHandle); - if (proposal.want.Liquidity !== undefined) { - rejectOffer( - offerHandle, - `A Liquidity amount should not be present in a swap`, - ); - } - - const poolAllocation = getPoolAllocation(); - const { - outputExtent, - newInputReserve, - newOutputReserve, - } = getCurrentPrice( - harden({ - inputExtent: proposal.give[giveKeyword].extent, - inputReserve: poolAllocation[giveKeyword].extent, - outputReserve: poolAllocation[wantKeyword].extent, - }), - ); - const amountOut = amountMaths[wantKeyword].make(outputExtent); - const wantedAmount = proposal.want[wantKeyword]; - const satisfiesWantedAmounts = () => - amountMaths[wantKeyword].isGTE(amountOut, wantedAmount); - if (!satisfiesWantedAmounts()) { - throw rejectOffer(offerHandle); - } - - const newUserAmounts = { - Liquidity: amountMaths.Liquidity.getEmpty(), - }; - newUserAmounts[giveKeyword] = amountMaths[giveKeyword].getEmpty(); - newUserAmounts[wantKeyword] = amountOut; - - const newPoolAmounts = { Liquidity: poolAllocation.Liquidity }; - newPoolAmounts[giveKeyword] = amountMaths[giveKeyword].make( - newInputReserve, - ); - newPoolAmounts[wantKeyword] = amountMaths[wantKeyword].make( - newOutputReserve, + const getPoolKeyword = brandToMatch => { + const entries = Object.entries(brandKeywordRecord); + for (const [keyword, brand] of entries) { + if (brand === brandToMatch) { + return keyword; + } + } + throw new Error('getPoolKeyword: brand not found'); + }; + + return makeEmptyOffer().then(poolHandle => { + const getPoolAmount = brand => { + const keyword = getPoolKeyword(brand); + return zcf.getCurrentAllocation(poolHandle)[keyword]; + }; + + const swapHook = offerHandle => { + const { + proposal: { + give: { In: amountIn }, + want: { Out: wantedAmountOut }, + }, + } = zcf.getOffer(offerHandle); + const outputExtent = getInputPrice( + harden({ + inputExtent: amountIn.extent, + inputReserve: getPoolAmount(amountIn.brand).extent, + outputReserve: getPoolAmount(wantedAmountOut.brand).extent, + }), + ); + const amountOut = zcf + .getAmountMath(wantedAmountOut.brand) + .make(outputExtent); + + trade( + { + offerHandle: poolHandle, + gains: { + [getPoolKeyword(amountIn.brand)]: amountIn, + }, + losses: { + [getPoolKeyword(amountOut.brand)]: amountOut, + }, + }, + { + offerHandle, + gains: { Out: amountOut }, + losses: { In: amountIn }, + }, + ); + zcf.complete(harden([offerHandle])); + return `Swap successfully completed.`; + }; + + const addLiquidityHook = offerHandle => { + const userAllocation = zcf.getCurrentAllocation(offerHandle); + + // Calculate how many liquidity tokens we should be minting. + // Calculations are based on the extents represented by TokenA. + // If the current supply is zero, start off by just taking the + // extent at TokenA and using it as the extent for the + // liquidity token. + const tokenAPoolAmount = getPoolAmount(userAllocation.TokenA.brand); + const inputReserve = tokenAPoolAmount ? tokenAPoolAmount.extent : 0; + const liquidityExtentOut = calcLiqExtentToMint( + harden({ + liqTokenSupply, + inputExtent: userAllocation.TokenA.extent, + inputReserve, + }), + ); + const liquidityAmountOut = liquidityAmountMath.make(liquidityExtentOut); + const liquidityPaymentP = liquidityMint.mintPayment(liquidityAmountOut); + + return escrowAndAllocateTo({ + amount: liquidityAmountOut, + payment: liquidityPaymentP, + keyword: 'Liquidity', + recipientHandle: offerHandle, + }).then(() => { + liqTokenSupply += liquidityExtentOut; + + trade( + { + offerHandle: poolHandle, + gains: { + TokenA: userAllocation.TokenA, + TokenB: userAllocation.TokenB, + }, + }, + // We've already given the user their liquidity using + // escrowAndAllocateTo + { offerHandle, gains: {} }, ); - zcf.reallocate( - harden([offerHandle, poolHandle]), - harden([newUserAmounts, newPoolAmounts]), - ); zcf.complete(harden([offerHandle])); - return `Swap successfully completed.`; - }; - - const buyASellB = harden({ - give: { TokenB: null }, - want: { TokenA: null }, - }); - - const buyBSellA = harden({ - give: { TokenA: null }, - want: { TokenB: null }, + return 'Added liquidity.'; }); + }; - const swapHook = offerHandle => { - assert( - !checkIfProposal(offerHandle, { give: { Liquidity: null } }), - details`A Liquidity amount should not be present in a swap`, - ); - assert( - !checkIfProposal(offerHandle, { want: { Liquidity: null } }), - details`A Liquidity amount should not be present in a swap`, - ); - if (checkIfProposal(offerHandle, buyASellB)) { - return swap(offerHandle, 'TokenB', 'TokenA'); - /* eslint-disable no-else-return */ - } else if (checkIfProposal(offerHandle, buyBSellA)) { - return swap(offerHandle, 'TokenA', 'TokenB'); - } else { - // Eject because the offer must be invalid - return rejectOffer(offerHandle); - } - }; - - const addLiquidityHook = offerHandle => { - const userAllocation = zcf.getCurrentAllocation(offerHandle); - const poolAllocation = getPoolAllocation(); - - // Calculate how many liquidity tokens we should be minting. - // Calculations are based on the extents represented by TokenA. - // If the current supply is zero, start off by just taking the - // extent at TokenA and using it as the extent for the - // liquidity token. - const liquidityExtentOut = calcLiqExtentToMint( - harden({ - liqTokenSupply, - inputExtent: userAllocation.TokenA.extent, - inputReserve: poolAllocation.TokenA.extent, - }), - ); - - const liquidityAmountOut = amountMaths.Liquidity.make( - liquidityExtentOut, - ); + const removeLiquidityHook = offerHandle => { + const userAllocation = zcf.getCurrentAllocation(offerHandle); + const liquidityExtentIn = userAllocation.Liquidity.extent; - const liquidityPaymentP = liquidityMint.mintPayment( - liquidityAmountOut, - ); - - return escrowAndAllocateTo({ - amount: liquidityAmountOut, - payment: liquidityPaymentP, - keyword: 'Liquidity', - recipientHandle: offerHandle, - }).then(() => { - liqTokenSupply += liquidityExtentOut; - - const add = (key, obj1, obj2) => - amountMaths[key].add(obj1[key], obj2[key]); - - const newPoolAmounts = harden({ - TokenA: add('TokenA', userAllocation, poolAllocation), - TokenB: add('TokenB', userAllocation, poolAllocation), - Liquidity: poolAllocation.Liquidity, - }); - - const newUserAmounts = harden({ - TokenA: amountMaths.TokenA.getEmpty(), - TokenB: amountMaths.TokenB.getEmpty(), - Liquidity: liquidityAmountOut, - }); - - zcf.reallocate( - harden([offerHandle, poolHandle]), - harden([newUserAmounts, newPoolAmounts]), - harden(['TokenA', 'TokenB', 'Liquidity']), - ); - zcf.complete(harden([offerHandle])); - return 'Added liquidity.'; - }); - }; - - const addLiquidityExpected = harden({ - give: { TokenA: null, TokenB: null }, - want: { Liquidity: null }, - }); - - const removeLiquidityHook = offerHandle => { - const userAllocation = zcf.getCurrentAllocation(offerHandle); - const liquidityExtentIn = userAllocation.Liquidity.extent; - - const poolAllocation = getPoolAllocation(); - - const newUserTokenAAmount = amountMaths.TokenA.make( + const newUserTokenAAmount = zcf + .getAmountMath(userAllocation.TokenA.brand) + .make( calcExtentToRemove( harden({ liqTokenSupply, - poolExtent: poolAllocation.TokenA.extent, + poolExtent: getPoolAmount(userAllocation.TokenA.brand).extent, liquidityExtentIn, }), ), ); - const newUserTokenBAmount = amountMaths.TokenB.make( + const newUserTokenBAmount = zcf + .getAmountMath(userAllocation.TokenB.brand) + .make( calcExtentToRemove( harden({ liqTokenSupply, - poolExtent: poolAllocation.TokenB.extent, + poolExtent: getPoolAmount(userAllocation.TokenB.brand).extent, liquidityExtentIn, }), ), ); - const newUserAmounts = harden({ - TokenA: newUserTokenAAmount, - TokenB: newUserTokenBAmount, - Liquidity: amountMaths.Liquidity.getEmpty(), - }); - - const newPoolAmounts = harden({ - TokenA: amountMaths.TokenA.subtract( - poolAllocation.TokenA, - newUserAmounts.TokenA, - ), - TokenB: amountMaths.TokenB.subtract( - poolAllocation.TokenB, - newUserAmounts.TokenB, - ), - Liquidity: amountMaths.Liquidity.add( - poolAllocation.Liquidity, - amountMaths.Liquidity.make(liquidityExtentIn), - ), - }); - - liqTokenSupply -= liquidityExtentIn; + liqTokenSupply -= liquidityExtentIn; - zcf.reallocate( - harden([offerHandle, poolHandle]), - harden([newUserAmounts, newPoolAmounts]), - ); - zcf.complete(harden([offerHandle])); - return 'Liquidity successfully removed.'; - }; - - const removeLiquidityExpected = harden({ - want: { TokenA: null, TokenB: null }, - give: { Liquidity: null }, - }); - - const makeAddLiquidityInvite = () => - zcf.makeInvitation( - checkHook(addLiquidityHook, addLiquidityExpected), - 'autoswap add liquidity', - ); - - return harden({ - invite: makeAddLiquidityInvite(), - publicAPI: { - /** - * `getCurrentPrice` calculates the result of a trade, given a certain amount - * of digital assets in. - * @param {object} amountInObj - the amount of digital - * assets to be sent in, keyed by keyword - */ - getCurrentPrice: amountInObj => { - const inKeywords = Object.getOwnPropertyNames(amountInObj); - assert( - inKeywords.length === 1, - details`argument to 'getCurrentPrice' must have one keyword`, - ); - const [inKeyword] = inKeywords; - assert( - ['TokenA', 'TokenB'].includes(inKeyword), - details`keyword ${inKeyword} was not valid`, - ); - const inputExtent = amountMaths[inKeyword].getExtent( - amountInObj[inKeyword], - ); - const poolAllocation = getPoolAllocation(); - const inputReserve = poolAllocation[inKeyword].extent; - const outKeyword = inKeyword === 'TokenA' ? 'TokenB' : 'TokenA'; - const outputReserve = poolAllocation[outKeyword].extent; - const { outputExtent } = getCurrentPrice( - harden({ - inputExtent, - inputReserve, - outputReserve, - }), - ); - return amountMaths[outKeyword].make(outputExtent); + trade( + { + offerHandle: poolHandle, + gains: { Liquidity: userAllocation.Liquidity }, + }, + { + offerHandle, + gains: { + TokenA: newUserTokenAAmount, + TokenB: newUserTokenBAmount, }, + }, + ); - getLiquidityIssuer: () => liquidityIssuer, + zcf.complete(harden([offerHandle])); + return 'Liquidity successfully removed.'; + }; - getPoolAllocation, + const addLiquidityExpected = harden({ + give: { TokenA: null, TokenB: null }, + want: { Liquidity: null }, + }); - makeSwapInvite: () => zcf.makeInvitation(swapHook, 'autoswap swap'), + const removeLiquidityExpected = harden({ + want: { TokenA: null, TokenB: null }, + give: { Liquidity: null }, + }); - makeAddLiquidityInvite, + const swapExpected = { + want: { Out: null }, + give: { In: null }, + }; + + const makeAddLiquidityInvite = () => + zcf.makeInvitation( + checkHook(addLiquidityHook, addLiquidityExpected), + 'autoswap add liquidity', + ); + + const makeRemoveLiquidityInvite = () => + zcf.makeInvitation( + checkHook(removeLiquidityHook, removeLiquidityExpected), + 'autoswap remove liquidity', + ); + + const makeSwapInvite = () => + zcf.makeInvitation(checkHook(swapHook, swapExpected), 'autoswap swap'); + + /** + * `getCurrentPrice` calculates the result of a trade, given a certain amount + * of digital assets in. + * @typedef {import('../zoe').Amount} Amount + * @param {Amount} amountIn - the amount of digital + * assets to be sent in + */ + const getCurrentPrice = (amountIn, brandOut) => { + const inputReserve = getPoolAmount(amountIn.brand).extent; + const outputReserve = getPoolAmount(brandOut).extent; + const outputExtent = getInputPrice( + harden({ + inputExtent: amountIn.extent, + inputReserve, + outputReserve, + }), + ); + return zcf.getAmountMath(brandOut).make(outputExtent); + }; + + const getPoolAllocation = () => + zcf.getCurrentAllocation(poolHandle, brandKeywordRecord); + + zcf.initPublicAPI( + harden({ + getCurrentPrice, + getLiquidityIssuer: () => liquidityIssuer, + getPoolAllocation, + makeSwapInvite, + makeAddLiquidityInvite, + makeRemoveLiquidityInvite, + }), + ); - makeRemoveLiquidityInvite: () => - zcf.makeInvitation( - checkHook(removeLiquidityHook, removeLiquidityExpected), - 'autoswap remove liquidity', - ), - }, - }); - }); + return makeAddLiquidityInvite(); }); - }, -); + }); +}; + +harden(makeContract); +export { makeContract }; diff --git a/packages/zoe/src/contracts/barterExchange.js b/packages/zoe/src/contracts/barterExchange.js new file mode 100644 index 00000000000..582db57d320 --- /dev/null +++ b/packages/zoe/src/contracts/barterExchange.js @@ -0,0 +1,134 @@ +// @ts-check + +import harden from '@agoric/harden'; +import makeStore from '@agoric/store'; +import { makeZoeHelpers, defaultAcceptanceMsg } from '../contractSupport'; + +/** + * This Barter Exchange accepts offers to trade arbitrary goods for other + * things. It doesn't require registration of Issuers. If two offers satisfy + * each other, it exchanges the specified amounts in each side's want clause. + * + * The Barter Exchange only accepts offers that look like + * { give: { In: amount }, want: { Out: amount} } + * The want amount will be matched, while the give amount is a maximum. Each + * successful trader gets their `want` and may trade with counter-parties who + * specify any amount up to their specified `give`. + * + * @typedef {import('../zoe').ContractFacet} ContractFacet + * @param {ContractFacet} zcf + */ +const makeContract = zcf => { + // bookOrders is a Map of Maps. The first key is the brand of the offer's + // GIVE, and the second key is the brand of its WANT. For each offer, we + // store its handle and the amounts for `give` and `want`. + const bookOrders = makeStore('bookOrders'); + + const { satisfies, trade } = makeZoeHelpers(zcf); + + function lookupBookOrders(brandIn, brandOut) { + if (!bookOrders.has(brandIn)) { + bookOrders.init(brandIn, new Map()); + } + const ordersMap = bookOrders.get(brandIn); + let ordersArray = ordersMap.get(brandOut); + if (!ordersArray) { + ordersArray = []; + ordersMap.set(brandOut, ordersArray); + } + return ordersArray; + } + + function findMatchingTrade(newDetails, orders) { + return orders.find(order => { + return ( + satisfies(newDetails.offerHandle, { Out: order.amountIn }) && + satisfies(order.offerHandle, { Out: newDetails.amountIn }) + ); + }); + } + + function removeFromOrders(offerDetails) { + const orders = lookupBookOrders( + offerDetails.amountIn.brand, + offerDetails.amountOut.brand, + ); + orders.splice(orders.indexOf(offerDetails), 1); + } + + function tradeWithMatchingOffer(offerDetails) { + const orders = lookupBookOrders( + offerDetails.amountOut.brand, + offerDetails.amountIn.brand, + ); + const matchingTrade = findMatchingTrade(offerDetails, orders); + if (matchingTrade) { + // reallocate by giving each side what it wants + trade( + { + offerHandle: matchingTrade.offerHandle, + gains: { + Out: matchingTrade.amountOut, + }, + losses: { + In: offerDetails.amountOut, + }, + }, + { + offerHandle: offerDetails.offerHandle, + gains: { + Out: offerDetails.amountOut, + }, + losses: { + In: matchingTrade.amountOut, + }, + }, + ); + removeFromOrders(matchingTrade); + zcf.complete([offerDetails.offerHandle, matchingTrade.offerHandle]); + + return true; + } + return false; + } + + function addToBook(offerDetails) { + const orders = lookupBookOrders( + offerDetails.amountIn.brand, + offerDetails.amountOut.brand, + ); + orders.push(offerDetails); + } + + function extractOfferDetails(offerHandle) { + const { + give: { In: amountIn }, + want: { Out: amountOut }, + } = zcf.getOffer(offerHandle).proposal; + + return { + offerHandle, + amountIn, + amountOut, + }; + } + const exchangeOfferHook = offerHandle => { + const offerDetails = extractOfferDetails(offerHandle); + + if (!tradeWithMatchingOffer(offerDetails)) { + addToBook(offerDetails); + } + + return defaultAcceptanceMsg; + }; + + const makeExchangeInvite = () => + zcf.makeInvitation(exchangeOfferHook, 'exchange'); + + zcf.initPublicAPI(harden({ makeInvite: makeExchangeInvite })); + + return makeExchangeInvite(); +}; + +harden(makeContract); +export { makeContract }; diff --git a/packages/zoe/src/contracts/coveredCall.js b/packages/zoe/src/contracts/coveredCall.js index 82ebe65340b..5d85c7909d0 100644 --- a/packages/zoe/src/contracts/coveredCall.js +++ b/packages/zoe/src/contracts/coveredCall.js @@ -7,66 +7,77 @@ import { makeZoeHelpers } from '../contractSupport'; const rejectMsg = `The covered call option is expired.`; -/** @typedef {import('../zoe').ContractFacet} ContractFacet */ +/** + * In a covered call, a digital asset's owner sells a call + * option. A call option is the right to buy the digital asset at a + * pre-determined price, called the strike price. The call option has an expiry + * date, when the contract will be cancelled. + * + * In this contract, the expiry date is the deadline when + * the offer escrowing the underlying assets is cancelled. + * Therefore, the proposal for the underlying assets must have an + * exit record with the key "afterDeadline". + * + * The invite received by the covered call creator is the call option. It has + * this additional information in the invite's extent: + * { expirationDate, timerAuthority, underlyingAsset, strikePrice } + * + * The initial proposal should be: + * { + * give: { UnderlyingAsset: assetAmount }, + * want: { StrikePrice: priceAmount }, + * exit: { afterDeadline: { deadline: time, timer: timer } }, + * } + * The result of the initial offer is { payout, outcome }, where payout will + * eventually resolve to the strikePrice, and outcome is an assayable invitation + * to buy the underlying asset. Since the contract provides assurance that the + * underlying asset is available on the specified terms, the invite itself can + * be traded as a valuable good. + * + * @typedef {import('../zoe').ContractFacet} ContractFacet + * @param {ContractFacet} zcf + */ +const makeContract = zcf => { + const { swap, assertKeywords, checkHook } = makeZoeHelpers(zcf); + assertKeywords(harden(['UnderlyingAsset', 'StrikePrice'])); -// In a covered call, the owner of a digital asset sells a call -// option. A call option is the right to buy the digital asset at a -// certain price, called the strike price. The call option has an expiry -// date, at which point the contract is cancelled. + const makeCallOptionInvite = sellerHandle => { + const { + proposal: { want, give, exit }, + } = zcf.getOffer(sellerHandle); -// In this contract, the expiry date is represented by the deadline at -// which the offer escrowing the underlying assets is cancelled. -// Therefore, the proposal for the underlying assets must have an -// exit record with the key "afterDeadline". - -// The invite that the creator of the covered call receives is the -// call option and has the following additional information in the -// extent of the invite: -// { expirationDate, timerAuthority, underlyingAsset, strikePrice } - -// zcf is the Zoe Contract Facet, i.e. the contract-facing API of Zoe -export const makeContract = harden( - /** @param {ContractFacet} zcf */ zcf => { - const { swap, assertKeywords, checkHook } = makeZoeHelpers(zcf); - assertKeywords(harden(['UnderlyingAsset', 'StrikePrice'])); - - const makeCallOptionInvite = sellerHandle => { - const { - proposal: { want, give, exit }, - } = zcf.getOffer(sellerHandle); + const exerciseOptionHook = offerHandle => + swap(sellerHandle, offerHandle, rejectMsg); + const exerciseOptionExpected = harden({ + give: { StrikePrice: null }, + want: { UnderlyingAsset: null }, + }); - const exerciseOptionHook = offerHandle => - swap(sellerHandle, offerHandle, rejectMsg); - const exerciseOptionExpected = harden({ - give: { StrikePrice: null }, - want: { UnderlyingAsset: null }, - }); + return zcf.makeInvitation( + checkHook(exerciseOptionHook, exerciseOptionExpected), + 'exerciseOption', + harden({ + customProperties: { + expirationDate: exit.afterDeadline.deadline, + timerAuthority: exit.afterDeadline.timer, + underlyingAsset: give.UnderlyingAsset, + strikePrice: want.StrikePrice, + }, + }), + ); + }; - return zcf.makeInvitation( - checkHook(exerciseOptionHook, exerciseOptionExpected), - 'exerciseOption', - harden({ - customProperties: { - expirationDate: exit.afterDeadline.deadline, - timerAuthority: exit.afterDeadline.timer, - underlyingAsset: give.UnderlyingAsset, - strikePrice: want.StrikePrice, - }, - }), - ); - }; + const writeOptionExpected = harden({ + give: { UnderlyingAsset: null }, + want: { StrikePrice: null }, + exit: { afterDeadline: null }, + }); - const writeOptionExpected = harden({ - give: { UnderlyingAsset: null }, - want: { StrikePrice: null }, - exit: { afterDeadline: null }, - }); + return zcf.makeInvitation( + checkHook(makeCallOptionInvite, writeOptionExpected), + 'makeCallOption', + ); +}; - return harden({ - invite: zcf.makeInvitation( - checkHook(makeCallOptionInvite, writeOptionExpected), - 'makeCallOption', - ), - }); - }, -); +harden(makeContract); +export { makeContract }; diff --git a/packages/zoe/src/contracts/mintAndSellNFT.js b/packages/zoe/src/contracts/mintAndSellNFT.js new file mode 100644 index 00000000000..988c74fc13d --- /dev/null +++ b/packages/zoe/src/contracts/mintAndSellNFT.js @@ -0,0 +1,115 @@ +// @ts-check + +import harden from '@agoric/harden'; +import produceIssuer from '@agoric/ertp'; + +/** + * This contract mints non-fungible tokens and creates a selling contract + * instance to sell the tokens in exchange for some sort of money. + * + * makeInstance() returns an invitation that, when exercised, returns a + * ticketMaker with a `.sellTokens()` method. `.sellTokens()` takes a + * specification of what is being sold, such as: + * { + * customExtentProperties: { ...arbitrary }, + * count: 3, + * moneyIssuer: moolaIssuer, + * sellItemsInstallationHandle, + * pricePerItem: moolaAmountMath.make(20), + * } + * The payouts are returned as an offerResult in the `outcome`, and an API that + * allows selling the tickets that were produced. You can reuse the ticket maker + * to mint more tickets (e.g. for a separate show.) + * + * @typedef {import('../zoe').ContractFacet} ContractFacet + * @param {ContractFacet} zcf + */ +const makeContract = zcf => { + const { terms } = zcf.getInstanceRecord(); + const { tokenName = 'token' } = terms; + + // Create the internal token mint + const { issuer, mint, amountMath: tokenAmountMath } = produceIssuer( + tokenName, + 'set', + ); + + const zoeService = zcf.getZoeService(); + + const sellTokens = ({ + customExtentProperties, + count, + moneyIssuer, + sellItemsInstallationHandle, + pricePerItem, + }) => { + const tokenAmount = tokenAmountMath.make( + harden( + Array(count) + // @ts-ignore + .fill() + .map((_, i) => { + const tokenNumber = i + 1; + return { + ...customExtentProperties, + number: tokenNumber, + }; + }), + ), + ); + const tokenPayment = mint.mintPayment(harden(tokenAmount)); + // Note that the proposal `want` is empty + // This is due to a current limitation in proposal + // expressiveness: + // https://github.com/Agoric/agoric-sdk/issues/855 + // It's impossible to know in advance how many tokens will be + // sold, so it's not possible to say `want: moola(3*22)` + // In a future version of Zoe, it will be possible to express: + // "I want n times moolas where n is the number of sold tokens" + const proposal = harden({ + give: { Items: tokenAmount }, + }); + const paymentKeywordRecord = harden({ Items: tokenPayment }); + + const issuerKeywordRecord = harden({ + Items: issuer, + Money: moneyIssuer, + }); + + const sellItemsTerms = harden({ + pricePerItem, + }); + return zoeService + .makeInstance( + sellItemsInstallationHandle, + issuerKeywordRecord, + sellItemsTerms, + ) + .then(({ invite, instanceRecord: { handle: instanceHandle } }) => { + return zoeService + .offer(invite, proposal, paymentKeywordRecord) + .then(offerResult => { + return harden({ + ...offerResult, + sellItemsInstanceHandle: instanceHandle, + }); + }); + }); + }; + + const mintTokensHook = _offerHandle => { + // outcome is an object with a sellTokens method + return harden({ sellTokens }); + }; + + zcf.initPublicAPI( + harden({ + getTokenIssuer: () => issuer, + }), + ); + + return zcf.makeInvitation(mintTokensHook, 'mint tokens'); +}; + +harden(makeContract); +export { makeContract }; diff --git a/packages/zoe/src/contracts/mintPayments.js b/packages/zoe/src/contracts/mintPayments.js index f5014cb7196..89dc451b78d 100644 --- a/packages/zoe/src/contracts/mintPayments.js +++ b/packages/zoe/src/contracts/mintPayments.js @@ -5,67 +5,77 @@ import harden from '@agoric/harden'; import produceIssuer from '@agoric/ertp'; import { makeZoeHelpers } from '../contractSupport'; -/* -This is the simplest contract to mint payments and send them to users -who request them. No offer safety is being enforced here. -*/ +/** + * This is a very simple contract that creates a new issuer and mints payments + * from it, in order to give an example of how that can be done. This contract + * sends new tokens to anyone who requests them. + * + * Offer safety is not enforced here: the expectation is that most contracts + * that want to do something similar would use the ability to mint new payments + * internally rather than sharing that ability widely as this one does. + * + * makeInstance returns an invitation that, when exercised, provides 1000 of the + * new tokens. publicAPI.makeInvite() returns an invitation that accepts an + * empty offer and provides 1000 tokens. + * + * @typedef {import('../zoe').ContractFacet} ContractFacet + * @param {ContractFacet} zcf + */ +const makeContract = zcf => { + // Create the internal token mint for a fungible digital asset + const { issuer, mint, amountMath } = produceIssuer('tokens'); -/** @typedef {import('../zoe').ContractFacet} ContractFacet */ + const zoeHelpers = makeZoeHelpers(zcf); -// zcf is the Zoe Contract Facet, i.e. the contract-facing API of Zoe -export const makeContract = harden( - /** @param {ContractFacet} zcf */ zcf => { - // Create the internal token mint for a fungible digital asset - const { issuer, mint, amountMath } = produceIssuer('tokens'); + // We need to tell Zoe about this issuer and add a keyword for the + // issuer. Let's call this the 'Token' issuer. + return zcf.addNewIssuer(issuer, 'Token').then(() => { + // We need to wait for the promise to resolve (meaning that Zoe + // has done the work of adding a new issuer). + const offerHook = offerHandle => { + // We will send everyone who makes an offer 1000 tokens - const zoeHelpers = makeZoeHelpers(zcf); + const tokens1000 = amountMath.make(1000); + const payment = mint.mintPayment(tokens1000); - // We need to tell Zoe about this issuer and add a keyword for the - // issuer. Let's call this the 'Token' issuer. - return zcf.addNewIssuer(issuer, 'Token').then(() => { - // We need to wait for the promise to resolve (meaning that Zoe - // has done the work of adding a new issuer). - const offerHook = offerHandle => { - // We will send everyone who makes an offer 1000 tokens + // Let's use a helper function which escrows the payment with + // Zoe, and reallocates to the recipientHandle. + return zoeHelpers + .escrowAndAllocateTo({ + amount: tokens1000, + payment, + keyword: 'Token', + recipientHandle: offerHandle, + }) + .then(() => { + // Complete the user's offer so that the user gets a payout + zcf.complete(harden([offerHandle])); - const tokens1000 = amountMath.make(1000); - const payment = mint.mintPayment(tokens1000); + // Since the user is getting the payout through Zoe, we can + // return anything here. Let's return some helpful instructions. + return 'Offer completed. You should receive a payment from Zoe'; + }); + }; - // Let's use a helper function which escrows the payment with - // Zoe, and reallocates to the recipientHandle. - return zoeHelpers - .escrowAndAllocateTo({ - amount: tokens1000, - payment, - keyword: 'Token', - recipientHandle: offerHandle, - }) - .then(() => { - // Complete the user's offer so that the user gets a payout - zcf.complete(harden([offerHandle])); + // A function for making invites to this contract + const makeInvite = () => zcf.makeInvitation(offerHook, 'mint a payment'); - // Since the user is getting the payout through Zoe, we can - // return anything here. Let's return some helpful instructions. - return 'Offer completed. You should receive a payment from Zoe'; - }); - }; + zcf.initPublicAPI( + harden({ + // provide a way for anyone who knows the instanceHandle of + // the contract to make their own invite. + makeInvite, + // make the token issuer public. Note that only the mint can + // make new digital assets. The issuer is ok to make public. + getTokenIssuer: () => issuer, + }), + ); - // A function for making invites to this contract - const makeInvite = () => zcf.makeInvitation(offerHook, 'mint a payment'); + // return an invite to the creator of the contract instance + // through Zoe + return makeInvite(); + }); +}; - return harden({ - // return an invite to the creator of the contract instance - // through Zoe - invite: makeInvite(), - publicAPI: { - // provide a way for anyone who knows the instanceHandle of - // the contract to make their own invite. - makeInvite, - // make the token issuer public. Note that only the mint can - // make new digital assets. The issuer is ok to make public. - getTokenIssuer: () => issuer, - }, - }); - }); - }, -); +harden(makeContract); +export { makeContract }; diff --git a/packages/zoe/src/contracts/multipoolAutoswap.js b/packages/zoe/src/contracts/multipoolAutoswap.js index a7d0ca8b7cc..6bf4b0a8252 100644 --- a/packages/zoe/src/contracts/multipoolAutoswap.js +++ b/packages/zoe/src/contracts/multipoolAutoswap.js @@ -9,629 +9,562 @@ import { makeTable, makeValidateProperties } from '../table'; import { assertKeywordName } from '../cleanProposal'; import { makeZoeHelpers, - getCurrentPrice, + getInputPrice, calcLiqExtentToMint, calcExtentToRemove, } from '../contractSupport'; +import { filterObj } from '../objArrayConversion'; /** - * @typedef {import('../zoe').ContractFacet} ContractFacet + * Autoswap is a rewrite of Uniswap. Please see the documentation for more + * https://agoric.com/documentation/zoe/guide/contracts/autoswap.html + * + * We expect that this contract will have tens to hundreds of issuers. + * Each liquidity pool is between the central token and a secondary + * token. Secondary tokens can be exchanged with each other, but only + * through the central token. For example, if X and Y are two token + * types and C is the central token, a swap giving X and wanting Y + * would first use the pool (X, C) then the pool (Y, C). There are no + * liquidity pools between two secondary tokens. + * + * There should only need to be one instance of this contract, so liquidity can + * be shared as much as possible. + * + * When the contract is instantiated, the central token is specified in the + * issuerKeywordRecord. The party that calls makeInstance gets an invitation + * that can be used to request an invitation to add liquidity. The same + * invitation is available by calling `publicAPI.getLiquidityIssuer(brand)`. + * Separate invitations are available for adding and removing liquidity, and for + * making trades. Other API operations support monitoring prices and the sizes + * of pools. + * * @typedef {import('@agoric/ertp/src/issuer').Amount} Amount + * @typedef {import('@agoric/ertp/src/issuer').Brand} Brand * @typedef {import('../zoe').AmountKeywordRecords} AmountKeywordRecords + * @typedef {import('../zoe').ContractFacet} ContractFacet + * @param {ContractFacet} zcf */ - -// Autoswap is a rewrite of Uniswap. Please see the documentation for more -// https://agoric.com/documentation/zoe/guide/contracts/autoswap.html - -// We expect that this contract will have tens to hundreds of issuers. -// Each liquidity pool is between the central token and a secondary -// token. Secondary tokens can be exchanged with each other, but only -// through the central token. For example, if X and Y are two token -// types and C is the central token, a swap giving X and wanting Y -// would first use the pool (X, C) then the pool (Y, C). There are no -// liquidity pools between two secondary tokens. - -export const makeContract = harden( - /** @param {ContractFacet} zcf */ zcf => { - // This contract must have a "central token" issuer in the terms. - const CENTRAL_TOKEN = 'CentralToken'; - - const getCentralTokenBrand = () => { - const { - terms: { CentralToken: centralTokenIssuer }, - } = zcf.getInstanceRecord(); - const { brand: centralTokenBrand } = zcf.getIssuerRecord( - centralTokenIssuer, - ); - assert( - centralTokenBrand !== undefined, - details`centralTokenBrand must be present`, +const makeContract = zcf => { + // This contract must have a "central token" issuer in the terms. + const CENTRAL_TOKEN = 'CentralToken'; + + const getCentralTokenBrand = () => { + const { brandKeywordRecord } = zcf.getInstanceRecord(); + assert( + brandKeywordRecord.CentralToken !== undefined, + details`centralTokenBrand must be present`, + ); + return brandKeywordRecord.CentralToken; + }; + const centralTokenBrand = getCentralTokenBrand(); + + const { + trade, + rejectOffer, + makeEmptyOffer, + checkHook, + assertKeywords, + escrowAndAllocateTo, + assertNatMathHelpers, + } = makeZoeHelpers(zcf); + + // There must be one keyword at the start, which is equal to the + // value of CENTRAL_TOKEN + assertKeywords([CENTRAL_TOKEN]); + + // We need to be able to retrieve information about the liquidity + // pools by tokenBrand. Key: tokenBrand Columns: poolHandle, + // tokenIssuer, liquidityMint, liquidityIssuer, tokenKeyword, + // liquidityKeyword, liquidityTokenSupply + const liquidityTable = makeTable( + makeValidateProperties( + harden([ + 'poolHandle', + 'tokenIssuer', + 'tokenBrand', + 'liquidityMint', + 'liquidityIssuer', + 'liquidityBrand', + 'tokenKeyword', + 'liquidityKeyword', + 'liquidityTokenSupply', + ]), + ), + ); + + // Allows users to add new liquidity pools. `newTokenIssuer` and + // `newTokenKeyword` must not have been already used + const addPool = (newTokenIssuer, newTokenKeyword) => { + assertKeywordName(newTokenKeyword); + const { brandKeywordRecord } = zcf.getInstanceRecord(); + const keywords = Object.keys(brandKeywordRecord); + const brands = Object.values(brandKeywordRecord); + assert( + !keywords.includes(newTokenKeyword), + details`newTokenKeyword must be unique`, + ); + // TODO: handle newTokenIssuer as a potential promise + assert( + !brands.includes(newTokenIssuer.brand), + details`newTokenIssuer must not be already present`, + ); + const newLiquidityKeyword = `${newTokenKeyword}Liquidity`; + assert( + !keywords.includes(newLiquidityKeyword), + details`newLiquidityKeyword must be unique`, + ); + const { + mint: liquidityMint, + issuer: liquidityIssuer, + brand: liquidityBrand, + } = produceIssuer(newLiquidityKeyword); + return Promise.all([ + zcf.addNewIssuer(newTokenIssuer, newTokenKeyword), + makeEmptyOffer(), + zcf.addNewIssuer(liquidityIssuer, newLiquidityKeyword), + ]).then(([newTokenIssuerRecord, poolHandle]) => { + // The final element of the above array is intentionally + // ignored, since we already have the liquidityIssuer and mint. + assertNatMathHelpers(newTokenIssuerRecord.brand); + liquidityTable.create( + harden({ + poolHandle, + tokenIssuer: newTokenIssuer, + tokenBrand: newTokenIssuerRecord.brand, + liquidityMint, + liquidityIssuer, + liquidityBrand, + tokenKeyword: newTokenKeyword, + liquidityKeyword: newLiquidityKeyword, + liquidityTokenSupply: 0, + }), + newTokenIssuerRecord.brand, ); - return centralTokenBrand; - }; - const centralTokenBrand = getCentralTokenBrand(); + return `liquidity pool for ${newTokenKeyword} added`; + }); + }; + + // The secondary token brand is used as the key of liquidityTable + // rows, and we use it to look up the pool allocation. We only + // return the keywords for the secondary token, the central token, + // and the associated liquidity token. + const getPoolAllocation = tokenBrand => { + const { poolHandle, tokenKeyword, liquidityKeyword } = liquidityTable.get( + tokenBrand, + ); + + const brandKeywordRecord = filterObj( + zcf.getInstanceRecord().brandKeywordRecord, + [tokenKeyword, CENTRAL_TOKEN, liquidityKeyword], + ); + return zcf.getCurrentAllocation(poolHandle, brandKeywordRecord); + }; + + const findSecondaryTokenBrand = ({ + brandIn, + brandOut, + offerHandleToReject, + }) => { + if (liquidityTable.has(brandIn)) { + return brandIn; + } + if (liquidityTable.has(brandOut)) { + return brandOut; + } + // We couldn't find either so throw. Reject the offer if + // offerHandleToReject is defined. + const msg = `No secondary token was found`; + if (offerHandleToReject !== undefined) { + rejectOffer(offerHandleToReject, msg); + } + throw new Error(msg); + }; + + const getPoolKeyword = brandToMatch => { + if (brandToMatch === centralTokenBrand) { + return CENTRAL_TOKEN; + } + if (!liquidityTable.has(brandToMatch)) { + throw new Error('getPoolKeyword: brand not found'); + } + const { tokenKeyword } = liquidityTable.get(brandToMatch); + return tokenKeyword; + }; + + const getPoolAmount = (poolAllocation, desiredBrand) => { + const keyword = getPoolKeyword(desiredBrand); + return poolAllocation[keyword]; + }; + + const doGetCurrentPrice = ({ amountIn, brandOut }) => { + const brandIn = amountIn.brand; + const secondaryTokenBrand = findSecondaryTokenBrand({ + brandIn, + brandOut, + }); + const poolAllocation = getPoolAllocation(secondaryTokenBrand); + const outputExtent = getInputPrice({ + inputExtent: amountIn.extent, + inputReserve: getPoolAmount(poolAllocation, brandIn).extent, + outputReserve: getPoolAmount(poolAllocation, brandOut).extent, + }); + return zcf.getAmountMath(brandOut).make(outputExtent); + }; + + const rejectIfNotTokenBrand = (inviteHandle, brand) => { + if (!liquidityTable.has(brand)) { + rejectOffer(inviteHandle, `brand ${brand} was not recognized`); + } + }; + + const addLiquidityExpected = harden({ + give: { + CentralToken: null, + SecondaryToken: null, + }, + want: { Liquidity: null }, + }); + + const addLiquidityHook = offerHandle => { + // Get the brand of the secondary token so we can identify the liquidity pool. const { - rejectOffer, - makeEmptyOffer, - rejectIfNotProposal, - assertKeywords, - getKeys, - escrowAndAllocateTo, - } = makeZoeHelpers(zcf); - - // There must be one keyword at the start, which is equal to the - // value of CENTRAL_TOKEN - assertKeywords([CENTRAL_TOKEN]); - - // We need to be able to retrieve information about the liquidity - // pools by tokenBrand. Key: tokenBrand Columns: poolHandle, - // tokenIssuer, liquidityMint, liquidityIssuer, tokenKeyword, - // liquidityKeyword, liquidityTokenSupply - const liquidityTable = makeTable( - makeValidateProperties( - harden([ - 'poolHandle', - 'tokenIssuer', - 'liquidityMint', - 'liquidityIssuer', - 'tokenKeyword', - 'liquidityKeyword', - 'liquidityTokenSupply', - ]), - ), + proposal: { + give: { + SecondaryToken: { brand: secondaryTokenBrand }, + }, + }, + } = zcf.getOffer(offerHandle); + + const { + tokenKeyword, + liquidityBrand, + liquidityTokenSupply, + liquidityMint, + poolHandle, + } = liquidityTable.get(secondaryTokenBrand); + + const userAllocation = zcf.getCurrentAllocation(offerHandle); + const poolAllocation = getPoolAllocation(secondaryTokenBrand); + + // Calculate how many liquidity tokens we should be minting. + const liquidityExtentOut = calcLiqExtentToMint( + harden({ + liqTokenSupply: liquidityTokenSupply, + inputExtent: userAllocation.CentralToken.extent, + inputReserve: poolAllocation.CentralToken.extent, + }), ); - // Allows users to add new liquidity pools. `newTokenIssuer` and - // `newTokenKeyword` must not have been already used - const addPool = (newTokenIssuer, newTokenKeyword) => { - assertKeywordName(newTokenKeyword); - const { issuerKeywordRecord } = zcf.getInstanceRecord(); - const keywords = Object.keys(issuerKeywordRecord); - const issuers = Object.values(issuerKeywordRecord); - assert( - !keywords.includes(newTokenKeyword), - details`newTokenKeyword must be unique`, - ); - // TODO: handle newTokenIssuer as a potential promise - assert( - !issuers.includes(newTokenIssuer), - details`newTokenIssuer must not be already present`, - ); - const newLiquidityKeyword = `${newTokenKeyword}Liquidity`; - assert( - !keywords.includes(newLiquidityKeyword), - details`newLiquidityKeyword must be unique`, - ); - const { mint: liquidityMint, issuer: liquidityIssuer } = produceIssuer( - newLiquidityKeyword, - ); - return Promise.all([ - zcf.addNewIssuer(newTokenIssuer, newTokenKeyword), - makeEmptyOffer(), - zcf.addNewIssuer(liquidityIssuer, newLiquidityKeyword), - ]).then(([newTokenIssuerRecord, poolHandle]) => { - // The third element of the above array is intentionally - // ignored, since we already have the liquidityIssuer and mint. - const amountMaths = zcf.getAmountMaths(harden([newTokenKeyword])); - assert( - amountMaths[newTokenKeyword].getMathHelpersName() === 'nat', - details`tokenIssuer must have natMathHelpers`, - ); - liquidityTable.create( - harden({ - poolHandle, - tokenIssuer: newTokenIssuer, - liquidityMint, - liquidityIssuer, - tokenKeyword: newTokenKeyword, - liquidityKeyword: newLiquidityKeyword, - liquidityTokenSupply: 0, - }), - newTokenIssuerRecord.brand, - ); - return `liquidity pool for ${newTokenKeyword} added`; - }); - }; - - // The secondary token brand is used as the key of liquidityTable - // rows, and we use it to look up the pool allocation. We only - // return the keywords for the secondary token, the central token, - // and the associated liquidity token. - const getPoolAllocation = tokenBrand => { - const { poolHandle, tokenKeyword, liquidityKeyword } = liquidityTable.get( - tokenBrand, - ); - return zcf.getCurrentAllocation( - poolHandle, - harden([tokenKeyword, CENTRAL_TOKEN, liquidityKeyword]), - ); - }; - - const doGetCurrentPrice = ({ - amountIn, - keywordIn, - keywordOut, - secondaryBrand, - }) => { - const poolAmounts = getPoolAllocation(secondaryBrand); - const { outputExtent } = getCurrentPrice( - harden({ - inputExtent: amountIn.extent, - inputReserve: poolAmounts[keywordIn].extent, - outputReserve: poolAmounts[keywordOut].extent, - }), - ); - const amountMaths = zcf.getAmountMaths(harden([keywordOut])); - return amountMaths[keywordOut].make(outputExtent); - }; - - const doSwap = ({ - userAllocation, - keywordIn, - keywordOut, - secondaryBrand, - }) => { - const { poolHandle } = liquidityTable.get(secondaryBrand); - const poolAllocation = getPoolAllocation(secondaryBrand); - const { - outputExtent, - newInputReserve, - newOutputReserve, - } = getCurrentPrice( - harden({ - inputExtent: userAllocation[keywordIn].extent, - inputReserve: poolAllocation[keywordIn].extent, - outputReserve: poolAllocation[keywordOut].extent, - }), - ); - const amountMaths = zcf.getAmountMaths([keywordIn, keywordOut]); - const amountOut = amountMaths[keywordOut].make(outputExtent); + const liquidityAmountOut = zcf + .getAmountMath(liquidityBrand) + .make(liquidityExtentOut); - const newUserAmounts = harden({ - [keywordIn]: amountMaths[keywordIn].getEmpty(), - [keywordOut]: amountOut, - }); + const liquidityPaymentP = liquidityMint.mintPayment(liquidityAmountOut); - const newPoolAmounts = harden({ - [keywordIn]: amountMaths[keywordIn].make(newInputReserve), - [keywordOut]: amountMaths[keywordOut].make(newOutputReserve), - }); + // We update the liquidityTokenSupply before the next turn + liquidityTable.update(secondaryTokenBrand, { + liquidityTokenSupply: liquidityTokenSupply + liquidityExtentOut, + }); - return harden({ poolHandle, newUserAmounts, newPoolAmounts }); - }; - - const getSecondaryBrand = ({ offerHandle, isAddLiquidity }) => { - const { proposal } = zcf.getOffer(offerHandle); - const key = isAddLiquidity ? 'give' : 'want'; - const { - // eslint-disable-next-line no-unused-vars - [key]: { [CENTRAL_TOKEN]: centralAmount, ...tokenAmountKeywordRecord }, - } = proposal; - const values = Object.values(tokenAmountKeywordRecord); - if (values.length !== 1) { - rejectOffer(offerHandle, `only one secondary brand should be present`); - } - return values[0].brand; - }; - - const makeAdd = amountMaths => (key, obj1, obj2) => - amountMaths[key].add(obj1[key], obj2[key]); - - const makeSubtract = amountMaths => (key, obj1, obj2) => - amountMaths[key].subtract(obj1[key], obj2[key]); - - const makeGetAllEmpty = amountMaths => keywords => { - const newObj = {}; - keywords.forEach( - keyword => (newObj[keyword] = amountMaths[keyword].getEmpty()), - ); - // intentionally not hardened - return newObj; - }; - - const rejectIfNotTokenBrand = (inviteHandle, brand) => { - if (!liquidityTable.has(brand)) { - rejectOffer(inviteHandle, `brand ${brand} was not recognized`); - } - }; - - const addLiquidityHook = offerHandle => { - // Get the brand of the secondary token so we can identify the liquidity pool. - const secondaryTokenBrand = getSecondaryBrand( - harden({ - offerHandle, - isAddLiquidity: true, - }), + // The contract needs to escrow the liquidity payment with Zoe + // to eventually payout to the user + return escrowAndAllocateTo({ + amount: liquidityAmountOut, + payment: liquidityPaymentP, + keyword: 'Liquidity', + recipientHandle: offerHandle, + }).then(() => { + trade( + { + offerHandle: poolHandle, + gains: { + CentralToken: userAllocation.CentralToken, + [tokenKeyword]: userAllocation.SecondaryToken, + }, + }, + // We reallocated liquidity in the call to + // escrowAndAllocateTo. + { offerHandle, gains: {}, losses: userAllocation }, ); - const { - tokenKeyword, - liquidityKeyword, - liquidityTokenSupply, - liquidityMint, - poolHandle, - } = liquidityTable.get(secondaryTokenBrand); - - // These are the keywords that will be used several times within this method - const liquidityKeys = harden([ - CENTRAL_TOKEN, - tokenKeyword, - liquidityKeyword, - ]); - - const expected = harden({ - give: { - [CENTRAL_TOKEN]: null, - [tokenKeyword]: null, + zcf.complete(harden([offerHandle])); + return 'Added liquidity.'; + }); + }; + + const removeLiquidityExpected = harden({ + want: { + CentralToken: null, + SecondaryToken: null, + }, + give: { + Liquidity: null, + }, + }); + + const removeLiquidityHook = offerHandle => { + // Get the brand of the secondary token so we can identify the liquidity pool. + const { + proposal: { + want: { + SecondaryToken: { brand: secondaryTokenBrand }, }, - want: { [liquidityKeyword]: null }, - }); - rejectIfNotProposal(offerHandle, expected); - - const userAmounts = zcf.getCurrentAllocation(offerHandle, liquidityKeys); - const poolAmounts = getPoolAllocation(secondaryTokenBrand); + }, + } = zcf.getOffer(offerHandle); - // Calculate how many liquidity tokens we should be minting. - const liquidityExtentOut = calcLiqExtentToMint( + const { + tokenKeyword, + liquidityKeyword, + liquidityTokenSupply, + poolHandle, + } = liquidityTable.get(secondaryTokenBrand); + + const userAllocation = zcf.getCurrentAllocation(offerHandle); + const poolAllocation = getPoolAllocation(secondaryTokenBrand); + const liquidityExtentIn = userAllocation.Liquidity.extent; + + const centralTokenAmountOut = zcf.getAmountMath(centralTokenBrand).make( + calcExtentToRemove( harden({ liqTokenSupply: liquidityTokenSupply, - inputExtent: userAmounts[CENTRAL_TOKEN].extent, - inputReserve: poolAmounts[CENTRAL_TOKEN].extent, + poolExtent: poolAllocation[CENTRAL_TOKEN].extent, + liquidityExtentIn, }), - ); - const amountMaths = zcf.getAmountMaths(liquidityKeys); + ), + ); - const liquidityAmountOut = amountMaths[liquidityKeyword].make( - liquidityExtentOut, - ); + const tokenKeywordAmountOut = zcf.getAmountMath(secondaryTokenBrand).make( + calcExtentToRemove( + harden({ + liqTokenSupply: liquidityTokenSupply, + poolExtent: poolAllocation[tokenKeyword].extent, + liquidityExtentIn, + }), + ), + ); - const liquidityPaymentP = liquidityMint.mintPayment(liquidityAmountOut); + liquidityTable.update(secondaryTokenBrand, { + liquidityTokenSupply: liquidityTokenSupply - liquidityExtentIn, + }); - // We update the liquidityTokenSupply before the next turn - liquidityTable.update(secondaryTokenBrand, { - liquidityTokenSupply: liquidityTokenSupply + liquidityExtentOut, - }); + trade( + { + offerHandle: poolHandle, + gains: { [liquidityKeyword]: userAllocation.Liquidity }, + losses: { + CentralToken: centralTokenAmountOut, + [tokenKeyword]: tokenKeywordAmountOut, + }, + }, + { + offerHandle, + gains: { + CentralToken: centralTokenAmountOut, + SecondaryToken: tokenKeywordAmountOut, + }, + losses: { + Liquidity: userAllocation.Liquidity, + }, + }, + ); - // The contract needs to escrow the liquidity payment with Zoe - // to eventually pay as a payout to the user - return escrowAndAllocateTo({ - amount: liquidityAmountOut, - payment: liquidityPaymentP, - keyword: liquidityKeyword, - recipientHandle: offerHandle, - }).then(() => { - const add = makeAdd(amountMaths); - const getAllEmpty = makeGetAllEmpty(amountMaths); - const newPoolAmounts = harden({ - [CENTRAL_TOKEN]: add(CENTRAL_TOKEN, userAmounts, poolAmounts), - [tokenKeyword]: add(tokenKeyword, userAmounts, poolAmounts), - [liquidityKeyword]: poolAmounts[liquidityKeyword], - }); - - const newUserAmounts = getAllEmpty(liquidityKeys); - newUserAmounts[liquidityKeyword] = liquidityAmountOut; - - zcf.reallocate( - harden([offerHandle, poolHandle]), - harden([newUserAmounts, newPoolAmounts]), - liquidityKeys, - ); - zcf.complete(harden([offerHandle])); - return 'Added liquidity.'; - }); - }; + zcf.complete(harden([offerHandle])); + return 'Liquidity successfully removed.'; + }; - const removeLiquidityHook = offerHandle => { - const secondaryTokenBrand = getSecondaryBrand( - harden({ offerHandle, isAddLiquidity: false }), - ); + const swapExpected = harden({ + give: { + In: null, + }, + want: { + Out: null, + }, + }); - const { - tokenKeyword, - liquidityKeyword, - liquidityTokenSupply, - poolHandle, - } = liquidityTable.get(secondaryTokenBrand); + const swapHook = offerHandle => { + const { + give: { In: amountIn }, + want: { Out: wantedAmountOut }, + } = zcf.getOffer(offerHandle).proposal; + const brandIn = amountIn.brand; + const brandOut = wantedAmountOut.brand; - const expected = harden({ - want: { [CENTRAL_TOKEN]: null, [tokenKeyword]: null }, - give: { [liquidityKeyword]: null }, - }); - rejectIfNotProposal(offerHandle, expected); + // we could be swapping (1) secondary to secondary, (2) central + // to secondary, or (3) secondary to central. - const liquidityKeys = harden([ - CENTRAL_TOKEN, - tokenKeyword, - liquidityKeyword, - ]); + // 1) secondary to secondary + if (liquidityTable.has(brandIn) && liquidityTable.has(brandOut)) { + rejectIfNotTokenBrand(offerHandle, brandIn); + rejectIfNotTokenBrand(offerHandle, brandOut); - const userAllocation = zcf.getCurrentAllocation( - offerHandle, - liquidityKeys, + const centralTokenAmount = doGetCurrentPrice( + harden({ + amountIn, + brandOut: centralTokenBrand, + }), ); - const poolAllocation = getPoolAllocation(secondaryTokenBrand); - const liquidityExtentIn = userAllocation[liquidityKeyword].extent; - - const amountMaths = zcf.getAmountMaths(liquidityKeys); - - const subtract = makeSubtract(amountMaths); - - const newUserAmounts = harden({ - [CENTRAL_TOKEN]: amountMaths[CENTRAL_TOKEN].make( - calcExtentToRemove( - harden({ - liqTokenSupply: liquidityTokenSupply, - poolExtent: poolAllocation[CENTRAL_TOKEN].extent, - liquidityExtentIn, - }), - ), - ), - [tokenKeyword]: amountMaths[tokenKeyword].make( - calcExtentToRemove( - harden({ - liqTokenSupply: liquidityTokenSupply, - poolExtent: poolAllocation[tokenKeyword].extent, - liquidityExtentIn, - }), - ), - ), - [liquidityKeyword]: amountMaths[liquidityKeyword].getEmpty(), + const amountOut = doGetCurrentPrice( + harden({ + amountIn: centralTokenAmount, + brandOut, + }), + ); + + const brandInAmountMath = zcf.getAmountMath(brandIn); + const finalUserAmounts = harden({ + In: brandInAmountMath.getEmpty(), + Out: amountOut, }); - const newPoolAmounts = harden({ - [CENTRAL_TOKEN]: subtract( - CENTRAL_TOKEN, - poolAllocation, - newUserAmounts, + const { poolHandle: poolHandleA } = liquidityTable.get(brandIn); + const poolAllocationA = zcf.getCurrentAllocation(poolHandleA); + const poolKeywordBrandIn = getPoolKeyword(brandIn); + const centralTokenAmountMath = zcf.getAmountMath(centralTokenBrand); + const finalPoolAmountsA = { + [poolKeywordBrandIn]: brandInAmountMath.add( + poolAllocationA[poolKeywordBrandIn], + amountIn, ), - [tokenKeyword]: subtract(tokenKeyword, poolAllocation, newUserAmounts), - [liquidityKeyword]: amountMaths[liquidityKeyword].add( - poolAllocation[liquidityKeyword], - amountMaths[liquidityKeyword].make(liquidityExtentIn), + CentralToken: centralTokenAmountMath.subtract( + poolAllocationA.CentralToken, + centralTokenAmount, ), - }); + }; - liquidityTable.update(secondaryTokenBrand, { - liquidityTokenSupply: liquidityTokenSupply - liquidityExtentIn, - }); + const { poolHandle: poolHandleB } = liquidityTable.get(brandOut); + const poolAllocationB = zcf.getCurrentAllocation(poolHandleB); + const poolKeywordBrandOut = getPoolKeyword(brandOut); + const brandOutAmountMath = zcf.getAmountMath(brandOut); + const finalPoolAmountsB = { + CentralToken: centralTokenAmountMath.add( + poolAllocationB.CentralToken, + centralTokenAmount, + ), + [poolKeywordBrandOut]: brandOutAmountMath.subtract( + poolAllocationB[poolKeywordBrandOut], + amountOut, + ), + }; zcf.reallocate( - harden([offerHandle, poolHandle]), - harden([newUserAmounts, newPoolAmounts]), - liquidityKeys, + harden([poolHandleA, poolHandleB, offerHandle]), + harden([finalPoolAmountsA, finalPoolAmountsB, finalUserAmounts]), ); zcf.complete(harden([offerHandle])); - return 'Liquidity successfully removed.'; - }; - - const swapHook = offerHandle => { - const { proposal } = zcf.getOffer(offerHandle); - const getKeywordAndBrand = amountKeywordRecord => { - const keywords = getKeys(amountKeywordRecord); - if (keywords.length !== 1) { - rejectOffer( - offerHandle, - `A swap requires giving one type of token for another, ${keywords.length} tokens were provided.`, - ); - } - return harden({ - keyword: keywords[0], - brand: Object.values(amountKeywordRecord)[0].brand, - }); - }; - - const { keyword: keywordIn, brand: brandIn } = getKeywordAndBrand( - proposal.give, - ); - const { keyword: keywordOut, brand: brandOut } = getKeywordAndBrand( - proposal.want, - ); + return `Swap successfully completed.`; + } + // 2) central to secondary and 3) secondary to central + const secondaryTokenBrand = findSecondaryTokenBrand({ + brandIn, + brandOut, + offerHandleToReject: offerHandle, + }); + const { poolHandle } = liquidityTable.get(secondaryTokenBrand); - const expected = harden({ - give: { [keywordIn]: null }, - want: { [keywordOut]: null }, - }); - rejectIfNotProposal(offerHandle, expected); - - // we could be swapping (1) central to secondary, (2) secondary to central, or (3) secondary to secondary. - - // 1) central to secondary - if (brandIn === centralTokenBrand) { - rejectIfNotTokenBrand(offerHandle, brandOut); - - const keywords = harden([keywordIn, keywordOut]); - const { poolHandle, newUserAmounts, newPoolAmounts } = doSwap( - harden({ - userAllocation: zcf.getCurrentAllocation(offerHandle, keywords), - keywordIn, - keywordOut, - secondaryBrand: brandOut, - }), - ); - zcf.reallocate( - harden([offerHandle, poolHandle]), - harden([newUserAmounts, newPoolAmounts]), - keywords, - ); - zcf.complete(harden([offerHandle])); - return `Swap successfully completed.`; - - // eslint-disable-next-line no-else-return - } else if (brandOut === centralTokenBrand) { - // 2) secondary to central - rejectIfNotTokenBrand(offerHandle, brandIn); - const keywords = harden([keywordIn, keywordOut]); - const { poolHandle, newUserAmounts, newPoolAmounts } = doSwap( - harden({ - userAllocation: zcf.getCurrentAllocation(offerHandle, keywords), - keywordIn, - keywordOut, - secondaryBrand: brandIn, - }), - ); - zcf.reallocate( - harden([offerHandle, poolHandle]), - harden([newUserAmounts, newPoolAmounts]), - keywords, - ); - zcf.complete(harden([offerHandle])); - return `Swap successfully completed.`; - } else { - // 3) secondary to secondary - rejectIfNotTokenBrand(offerHandle, brandIn); - rejectIfNotTokenBrand(offerHandle, brandOut); - - const { - poolHandle: poolHandleA, - newUserAmounts: newUserAmountsA, - newPoolAmounts: newPoolAmountsA, - } = doSwap( - harden({ - userAllocation: zcf.getCurrentAllocation( - offerHandle, - harden([keywordIn, CENTRAL_TOKEN]), - ), - keywordIn, - keywordOut: CENTRAL_TOKEN, - secondaryBrand: brandIn, - }), - ); - const { - poolHandle: poolHandleB, - newUserAmounts, - newPoolAmounts: newPoolAmountsB, - } = doSwap( - harden({ - userAllocation: newUserAmountsA, - keywordIn: CENTRAL_TOKEN, - keywordOut, - secondaryBrand: brandOut, - }), - ); - const keywords = harden([keywordIn, keywordOut, CENTRAL_TOKEN]); - const amountMaths = zcf.getAmountMaths(keywords); - const finalPoolAmountsA = { - ...newPoolAmountsA, - [keywordOut]: amountMaths[keywordOut].getEmpty(), - }; - const finalPoolAmountsB = { - ...newPoolAmountsB, - [keywordIn]: amountMaths[keywordIn].getEmpty(), - }; - const finalUserAmounts = { - ...newUserAmounts, - [keywordIn]: newUserAmountsA[keywordIn], - }; - zcf.reallocate( - harden([poolHandleA, poolHandleB, offerHandle]), - harden([finalPoolAmountsA, finalPoolAmountsB, finalUserAmounts]), - keywords, - ); - zcf.complete(harden([offerHandle])); - return `Swap successfully completed.`; - } - }; - - const makeAddLiquidityInvite = () => - zcf.makeInvitation(addLiquidityHook, 'multipool autoswap add liquidity'); - - return harden({ - invite: makeAddLiquidityInvite(), - publicAPI: { - getBrandKeywordRecord: () => { - const { issuerKeywordRecord } = zcf.getInstanceRecord(); - const brandKeywordRecord = {}; - Object.entries(issuerKeywordRecord).forEach(([keyword, issuer]) => { - const { brand } = zcf.getIssuerRecord(issuer); - brandKeywordRecord[keyword] = brand; - }); - return harden(brandKeywordRecord); - }, - getKeywordForBrand: brand => { - assert( - liquidityTable.has(brand), - details`There is no pool for this brand. To create a pool, call 'addPool'`, - ); - return liquidityTable.get(brand).tokenKeyword; + const amountOut = doGetCurrentPrice( + harden({ + amountIn, + brandOut, + }), + ); + trade( + { + offerHandle: poolHandle, + gains: { + [getPoolKeyword(brandIn)]: amountIn, }, - addPool, - getPoolAllocation, - getLiquidityIssuer: tokenBrand => - liquidityTable.get(tokenBrand).liquidityIssuer, - /** - * `getCurrentPrice` calculates the result of a trade, given a certain - * amount of digital assets in. - * @param {object} amountIn - the amount of digital assets to be - * sent in - */ - getCurrentPrice: (amountIn, brandOut) => { - const brandIn = amountIn.brand; - // brandIn could either be the central token brand, or one of - // the secondary token brands - - // CentralToken to SecondaryToken - if (brandIn === centralTokenBrand) { - assert( - liquidityTable.has(brandOut), - details`brandOut ${brandOut} was not recognized`, - ); - return doGetCurrentPrice( - harden({ - amountIn, - keywordIn: CENTRAL_TOKEN, - keywordOut: liquidityTable.get(brandOut).tokenKeyword, - secondaryBrand: brandOut, - }), - ); - // eslint-disable-next-line no-else-return - } else if (brandOut === centralTokenBrand) { - // SecondaryToken to CentralToken - assert( - liquidityTable.has(brandIn), - details`amountIn brand ${amountIn} was not recognized`, - ); - return doGetCurrentPrice( - harden({ - amountIn, - keywordIn: liquidityTable.get(brandIn).tokenKeyword, - keywordOut: CENTRAL_TOKEN, - secondaryBrand: brandIn, - }), - ); - } else { - // SecondaryToken to SecondaryToken - assert( - liquidityTable.has(brandIn) && liquidityTable.has(brandOut), - details`amountIn brand ${brandIn} or brandOut ${brandOut} was not recognized`, - ); - - // We must do two consecutive `doGetCurrentPrice` calls: from - // the brandIn to the central token, then from the central - // token to the brandOut - const centralTokenAmount = doGetCurrentPrice( - harden({ - amountIn, - keywordIn: liquidityTable.get(brandIn).tokenKeyword, - keywordOut: CENTRAL_TOKEN, - secondaryBrand: brandIn, - }), - ); - return doGetCurrentPrice( - harden({ - amountIn: centralTokenAmount, - keywordIn: CENTRAL_TOKEN, - keywordOut: liquidityTable.get(brandOut).tokenKeyword, - secondaryBrand: brandOut, - }), - ); - } + losses: { + [getPoolKeyword(brandOut)]: amountOut, }, - makeSwapInvite: () => zcf.makeInvitation(swapHook, 'autoswap swap'), - makeAddLiquidityInvite, - makeRemoveLiquidityInvite: () => - zcf.makeInvitation(removeLiquidityHook, 'autoswap remove liquidity'), }, - }); - }, -); + { + offerHandle, + gains: { Out: amountOut }, + losses: { In: amountIn }, + }, + ); + zcf.complete(harden([offerHandle])); + return `Swap successfully completed.`; + }; + + /** + * `getCurrentPrice` calculates the result of a trade, given a certain + * amount of digital assets in. + * @param {Amount} amountIn - the amount of digital assets to be + * sent in + * @param {Brand} brandOut - the brand of the requested payment. + */ + const getCurrentPrice = (amountIn, brandOut) => { + const brandIn = amountIn.brand; + // brandIn could either be the central token brand, or one of + // the secondary token brands + + // SecondaryToken to SecondaryToken + if (brandIn !== centralTokenBrand && brandOut !== centralTokenBrand) { + assert( + liquidityTable.has(brandIn) && liquidityTable.has(brandOut), + details`amountIn brand ${brandIn} or brandOut ${brandOut} was not recognized`, + ); + + // We must do two consecutive `doGetCurrentPrice` calls: from + // the brandIn to the central token, then from the central + // token to the brandOut + const centralTokenAmount = doGetCurrentPrice( + harden({ + amountIn, + brandOut: centralTokenBrand, + }), + ); + return doGetCurrentPrice( + harden({ + amountIn: centralTokenAmount, + brandOut, + }), + ); + } + + // All other cases: secondaryToken to CentralToken or vice versa. + return doGetCurrentPrice( + harden({ + amountIn, + brandOut, + }), + ); + }; + + const getLiquidityIssuer = tokenBrand => + liquidityTable.get(tokenBrand).liquidityIssuer; + + const makeAddLiquidityInvite = () => + zcf.makeInvitation( + checkHook(addLiquidityHook, addLiquidityExpected), + 'multipool autoswap add liquidity', + ); + + const makeSwapInvite = () => + zcf.makeInvitation(checkHook(swapHook, swapExpected), 'autoswap swap'); + + const makeRemoveLiquidityInvite = () => + zcf.makeInvitation( + checkHook(removeLiquidityHook, removeLiquidityExpected), + 'autoswap remove liquidity', + ); + + zcf.initPublicAPI( + harden({ + addPool, + getPoolAllocation, + getLiquidityIssuer, + getCurrentPrice, + makeSwapInvite, + makeAddLiquidityInvite, + makeRemoveLiquidityInvite, + }), + ); + + return makeAddLiquidityInvite(); +}; + +harden(makeContract); +export { makeContract }; diff --git a/packages/zoe/src/contracts/operaConcertTicket.js b/packages/zoe/src/contracts/operaConcertTicket.js deleted file mode 100644 index 1bdc0d09d5b..00000000000 --- a/packages/zoe/src/contracts/operaConcertTicket.js +++ /dev/null @@ -1,173 +0,0 @@ -/* eslint-disable no-use-before-define */ -// @ts-check - -import harden from '@agoric/harden'; -import produceIssuer from '@agoric/ertp'; -import { makeZoeHelpers, defaultAcceptanceMsg } from '../contractSupport'; - -/* - Roles in the arrangement: - - Contract creator: describes the contract with: - - number of seats, show, date/time of start - - expected (ERTP) amount per ticket (we assume all tickets cost the same) - - Smart Contract: - - mints the tickets - - provides the seats - - Auditorium (unique contract seat, usually taken by the contract creator): - the person hosting - the Opera show, selling the tickets and getting the payment back - - Ticket buyers (contract seat created on demand): - - can see the available opera show seats - - can consult the terms - - can redeem the zoe invite with the proper payment to get the ticket back - - ERTP and Zoe are considered to be the most highly trusted pieces of code by - everyone - They are more trusted than the code of this contract - As a consequence, they are going to be leveraged as much as possible by this - contract - to increase its trustworthiness and by the contract users -*/ - -/** @typedef {import('../zoe').ContractFacet} ContractFacet */ - -// zcf is the Zoe Contract Facet, i.e. the contract-facing API of Zoe -export const makeContract = harden( - /** @param {ContractFacet} zcf */ zcf => { - // Create the internal ticket mint - const { issuer, mint, amountMath: ticketAmountMath } = produceIssuer( - 'Opera tickets', - 'set', - ); - - const { - terms: { show, start, count, expectedAmountPerTicket }, - issuerKeywordRecord: { Money: moneyIssuer }, - } = zcf.getInstanceRecord(); - - const { amountMath: moneyAmountMath } = zcf.getIssuerRecord(moneyIssuer); - - const { rejectOffer, checkHook, escrowAndAllocateTo } = makeZoeHelpers(zcf); - - let auditoriumOfferHandle; - - return zcf.addNewIssuer(issuer, 'Ticket').then(() => { - // Mint tickets inside the contract - // In a more realistic contract, the Auditorium would certainly mint the - // tickets themselves - // but because of a current technical limitation when running the Agoric - // stack on a blockchain, - // minting has to happen inside a Zoe contract - // https://github.com/Agoric/agoric-sdk/issues/821 - - // Mint the tickets ahead-of-time (instead of on-demand) - // This way, they can be passed to Zoe + ERTP who will be doing the - // bookkeeping - // of which tickets have been sold and which tickets are still for sale - const ticketsAmount = ticketAmountMath.make( - harden( - Array(count) - .fill() - .map((_, i) => { - const ticketNumber = i + 1; - return harden({ - show, - start, - number: ticketNumber, - }); - }), - ), - ); - const ticketsPayment = mint.mintPayment(ticketsAmount); - - const auditoriumOfferHook = offerHandle => { - auditoriumOfferHandle = offerHandle; - return escrowAndAllocateTo({ - amount: ticketsAmount, - payment: ticketsPayment, - keyword: 'Ticket', - recipientHandle: auditoriumOfferHandle, - }).then(() => defaultAcceptanceMsg); - }; - - const buyTicketOfferHook = buyerOfferHandle => { - const buyerOffer = zcf.getOffer(buyerOfferHandle); - - const currentAuditoriumAllocation = zcf.getCurrentAllocation( - auditoriumOfferHandle, - ); - const currentBuyerAllocation = zcf.getCurrentAllocation( - buyerOfferHandle, - ); - - const wantedTicketsCount = - buyerOffer.proposal.want.Ticket.extent.length; - const wantedMoney = expectedAmountPerTicket.extent * wantedTicketsCount; - - try { - if ( - !moneyAmountMath.isGTE( - currentBuyerAllocation.Money, - moneyAmountMath.make(wantedMoney), - ) - ) { - throw new Error( - 'The offer associated with this seat does not contain enough moolas', - ); - } - - const wantedAuditoriumAllocation = { - Money: moneyAmountMath.add( - currentAuditoriumAllocation.Money, - currentBuyerAllocation.Money, - ), - Ticket: ticketAmountMath.subtract( - currentAuditoriumAllocation.Ticket, - buyerOffer.proposal.want.Ticket, - ), - }; - - const wantedBuyerAllocation = { - Money: moneyAmountMath.getEmpty(), - Ticket: ticketAmountMath.add( - currentBuyerAllocation.Ticket, - buyerOffer.proposal.want.Ticket, - ), - }; - - zcf.reallocate( - [auditoriumOfferHandle, buyerOfferHandle], - [wantedAuditoriumAllocation, wantedBuyerAllocation], - ); - zcf.complete([buyerOfferHandle]); - } catch (err) { - // amounts don't match or reallocate certainly failed - rejectOffer(buyerOfferHandle); - } - }; - - const buyTicketExpected = harden({ - want: { Ticket: null }, - give: { Money: null }, - }); - - return harden({ - invite: zcf.makeInvitation(auditoriumOfferHook, 'auditorium'), - publicAPI: { - makeBuyerInvite: () => - zcf.makeInvitation( - checkHook(buyTicketOfferHook, buyTicketExpected), - 'buy ticket', - ), - getTicketIssuer: () => issuer, - getAvailableTickets() { - // Because of a technical limitation in @agoric/marshal, an array of extents - // is better than a Map https://github.com/Agoric/agoric-sdk/issues/838 - return zcf.getCurrentAllocation(auditoriumOfferHandle).Ticket - .extent; - }, - }, - }); - }); - }, -); diff --git a/packages/zoe/src/contracts/publicAuction.js b/packages/zoe/src/contracts/publicAuction.js index 31a69720cf0..5d3398a90ad 100644 --- a/packages/zoe/src/contracts/publicAuction.js +++ b/packages/zoe/src/contracts/publicAuction.js @@ -11,112 +11,139 @@ import { closeAuction, } from '../contractSupport'; -/** @typedef {import('../zoe').ContractFacet} ContractFacet */ - -// zcf is the Zoe Contract Facet, i.e. the contract-facing API of Zoe -export const makeContract = harden( - /** @param {ContractFacet} zcf */ zcf => { - const { - rejectOffer, - canTradeWith, - assertKeywords, - checkHook, - } = makeZoeHelpers(zcf); - - let { - terms: { numBidsAllowed }, - } = zcf.getInstanceRecord(); - numBidsAllowed = Nat(numBidsAllowed !== undefined ? numBidsAllowed : 3); - - let sellerOfferHandle; - let minimumBid; - let auctionedAssets; - const allBidHandles = []; - - assertKeywords(harden(['Asset', 'Bid'])); - - const bidderOfferHook = offerHandle => { - // Check that the item is still up for auction - if (!zcf.isOfferActive(sellerOfferHandle)) { - const rejectMsg = `The item up for auction is not available or the auction has completed`; - throw rejectOffer(offerHandle, rejectMsg); - } - if (allBidHandles.length >= numBidsAllowed) { - throw rejectOffer(offerHandle, `No further bids allowed.`); - } - if (!canTradeWith(sellerOfferHandle, offerHandle)) { - const rejectMsg = `Bid was under minimum bid or for the wrong assets`; - throw rejectOffer(offerHandle, rejectMsg); - } - - // Save valid bid and try to close. - allBidHandles.push(offerHandle); - if (allBidHandles.length >= numBidsAllowed) { - closeAuction(zcf, { - auctionLogicFn: secondPriceLogic, - sellerOfferHandle, - allBidHandles, - }); - } - return defaultAcceptanceMsg; - }; - - const bidderOfferExpected = harden({ - give: { Bid: null }, - want: { Asset: null }, - }); +/** + * An auction contract in which the seller offers an Asset for sale, and states + * a minimum price. A pre-announced number of bidders compete to offer the best + * price. When the appropriate number of bids have been received, the second + * price rule is followed, so the highest bidder pays the amount bid by the + * second highest bidder. + * + * makeInstance() specifies the issuers and terms ({ numBidsAllowed }) specify + * the number of bids required. An invitation for the seller is returned. The + * seller's offer should look like + * { give: { Asset: asset }, want: { Ask: minimumBidAmount } } + * The asset can be non-fungible, but the Ask amount should be of a fungible + * brand. + * The bidder invitations are available from publicAPI.makeInvites(n). Each + * bidder can submit an offer: { give: { Bid: null } want: { Asset: null } }. + * + * publicAPI also has methods to find out what's being auctioned + * (getAuctionedAssetsAmounts()), or the minimum bid (getMinimumBid()). + * + * @typedef {import('../zoe').ContractFacet} ContractFacet + * @param {ContractFacet} zcf + */ +const makeContract = zcf => { + const { rejectOffer, satisfies, assertKeywords, checkHook } = makeZoeHelpers( + zcf, + ); + + let { + terms: { numBidsAllowed }, + } = zcf.getInstanceRecord(); + numBidsAllowed = Nat(numBidsAllowed !== undefined ? numBidsAllowed : 3); + + let sellerOfferHandle; + let minimumBid; + let auctionedAssets; + const allBidHandles = []; + + // seller will use 'Asset' and 'Ask'. buyer will use 'Asset' and 'Bid' + assertKeywords(harden(['Asset', 'Ask'])); - const makeBidderInvite = () => - zcf.makeInvitation( - checkHook(bidderOfferHook, bidderOfferExpected), - 'bid', - harden({ - customProperties: { - auctionedAssets, - minimumBid, - }, - }), - ); - - const sellerOfferHook = offerHandle => { - if (auctionedAssets) { - throw rejectOffer(offerHandle, `assets already present`); - } - // Save the valid offer - sellerOfferHandle = offerHandle; - const { proposal } = zcf.getOffer(offerHandle); - auctionedAssets = proposal.give.Asset; - minimumBid = proposal.want.Bid; - return defaultAcceptanceMsg; - }; - - const sellerOfferExpected = harden({ - give: { Asset: null }, - want: { Bid: null }, + const bidderOfferHook = offerHandle => { + // Check that the item is still up for auction + if (!zcf.isOfferActive(sellerOfferHandle)) { + const rejectMsg = `The item up for auction is not available or the auction has completed`; + throw rejectOffer(offerHandle, rejectMsg); + } + if (allBidHandles.length >= numBidsAllowed) { + throw rejectOffer(offerHandle, `No further bids allowed.`); + } + const sellerSatisfied = satisfies(sellerOfferHandle, { + Ask: zcf.getCurrentAllocation(offerHandle).Bid, + Asset: zcf.getAmountMath(auctionedAssets.brand).getEmpty(), }); + const bidderSatisfied = satisfies(offerHandle, { + Asset: zcf.getCurrentAllocation(sellerOfferHandle).Asset, + Bid: zcf.getAmountMath(minimumBid.brand).getEmpty(), + }); + if (!(sellerSatisfied && bidderSatisfied)) { + const rejectMsg = `Bid was under minimum bid or for the wrong assets`; + throw rejectOffer(offerHandle, rejectMsg); + } + + // Save valid bid and try to close. + allBidHandles.push(offerHandle); + if (allBidHandles.length >= numBidsAllowed) { + closeAuction(zcf, { + auctionLogicFn: secondPriceLogic, + sellerOfferHandle, + allBidHandles, + }); + } + return defaultAcceptanceMsg; + }; + + const bidderOfferExpected = harden({ + give: { Bid: null }, + want: { Asset: null }, + }); - const makeSellerInvite = () => - zcf.makeInvitation( - checkHook(sellerOfferHook, sellerOfferExpected), - 'sellAssets', - ); - - return harden({ - invite: makeSellerInvite(), - publicAPI: { - makeInvites: numInvites => { - if (auctionedAssets === undefined) { - throw new Error(`No assets are up for auction.`); - } - const invites = []; - for (let i = 0; i < numInvites; i += 1) { - invites.push(makeBidderInvite()); - } - return invites; + const makeBidderInvite = () => + zcf.makeInvitation( + checkHook(bidderOfferHook, bidderOfferExpected), + 'bid', + harden({ + customProperties: { + auctionedAssets, + minimumBid, }, - getAuctionedAssetsAmounts: () => auctionedAssets, - getMinimumBid: () => minimumBid, + }), + ); + + const sellerOfferHook = offerHandle => { + if (auctionedAssets) { + throw rejectOffer(offerHandle, `assets already present`); + } + // Save the valid offer + sellerOfferHandle = offerHandle; + const { proposal } = zcf.getOffer(offerHandle); + auctionedAssets = proposal.give.Asset; + minimumBid = proposal.want.Ask; + return defaultAcceptanceMsg; + }; + + const sellerOfferExpected = harden({ + give: { Asset: null }, + want: { Ask: null }, + }); + + const makeSellerInvite = () => + zcf.makeInvitation( + checkHook(sellerOfferHook, sellerOfferExpected), + 'sellAssets', + ); + + zcf.initPublicAPI( + harden({ + makeInvites: numInvites => { + if (auctionedAssets === undefined) { + throw new Error(`No assets are up for auction.`); + } + const invites = []; + for (let i = 0; i < numInvites; i += 1) { + invites.push(makeBidderInvite()); + } + return invites; }, - }); - }, -); + getAuctionedAssetsAmounts: () => auctionedAssets, + getMinimumBid: () => minimumBid, + }), + ); + + return makeSellerInvite(); +}; + +harden(makeContract); +export { makeContract }; diff --git a/packages/zoe/src/contracts/sellItems.js b/packages/zoe/src/contracts/sellItems.js new file mode 100644 index 00000000000..aaeefccbb2c --- /dev/null +++ b/packages/zoe/src/contracts/sellItems.js @@ -0,0 +1,129 @@ +/* eslint-disable no-use-before-define */ +// @ts-check + +import harden from '@agoric/harden'; +import { assert, details } from '@agoric/assert'; +import { makeZoeHelpers, defaultAcceptanceMsg } from '../contractSupport'; + +/** @typedef {import('../zoe').ContractFacet} ContractFacet */ + +/** + * Sell items in exchange for money. Items may be fungible or + * non-fungible and multiple items may be bought at once. Money must + * be fungible. + * + * The `pricePerItem` is to be set in the terms. It is expected that all items + * are sold for the same uniform price. + * + * The initial offer should be { give: { Items: items } }, accompanied by + * terms as described above. + * Buyers use offers that match { want: { Items: items } give: { Money: m } }. + * The items provided should match particular items that the seller still has + * available to sell, and the money should be pricePerItem times the number of + * items requested. + * + * @typedef {import('../zoe').ContractFacet} ContractFacet + * @param {ContractFacet} zcf + */ +const makeContract = zcf => { + const allKeywords = ['Items', 'Money']; + const { + assertKeywords, + rejectOffer, + checkHook, + assertNatMathHelpers, + trade, + } = makeZoeHelpers(zcf); + assertKeywords(harden(allKeywords)); + + const { pricePerItem } = zcf.getInstanceRecord().terms; + assertNatMathHelpers(pricePerItem.brand); + let sellerOfferHandle; + + const sellerOfferHook = offerHandle => { + sellerOfferHandle = offerHandle; + return defaultAcceptanceMsg; + }; + + const buyerOfferHook = buyerOfferHandle => { + const { brandKeywordRecord } = zcf.getInstanceRecord(); + const [sellerAllocation, buyerAllocation] = zcf.getCurrentAllocations( + [sellerOfferHandle, buyerOfferHandle], + [brandKeywordRecord, brandKeywordRecord], + ); + const currentItemsForSale = sellerAllocation.Items; + const providedMoney = buyerAllocation.Money; + + const { proposal } = zcf.getOffer(buyerOfferHandle); + const wantedItems = proposal.want.Items; + const numItemsWanted = wantedItems.extent.length; + const totalCostExtent = pricePerItem.extent * numItemsWanted; + const moneyAmountMaths = zcf.getAmountMath(pricePerItem.brand); + const itemsAmountMath = zcf.getAmountMath(wantedItems.brand); + + const totalCost = moneyAmountMaths.make(totalCostExtent); + + // Check that the wanted items are still for sale. + if (!itemsAmountMath.isGTE(currentItemsForSale, wantedItems)) { + return rejectOffer( + buyerOfferHandle, + `Some of the wanted items were not available for sale`, + ); + } + + // Check that the money provided to pay for the items is greater than the totalCost. + if (!moneyAmountMaths.isGTE(providedMoney, totalCost)) { + return rejectOffer( + buyerOfferHandle, + `More money (${totalCost}) is required to buy these items`, + ); + } + + // Reallocate. We are able to trade by only defining the gains + // (omitting the losses) because the keywords for both offers are + // the same, so the gains for one offer are the losses for the + // other. + trade( + { offerHandle: sellerOfferHandle, gains: { Money: providedMoney } }, + { offerHandle: buyerOfferHandle, gains: { Items: wantedItems } }, + ); + + // Complete the buyer offer. + zcf.complete([buyerOfferHandle]); + return defaultAcceptanceMsg; + }; + + const buyerExpected = harden({ + want: { Items: null }, + give: { Money: null }, + }); + + zcf.initPublicAPI( + harden({ + makeBuyerInvite: () => { + const itemsAmount = zcf.getCurrentAllocation(sellerOfferHandle).Items; + const itemsAmountMath = zcf.getAmountMath(itemsAmount.brand); + assert( + sellerOfferHandle && !itemsAmountMath.isEmpty(itemsAmount), + details`no items are for sale`, + ); + return zcf.makeInvitation( + checkHook(buyerOfferHook, buyerExpected), + 'buyer', + ); + }, + getAvailableItems: () => { + if (!sellerOfferHandle) { + throw new Error(`no items have been escrowed`); + } + return zcf.getCurrentAllocation(sellerOfferHandle).Items; + }, + getItemsIssuer: () => zcf.getInstanceRecord().issuerKeywordRecord.Items, + }), + ); + + return zcf.makeInvitation(sellerOfferHook, 'seller'); +}; + +harden(makeContract); +export { makeContract }; diff --git a/packages/zoe/src/contracts/simpleExchange.js b/packages/zoe/src/contracts/simpleExchange.js index fa5a885cbe0..7aea6f7971c 100644 --- a/packages/zoe/src/contracts/simpleExchange.js +++ b/packages/zoe/src/contracts/simpleExchange.js @@ -5,13 +5,13 @@ import { produceNotifier } from '@agoric/notifier'; import { makeZoeHelpers, defaultAcceptanceMsg } from '../contractSupport'; /** - * @typedef {import('../zoe').ContractFacet} ContractFacet - */ - -/** - * The SimpleExchange uses Asset and Price as its keywords. In usage, - * they're somewhat symmetrical. Participants will be buying or - * selling in both directions. + * SimpleExchange is an exchange with a simple matching algorithm, which allows + * an unlimited number of parties to create new orders or accept existing + * orders. The notifier allows callers to find the current list of orders. + * + * The SimpleExchange uses Asset and Price as its keywords. The contract treats + * the two keywords symmetrically. New offers can be created and existing offers + * can be accepted in either direction. * * { give: { 'Asset', simoleans(5) }, want: { 'Price', quatloos(3) } } * { give: { 'Price', quatloos(8) }, want: { 'Asset', simoleans(3) } } @@ -19,130 +19,143 @@ import { makeZoeHelpers, defaultAcceptanceMsg } from '../contractSupport'; * The Asset is treated as an exact amount to be exchanged, while the * Price is a limit that may be improved on. This simple exchange does * not partially fill orders. + * + * The invitation returned on installation of the contract is the same as what + * is returned by calling `publicAPI.makeInvite(). + * + * @typedef {import('../zoe').ContractFacet} ContractFacet + * @param {ContractFacet} zcf */ -export const makeContract = harden( - /** @param {ContractFacet} zcf */ zcf => { - let sellOfferHandles = []; - let buyOfferHandles = []; - const { notifier, updater } = produceNotifier(); - - const { - rejectOffer, - checkIfProposal, - swap, - canTradeWith, - getActiveOffers, - assertKeywords, - } = makeZoeHelpers(zcf); - - assertKeywords(harden(['Asset', 'Price'])); - - function flattenOffer(o) { - return { - want: o.proposal.want, - give: o.proposal.give, - }; - } - - function flattenOrders(offerHandles) { - const result = zcf - .getOffers(zcf.getOfferStatuses(offerHandles).active) - .map(offerRecord => flattenOffer(offerRecord)); - return result; - } - - function getBookOrders() { - return { - buys: flattenOrders(buyOfferHandles), - sells: flattenOrders(sellOfferHandles), - }; - } +const makeContract = zcf => { + let sellOfferHandles = []; + let buyOfferHandles = []; + const { notifier, updater } = produceNotifier(); + + const { + rejectOffer, + checkIfProposal, + swap, + satisfies, + getActiveOffers, + assertKeywords, + } = makeZoeHelpers(zcf); + + assertKeywords(harden(['Asset', 'Price'])); + + function flattenOffer(o) { + return { + want: o.proposal.want, + give: o.proposal.give, + }; + } + + function flattenOrders(offerHandles) { + const result = zcf + .getOffers(zcf.getOfferStatuses(offerHandles).active) + .map(offerRecord => flattenOffer(offerRecord)); + return result; + } + + function getBookOrders() { + return { + buys: flattenOrders(buyOfferHandles), + sells: flattenOrders(sellOfferHandles), + }; + } - function getOffer(offerHandle) { - for (const handle of [...sellOfferHandles, ...buyOfferHandles]) { - if (offerHandle === handle) { - return flattenOffer(getActiveOffers([offerHandle])[0]); - } + function getOffer(offerHandle) { + for (const handle of [...sellOfferHandles, ...buyOfferHandles]) { + if (offerHandle === handle) { + return flattenOffer(getActiveOffers([offerHandle])[0]); } - return 'not an active offer'; - } - - // Tell the notifier that there has been a change to the book orders - function bookOrdersChanged() { - updater.updateState(getBookOrders()); } - - // If there's an existing offer that this offer is a match for, make the trade - // and return the handle for the matched offer. If not, return undefined, so - // the caller can know to add the new offer to the book. - function swapIfCanTrade(offerHandles, offerHandle) { - for (const iHandle of offerHandles) { - if (canTradeWith(offerHandle, iHandle)) { - swap(offerHandle, iHandle); - // return handle to remove - return iHandle; - } + return 'not an active offer'; + } + + // Tell the notifier that there has been a change to the book orders + function bookOrdersChanged() { + updater.updateState(getBookOrders()); + } + + // If there's an existing offer that this offer is a match for, make the trade + // and return the handle for the matched offer. If not, return undefined, so + // the caller can know to add the new offer to the book. + function swapIfCanTrade(offerHandles, offerHandle) { + for (const iHandle of offerHandles) { + const satisfiedBy = (xHandle, yHandle) => + satisfies(xHandle, zcf.getCurrentAllocation(yHandle)); + if ( + satisfiedBy(iHandle, offerHandle) && + satisfiedBy(offerHandle, iHandle) + ) { + swap(offerHandle, iHandle); + // return handle to remove + return iHandle; } - return undefined; } - - // try to swap offerHandle with one of the counterOffers. If it works, remove - // the matching offer and return the remaining counterOffers. If there's no - // matching offer, add the offerHandle to the coOffers, and return the - // unmodified counterOfffers - function swapIfCanTradeAndUpdateBook(counterOffers, coOffers, offerHandle) { - const handle = swapIfCanTrade(counterOffers, offerHandle); - if (handle) { - // remove the matched offer. - counterOffers = counterOffers.filter(value => value !== handle); - } else { - // Save the order in the book - coOffers.push(offerHandle); - } - - return counterOffers; + return undefined; + } + + // try to swap offerHandle with one of the counterOffers. If it works, remove + // the matching offer and return the remaining counterOffers. If there's no + // matching offer, add the offerHandle to the coOffers, and return the + // unmodified counterOfffers + function swapIfCanTradeAndUpdateBook(counterOffers, coOffers, offerHandle) { + const handle = swapIfCanTrade(counterOffers, offerHandle); + if (handle) { + // remove the matched offer. + counterOffers = counterOffers.filter(value => value !== handle); + } else { + // Save the order in the book + coOffers.push(offerHandle); } - const exchangeOfferHook = offerHandle => { - const buyAssetForPrice = harden({ - give: { Price: null }, - want: { Asset: null }, - }); - const sellAssetForPrice = harden({ - give: { Asset: null }, - want: { Price: null }, - }); - if (checkIfProposal(offerHandle, sellAssetForPrice)) { - buyOfferHandles = swapIfCanTradeAndUpdateBook( - buyOfferHandles, - sellOfferHandles, - offerHandle, - ); - /* eslint-disable no-else-return */ - } else if (checkIfProposal(offerHandle, buyAssetForPrice)) { - sellOfferHandles = swapIfCanTradeAndUpdateBook( - sellOfferHandles, - buyOfferHandles, - offerHandle, - ); - } else { - // Eject because the offer must be invalid - return rejectOffer(offerHandle); - } - bookOrdersChanged(); - return defaultAcceptanceMsg; - }; - - const makeExchangeInvite = () => - zcf.makeInvitation(exchangeOfferHook, 'exchange'); + return counterOffers; + } - return harden({ - invite: makeExchangeInvite(), - publicAPI: { - makeInvite: makeExchangeInvite, - getOffer, - getNotifier: () => notifier, - }, + const exchangeOfferHook = offerHandle => { + const buyAssetForPrice = harden({ + give: { Price: null }, + want: { Asset: null }, + }); + const sellAssetForPrice = harden({ + give: { Asset: null }, + want: { Price: null }, }); - }, -); + if (checkIfProposal(offerHandle, sellAssetForPrice)) { + buyOfferHandles = swapIfCanTradeAndUpdateBook( + buyOfferHandles, + sellOfferHandles, + offerHandle, + ); + /* eslint-disable no-else-return */ + } else if (checkIfProposal(offerHandle, buyAssetForPrice)) { + sellOfferHandles = swapIfCanTradeAndUpdateBook( + sellOfferHandles, + buyOfferHandles, + offerHandle, + ); + } else { + // Eject because the offer must be invalid + return rejectOffer(offerHandle); + } + bookOrdersChanged(); + return defaultAcceptanceMsg; + }; + + const makeExchangeInvite = () => + zcf.makeInvitation(exchangeOfferHook, 'exchange'); + + zcf.initPublicAPI( + harden({ + makeInvite: makeExchangeInvite, + getOffer, + getNotifier: () => notifier, + }), + ); + + return makeExchangeInvite(); +}; + +harden(makeContract); +export { makeContract }; diff --git a/packages/zoe/src/objArrayConversion.js b/packages/zoe/src/objArrayConversion.js index 572ecf0e263..cc90337265d 100644 --- a/packages/zoe/src/objArrayConversion.js +++ b/packages/zoe/src/objArrayConversion.js @@ -31,32 +31,13 @@ export const assertSubset = (whole, part) => { }); }; -// Keywords must equal the keys of obj -export const objToArrayAssertFilled = (obj, keywords) => { - const keys = Object.getOwnPropertyNames(obj); - assert( - keys.length === keywords.length, - details`object keys ${q(keys)} and keywords ${q( - keywords, - )} must be of equal length`, - ); - assertSubset(keywords, keys); - // ensure all keywords are defined on obj - return keywords.map(keyword => { - assert( - obj[keyword] !== undefined, - details`obj[keyword] must be defined for keyword ${q(keyword)}`, - ); - return obj[keyword]; - }); -}; - -// return a new object with only the keys in subsetKeywords. `obj` -// must have values for all the `subsetKeywords`. -export const filterObj = /** @type {function(T, string[]): T} */ ( - obj, - subsetKeywords, -) => { +/** + * Return a new object with only the keys in subsetKeywords. + * `obj` must have values for all the `subsetKeywords`. + * @param {Object} obj + * @param {import('./zoe').Keyword[]} subsetKeywords + */ +export const filterObj = (obj, subsetKeywords) => { const newObj = {}; subsetKeywords.forEach(keyword => { assert( @@ -68,41 +49,22 @@ export const filterObj = /** @type {function(T, string[]): T} */ ( return newObj; }; -// return a new object with only the keys in subsetKeywords. `obj` -// is allowed to not include keywords in subsetKeywords. -export const filterObjOkIfMissing = /** @type {function(T, string[]): T} */ ( - obj, - subsetKeywords, -) => { - const newObj = {}; - subsetKeywords.forEach(keyword => { - if (obj[keyword] !== undefined) { - newObj[keyword] = obj[keyword]; - } - }); - return newObj; -}; - /** - * @typedef {import('./zoe').Allocation} Allocation - * @typedef {import('@agoric/ertp/src/amountMath').AmountMath} AmountMath - * @typedef {{[Keyword:string]:AmountMath}} KeywordAmountMathRecord - */ - -// return a new object with only the keys in subsetKeywords, but fill -// in empty amounts for any key that is undefined in the original obj -export const filterFillAmounts = /** @type {function(Allocation, string[], KeywordAmountMathRecord):Allocation} */ ( - obj, - subsetKeywords, - amountMathKeywordRecord, -) => { - const newObj = {}; + * Return a new object with only the keys in `amountMathKeywordRecord`, but fill + * in empty amounts for any key that is undefined in the original allocation + * @param {import('./zoe').Allocation} allocation + * @param {import('./zoe').AmountMathKeywordRecord} amountMathKeywordRecord + * @returns Allocation + * */ +export const filterFillAmounts = (allocation, amountMathKeywordRecord) => { + const filledAllocation = {}; + const subsetKeywords = Object.getOwnPropertyNames(amountMathKeywordRecord); subsetKeywords.forEach(keyword => { - if (obj[keyword] === undefined) { - newObj[keyword] = amountMathKeywordRecord[keyword].getEmpty(); + if (allocation[keyword] === undefined) { + filledAllocation[keyword] = amountMathKeywordRecord[keyword].getEmpty(); } else { - newObj[keyword] = obj[keyword]; + filledAllocation[keyword] = allocation[keyword]; } }); - return newObj; + return filledAllocation; }; diff --git a/packages/zoe/src/offerSafety.js b/packages/zoe/src/offerSafety.js index 6c772c56eef..b9d53ec36e6 100644 --- a/packages/zoe/src/offerSafety.js +++ b/packages/zoe/src/offerSafety.js @@ -1,45 +1,97 @@ /** - * `isOfferSafeForOffer` checks offer safety for a single offer. - * - * Note: This implementation checks whether we refund for all rules or - * return winnings for all rules. It does not allow some refunds and - * some winnings, which is what would happen if you checked the rules - * independently. It *does* allow for returning a full refund plus - * full winnings. - * - * @param {object} amountMathKeywordRecord - a record with keywords as - * keys and amountMath as values - * @param {object} proposal - the rules that accompanied the - * escrow of payments that dictate what the user expected to get back - * from Zoe. A proposal is a record with keys `give`, - * `want`, and `exit`. `give` and `want` are records with keywords - * as keys and amounts as values. The proposal is a player's - * understanding of the contract that they are entering when they make - * an offer. - * @param {object} newAmountKeywordRecord - a record with keywords as keys and - * amounts as values. These amounts are the reallocation to be given to a user. + * @typedef {import('@agoric/ertp/src/issuer').Brand} Brand + * @typedef {import('@agoric/ertp/src/issuer').AmountMath} AmountMath + * @typedef {import('./zoe').Proposal} Proposal + * @typedef {import('./zoe').AmountKeywordRecord} AmountKeywordRecord + */ + +/** + * Helper to perform satisfiesWant and satisfiesGive. Is + * allocationAmount greater than or equal to requiredAmount for every + * keyword of giveOrWant? + * @param {(Brand) => AmountMath} getAmountMath + * @param {Proposal["give"] | Proposal["want"]} giveOrWant + * @param {AmountKeywordRecord} allocation + */ +const satisfiesInternal = (getAmountMath, giveOrWant, allocation) => { + const isGTEByKeyword = ([keyword, requiredAmount]) => { + // If there is no allocation for a keyword, we know the giveOrWant + // is not satisfied without checking further. + if (allocation[keyword] === undefined) { + return false; + } + const amountMath = getAmountMath(requiredAmount.brand); + const allocationAmount = allocation[keyword]; + return amountMath.isGTE(allocationAmount, requiredAmount); + }; + return Object.entries(giveOrWant).every(isGTEByKeyword); +}; + +/** + * For this allocation to satisfy what the user wanted, their + * allocated amounts must be greater than or equal to proposal.want. + * @param {(Brand) => AmountMath} getAmountMath - a function that + * takes a brand and returns the appropriate amountMath. The function + * must have an amountMath for every brand in proposal.want. + * @param {Proposal} proposal - the rules that accompanied the escrow + * of payments that dictate what the user expected to get back from + * Zoe. A proposal is a record with keys `give`, `want`, and `exit`. + * `give` and `want` are records with keywords as keys and amounts as + * values. The proposal is a user's understanding of the contract that + * they are entering when they make an offer. + * @param {AmountKeywordRecord} allocation - a record with keywords + * as keys and amounts as values. These amounts are the reallocation + * to be given to a user. */ -function isOfferSafeForOffer( - amountMathKeywordRecord, - proposal, - newAmountKeywordRecord, -) { - const isGTEByKeyword = ([keyword, amount]) => - amountMathKeywordRecord[keyword].isGTE( - newAmountKeywordRecord[keyword], - amount, - ); +const satisfiesWant = (getAmountMath, proposal, allocation) => + satisfiesInternal(getAmountMath, proposal.want, allocation); - // For this allocation to count as a full refund, the allocated - // amount must be greater than or equal to what was originally - // offered. - const refundOk = Object.entries(proposal.give).every(isGTEByKeyword); +/** + * For this allocation to count as a full refund, the allocated + * amounts must be greater than or equal to what was originally + * offered (proposal.give). + * @param {(Brand) => AmountMath} getAmountMath - a function that + * takes a brand and returns the appropriate amountMath. The function + * must have an amountMath for every brand in proposal.give. + * @param {Proposal} proposal - the rules that accompanied the escrow + * of payments that dictate what the user expected to get back from + * Zoe. A proposal is a record with keys `give`, `want`, and `exit`. + * `give` and `want` are records with keywords as keys and amounts as + * values. The proposal is a user's understanding of the contract that + * they are entering when they make an offer. + * @param {AmountKeywordRecord} allocation - a record with keywords + * as keys and amounts as values. These amounts are the reallocation + * to be given to a user. + */ +const satisfiesGive = (getAmountMath, proposal, allocation) => + satisfiesInternal(getAmountMath, proposal.give, allocation); - // For this allocation to count as a full payout of what the user - // wanted, their allocated amount must be greater than or equal to - // what the payoutRules said they wanted. - const winningsOk = Object.entries(proposal.want).every(isGTEByKeyword); - return refundOk || winningsOk; +/** + * `isOfferSafe` checks offer safety for a single offer. + * + * Note: This implementation checks whether we fully satisfy + * `proposal.give` (giving a refund) or whether we fully satisfy + * `proposal.want`. Both can be fully satisfied. + * + * @param {(Brand) => AmountMath} getAmountMath - a function that + * takes a brand and returns the appropriate amountMath. The function + * must have an amountMath for every brand in proposal.want and + * proposal.give. + * @param {Proposal} proposal - the rules that accompanied the escrow + * of payments that dictate what the user expected to get back from + * Zoe. A proposal is a record with keys `give`, `want`, and `exit`. + * `give` and `want` are records with keywords as keys and amounts as + * values. The proposal is a user's understanding of the contract that + * they are entering when they make an offer. + * @param {AmountKeywordRecord} allocation - a record with keywords + * as keys and amounts as values. These amounts are the reallocation + * to be given to a user. + */ +function isOfferSafe(getAmountMath, proposal, allocation) { + return ( + satisfiesGive(getAmountMath, proposal, allocation) || + satisfiesWant(getAmountMath, proposal, allocation) + ); } -export { isOfferSafeForOffer }; +export { isOfferSafe, satisfiesWant }; diff --git a/packages/zoe/src/rightsConservation.js b/packages/zoe/src/rightsConservation.js index 5aeedb99dc6..9be99bf79cc 100644 --- a/packages/zoe/src/rightsConservation.js +++ b/packages/zoe/src/rightsConservation.js @@ -1,63 +1,78 @@ +import makeStore from '@agoric/store'; +import { assert, details } from '@agoric/assert'; + /** - * Transpose an array of arrays - * @param {matrix} matrix + * @typedef {import('@agoric/ertp').Amount} Amount + * @typedef {import('@agoric/ertp').Brand} Brand + * @typedef {import('@agoric/ertp').AmountMath} AmountMath + * @typedef {import('@agoric/store').Store} Store */ -// https://stackoverflow.com/questions/17428587/transposing-a-2d-array-in-javascript/41772644#41772644 -const transpose = matrix => - matrix.reduce( - (acc, row) => row.map((_, i) => [...(acc[i] || []), row[i]]), - [], - ); /** - * The columns in an `amount` matrix are per issuer, and the rows - * are per offer. We want to transpose the matrix such that each - * row is per issuer so we can do 'with' on the array to get a total - * per issuer and make sure the rights are conserved. - * @param {amountMath[]} amountMathArray - an array of amountMath per issuer - * @param {amount[][]} amountMatrix - an array of arrays with a row per - * offer indexed by issuer + * Iterate over the amounts and sum, storing the sums in a + * map by brand. + * @param {(brand: Brand) => AmountMath} getAmountMath - a function + * to get amountMath given a brand. + * @param {Amount[]} amounts - an array of amounts + * @returns {Store} sumsByBrand - a map of Brand keys and + * Amount values. The amounts are the sums. */ -const sumByIssuer = (amountMathArray, amountMatrix) => - transpose(amountMatrix).map((amountPerIssuer, i) => - amountPerIssuer.reduce( - amountMathArray[i].add, - amountMathArray[i].getEmpty(), - ), - ); +const sumByBrand = (getAmountMath, amounts) => { + const sumsByBrand = makeStore('brand'); + amounts.forEach(amount => { + const { brand } = amount; + const amountMath = getAmountMath(brand); + if (!sumsByBrand.has(brand)) { + sumsByBrand.init(brand, amountMath.getEmpty()); + } + const sumSoFar = sumsByBrand.get(brand); + sumsByBrand.set(brand, amountMath.add(sumSoFar, amount)); + }); + return sumsByBrand; +}; /** - * Does the left array of summed amount equal the right array of - * summed amount? - * @param {amountMath[]} amountMathArray - an array of amountMath per issuer - * @param {amount[]} leftAmounts- an array of total amount per issuer - * @param {amount[]} rightAmounts - an array of total amount per issuer + * Do the left sums by brand equal the right sums by brand? + * @param {(brand: Brand) => AmountMath} getAmountMath - a function + * to get amountMath given a brand. + * @param {Store} leftSumsByBrand - a map of brands to sums + * @param {Store} rightSumsByBrand - a map of brands to sums * indexed by issuer */ -const isEqualPerIssuer = (amountMathArray, leftAmounts, rightAmounts) => - leftAmounts.every((leftAmount, i) => - amountMathArray[i].isEqual(leftAmount, rightAmounts[i]), +const isEqualPerBrand = (getAmountMath, leftSumsByBrand, rightSumsByBrand) => { + const leftKeys = leftSumsByBrand.keys(); + const rightKeys = rightSumsByBrand.keys(); + assert.equal( + leftKeys.length, + rightKeys.length, + details`${leftKeys.length} should be equal to ${rightKeys.length}`, ); + return leftSumsByBrand + .keys() + .every(brand => + getAmountMath(brand).isEqual( + leftSumsByBrand.get(brand), + rightSumsByBrand.get(brand), + ), + ); +}; /** - * `areRightsConserved` checks that the total amount per issuer stays - * the same regardless of the reallocation. - * @param {amountMath[]} amountMathArray - an array of amountMath per issuer - * @param {amount[][]} previousAmountsMatrix - array of arrays where a row - * is the array of amount for a particular offer, per - * issuer - * @param {amount[][]} newAmountsMatrix - array of arrays where a row - * is the array of reallocated amount for a particular offer, per - * issuer + * `areRightsConserved` checks that the total amount per brand is + * equal to the total amount per brand in the proposed reallocation + * @param {(brand: Brand) => AmountMath} getAmountMath - a function + * to get amountMath given a brand. + * @param {Amount[]} previousAmounts - an array of the amounts before the + * proposed reallocation + * @param {Amount[]} newAmounts - an array of the amounts in the + * proposed reallocation + * + * @returns {boolean} isEqualPerBrand */ -function areRightsConserved( - amountMathArray, - previousAmountsMatrix, - newAmountsMatrix, -) { - const sumsPrevAmounts = sumByIssuer(amountMathArray, previousAmountsMatrix); - const sumsNewAmounts = sumByIssuer(amountMathArray, newAmountsMatrix); - return isEqualPerIssuer(amountMathArray, sumsPrevAmounts, sumsNewAmounts); +function areRightsConserved(getAmountMath, previousAmounts, newAmounts) { + const sumsPrevAmounts = sumByBrand(getAmountMath, previousAmounts); + const sumsNewAmounts = sumByBrand(getAmountMath, newAmounts); + return isEqualPerBrand(getAmountMath, sumsPrevAmounts, sumsNewAmounts); } -export { areRightsConserved, transpose }; +export { areRightsConserved }; diff --git a/packages/zoe/src/state.js b/packages/zoe/src/state.js index 1850c3509d8..65f39991ce7 100644 --- a/packages/zoe/src/state.js +++ b/packages/zoe/src/state.js @@ -5,6 +5,11 @@ import makeStore from '@agoric/weak-store'; import makeAmountMath from '@agoric/ertp/src/amountMath'; import { makeTable, makeValidateProperties } from './table'; +/** + * @typedef {import('./zoe').OfferHandle} OfferHandle + * @typedef {import('@agoric/ertp').Payment} Payment + */ + // Installation Table // Columns: handle | installation | bundle const makeInstallationTable = () => { @@ -15,12 +20,19 @@ const makeInstallationTable = () => { }; // Instance Table -// Columns: handle | installationHandle | publicAPI | terms | issuerKeywordRecord +// Columns: handle | installationHandle | publicAPI | terms | +// issuerKeywordRecord | brandKeywordRecord const makeInstanceTable = () => { // TODO: make sure this validate function protects against malicious // misshapen objects rather than just a general check. const validateSomewhat = makeValidateProperties( - harden(['installationHandle', 'publicAPI', 'terms', 'issuerKeywordRecord']), + harden([ + 'installationHandle', + 'publicAPI', + 'terms', + 'issuerKeywordRecord', + 'brandKeywordRecord', + ]), ); return makeTable(validateSomewhat); @@ -92,8 +104,12 @@ const makeOfferTable = () => { }; // Payout Map -// PrivateName: offerHandle | payoutPromise -const makePayoutMap = makeStore; +/** + * Create payoutMap + * @returns {import('@agoric/store').Store>} Store + */ +const makePayoutMap = () => makeStore('offerHandle'); // Issuer Table // Columns: brand | issuer | purse | amountMath diff --git a/packages/zoe/src/zoe.js b/packages/zoe/src/zoe.js index 964ef12930c..bd8186c6cf8 100644 --- a/packages/zoe/src/zoe.js +++ b/packages/zoe/src/zoe.js @@ -13,20 +13,13 @@ import { getKeywords, cleanKeywords, } from './cleanProposal'; -import { - arrayToObj, - objToArray, - objToArrayAssertFilled, - filterObj, - filterFillAmounts, - assertSubset, -} from './objArrayConversion'; -import { isOfferSafeForOffer } from './offerSafety'; +import { arrayToObj, filterFillAmounts, filterObj } from './objArrayConversion'; +import { isOfferSafe } from './offerSafety'; import { areRightsConserved } from './rightsConservation'; import { evalContractBundle } from './evalContractCode'; import { makeTables } from './state'; -// TODO Update types and documentatuon to describe the new API +// TODO Update types and documentation to describe the new API /** * Zoe uses ERTP, the Electronic Rights Transfer Protocol */ @@ -39,10 +32,16 @@ import { makeTables } from './state'; * @typedef {import('@agoric/ertp/src/issuer').Issuer} Issuer * @typedef {import('@agoric/ertp/src/issuer').Purse} Purse * + * @typedef {import('./rightsConservation').areRightsConserved} areRightsConserved + * * @typedef {any} TODO Needs to be typed * @typedef {string} Keyword * @typedef {{}} InstallationHandle * @typedef {Object.} IssuerKeywordRecord + * @typedef {Object} Bundle + * @property {string} source + * @property {string} sourceMap + * @property {string} moduleFormat */ /** @@ -67,14 +66,14 @@ import { makeTables } from './state'; * that represent the right to interact with a smart contract in * particular ways. * - * @property {(code: string, moduleFormat: string) => InstallationHandle} install + * @property {(bundle: Bundle, moduleFormat?: string) => InstallationHandle} install * Create an installation by safely evaluating the code and * registering it with Zoe. Returns an installationHandle. * * @property {(installationHandle: InstallationHandle, * issuerKeywordRecord: IssuerKeywordRecord, * terms?: object) - * => Promise} makeInstance + * => Promise} makeInstance * Zoe is long-lived. We can use Zoe to create smart contract * instances by specifying a particular contract installation to * use, as well as the `issuerKeywordRecord` and `terms` of the contract. The @@ -113,8 +112,8 @@ import { makeTables } from './state'; * @property {(offerHandle: OfferHandle) => boolean} isOfferActive * @property {(offerHandles: OfferHandle[]) => OfferRecord[]} getOffers * @property {(offerHandle: OfferHandle) => OfferRecord} getOffer - * @property {(offerHandle: OfferHandle, sparseKeywords?: SparseKeywords) => Allocation} getCurrentAllocation - * @property {(offerHandles: OfferHandle[], sparseKeywords?: SparseKeywords) => Allocation[]} getCurrentAllocations + * @property {(offerHandle: OfferHandle, brandKeywordRecord?: BrandKeywordRecords) => Allocation} getCurrentAllocation + * @property {(offerHandles: OfferHandle[], brandKeywordRecord[]?: BrandKeywordRecords) => Allocation[]} getCurrentAllocations * @property {(installationHandle: InstallationHandle) => string} getInstallation * Get the source code for the installed contract. Throws an error if the * installationHandle is not found. @@ -175,11 +174,7 @@ import { makeTables } from './state'; /** * @callback MakeContract The type exported from a Zoe contract * @param {ContractFacet} zcf The Zoe Contract Facet - * @returns {ContractInstance} The instantiated contract - * - * @typedef {Object} ContractInstance - * @property {Invite} invite The closely-held administrative invite - * @property {Object.} publicAPI Public functions that can be called on the instance + * @returns {Invite} invite The closely-held administrative invite */ /** @@ -200,6 +195,8 @@ import { makeTables } from './state'; * @property {Object.} publicAPI - the invite-free publicly accessible API for the contract * @property {Object} terms - contract parameters * @property {IssuerKeywordRecord} issuerKeywordRecord - record with keywords keys, issuer values + * @property {BrandKeywordRecord} brandKeywordRecord - record with + * keywords keys, brand values * * @typedef {TODO} IssuerRecord * @@ -213,6 +210,9 @@ import { makeTables } from './state'; * * @typedef {Keyword[]} SparseKeywords * @typedef {{[Keyword:string]:Amount}} Allocation + * @typedef {{[Keyword:string]:AmountMath}} AmountMathKeywordRecord + * @typedef {{[Keyword:string]:Brand}} + * BrandKeywordRecord */ /** @@ -226,29 +226,39 @@ import { makeTables } from './state'; * @property {Complete} complete Complete an offer * @property {MakeInvitation} makeInvitation * @property {AddNewIssuer} addNewIssuer + * @property {InitPublicAPI} initPublicAPI * @property {() => ZoeService} getZoeService * @property {() => Issuer} getInviteIssuer - * @property {(sparseKeywords: SparseKeywords) => {[Keyword:string]:AmountMath}} getAmountMaths * @property {(offerHandles: OfferHandle[]) => { active: OfferStatus[], inactive: OfferStatus[] }} getOfferStatuses * @property {(offerHandle: OfferHandle) => boolean} isOfferActive * @property {(offerHandles: OfferHandle[]) => OfferRecord[]} getOffers * @property {(offerHandle: OfferHandle) => OfferRecord} getOffer - * @property {(offerHandle: OfferHandle, sparseKeywords?: SparseKeywords) => Allocation} getCurrentAllocation - * @property {(offerHandles: OfferHandle[], sparseKeywords?: SparseKeywords) => Allocation[]} getCurrentAllocations + * @property {(offerHandle: OfferHandle, brandKeywordRecord?: BrandKeywordRecord) => Allocation} getCurrentAllocation + * @property {(offerHandles: OfferHandle[], brandKeywordRecords?: BrandKeywordRecord[]) => Allocation[]} getCurrentAllocations * @property {() => InstanceRecord} getInstanceRecord - * @property {(issuer: Issuer) => IssuerRecord} getIssuerRecord + * @property {(issuer: Issuer) => Brand} getBrandForIssuer + * @property {(brand: Brand) => AmountMath} getAmountMath * * @callback Reallocate - * The contract can propose a reallocation of extents per offer, - * which will only succeed if the reallocation 1) conserves - * rights, and 2) is 'offer-safe' for all parties involved. This - * reallocation is partial, meaning that it applies only to the - * amount associated with the offerHandles that are passed in. - * We are able to ensure that with each reallocation, rights are - * conserved and offer safety is enforced for all offers, even - * though the reallocation is partial, because once these - * invariants are true, they will remain true until changes are - * made. + * The contract can propose a reallocation of extents across offers + * by providing two parallel arrays: offerHandles and newAllocations. + * Each element of newAllocations is an AmountKeywordRecord whose + * amount should replace the old amount for that keyword for the + * corresponding offer. + * + * The reallocation will only succeed if the reallocation 1) conserves + * rights (the amounts specified have the same total value as the + * current total amount), and 2) is 'offer-safe' for all parties involved. + * + * The reallocation is partial, meaning that it applies only to the + * amount associated with the offerHandles that are passed in. By + * induction, if rights conservation and offer safety hold before, + * they will hold after a safe reallocation, even though we only + * re-validate for the offers whose allocations will change. Since + * rights are conserved for the change, overall rights will be unchanged, + * and a reallocation can only effect offer safety for offers whose + * allocations change. + * * zcf.reallocate will throw an error if any of the * newAllocations do not have a value for all the * keywords in sparseKeywords. An error will also be thrown if @@ -259,9 +269,7 @@ import { makeTables } from './state'; * @param {AmountKeywordRecord[]} newAllocations An * array of amountKeywordRecords - objects with keyword keys * and amount values, with one keywordRecord per offerHandle. - * @param {Keyword[]=} sparseKeywords An array of string - * keywords, which may be a subset of allKeywords - * @returns {TODO} + * @returns {undefined} * * @callback Complete * The contract can "complete" an offer to remove it from the @@ -309,6 +317,13 @@ import { makeTables } from './state'; * @param {Promise|Issuer} issuerP Promise for issuer * @param {Keyword} keyword Keyword for added issuer * @returns {Promise} Issuer is added and ready + * + * * @callback InitPublicAPI + * Initialize the publicAPI for the contract instance, as stored by Zoe in + * the instanceRecord. + * @param {Object} publicAPI - an object whose methods are the API + * available to anyone who knows the instanceHandle + * @returns {void} */ /** @@ -324,6 +339,7 @@ import { makeTables } from './state'; const makeZoe = (additionalEndowments = {}, vatPowers = {}) => { // Zoe maps the inviteHandles to contract offerHook upcalls const inviteHandleToOfferHook = makeStore(); + const { mint: inviteMint, issuer: inviteIssuer, @@ -369,18 +385,6 @@ const makeZoe = (additionalEndowments = {}, vatPowers = {}) => { } }; - // presumes global keywords - const getAmountMaths = (instanceHandle, sparseKeywords) => { - const amountMathKeywordRecord = /** @type {Object.} */ ({}); - const { issuerKeywordRecord } = instanceTable.get(instanceHandle); - // this method presumes that issuers have all been retrieved by this point - sparseKeywords.forEach(keyword => { - const brand = issuerTable.brandFromIssuer(issuerKeywordRecord[keyword]); - amountMathKeywordRecord[keyword] = issuerTable.get(brand).amountMath; - }); - return amountMathKeywordRecord; - }; - const removePurse = issuerRecord => filterObj(issuerRecord, ['issuer', 'brand', 'amountMath']); @@ -399,26 +403,27 @@ const makeZoe = (additionalEndowments = {}, vatPowers = {}) => { }); }; - const doGetCurrentAllocation = ( - instanceHandle, - offerHandle, - sparseKeywords, - ) => { - const { issuerKeywordRecord } = instanceTable.get(instanceHandle); - const allKeywords = getKeywords(issuerKeywordRecord); - if (sparseKeywords === undefined) { - sparseKeywords = allKeywords; - } - const amountMathKeywordRecord = getAmountMaths( - instanceHandle, - sparseKeywords, - ); - assertSubset(allKeywords, sparseKeywords); + const doGetCurrentAllocation = (offerHandle, brandKeywordRecord) => { const { currentAllocation } = offerTable.get(offerHandle); - return filterFillAmounts( - currentAllocation, - sparseKeywords, - amountMathKeywordRecord, + if (brandKeywordRecord === undefined) { + return currentAllocation; + } + const amountMathKeywordRecord = {}; + Object.getOwnPropertyNames(brandKeywordRecord).forEach(keyword => { + const brand = brandKeywordRecord[keyword]; + amountMathKeywordRecord[keyword] = issuerTable.get(brand).amountMath; + }); + return filterFillAmounts(currentAllocation, amountMathKeywordRecord); + }; + + const doGetCurrentAllocations = (offerHandles, brandKeywordRecords) => { + if (brandKeywordRecords === undefined) { + return offerHandles.map(offerHandle => + doGetCurrentAllocation(offerHandle), + ); + } + return offerHandles.map((offerHandle, i) => + doGetCurrentAllocation(offerHandle, brandKeywordRecords[i]), ); }; @@ -442,7 +447,7 @@ const makeZoe = (additionalEndowments = {}, vatPowers = {}) => { * @type {ContractFacet} */ const contractFacet = harden({ - reallocate: (offerHandles, newAllocations, sparseKeywords) => { + reallocate: (offerHandles, newAllocations) => { assertOffersHaveInstanceHandle(offerHandles, instanceHandle); // We may want to handle this with static checking instead. // Discussion at: https://github.com/Agoric/agoric-sdk/issues/1017 @@ -450,73 +455,50 @@ const makeZoe = (additionalEndowments = {}, vatPowers = {}) => { offerHandles.length >= 2, details`reallocating must be done over two or more offers`, ); - - // Set sparseKeywords if undefined. - const { issuerKeywordRecord } = instanceTable.get(instanceHandle); - const allKeywords = getKeywords(issuerKeywordRecord); - if (sparseKeywords === undefined) { - sparseKeywords = allKeywords; - } - - // 1) ensure that rights are conserved overall - const amountMathKeywordRecord = contractFacet.getAmountMaths( - sparseKeywords, - ); - const amountMathsArray = objToArray( - amountMathKeywordRecord, - sparseKeywords, - ); - const currentAmountMatrix = offerHandles.map(handle => { - const filteredAmounts = contractFacet.getCurrentAllocation( - handle, - sparseKeywords, - ); - return objToArray(filteredAmounts, sparseKeywords); - }); - const newAmountMatrix = newAllocations.map(amountObj => - objToArrayAssertFilled(amountObj, sparseKeywords), - ); assert( - areRightsConserved( - amountMathsArray, - currentAmountMatrix, - newAmountMatrix, - ), - details`Rights are not conserved in the proposed reallocation`, + offerHandles.length === newAllocations.length, + details`There must be as many offerHandles as entries in newAllocations`, ); - // 2) Ensure 'offer safety' for each offer separately. - - // Make the potential reallocation and test for offer safety - // by comparing the potential reallocation to the proposal. - const makePotentialReallocation = ( - offerHandle, - sparseKeywordsAllocation, - ) => { + // 1) Ensure 'offer safety' for each offer separately. + const makeOfferSafeReallocation = (offerHandle, newAllocation) => { const { proposal, currentAllocation } = offerTable.get(offerHandle); - const potentialReallocation = harden({ + const reallocation = harden({ ...currentAllocation, - ...sparseKeywordsAllocation, + ...newAllocation, }); - const proposalKeywords = [ - ...getKeywords(proposal.want), - ...getKeywords(proposal.give), - ]; + assert( - isOfferSafeForOffer( - contractFacet.getAmountMaths(proposalKeywords), - proposal, - potentialReallocation, - ), - details`The proposed reallocation was not offer safe`, + isOfferSafe(getAmountMathForBrand, proposal, reallocation), + details`The reallocation was not offer safe`, ); - - // The reallocation passes the offer safety check - return potentialReallocation; + return reallocation; }; + // Make the reallocation and test for offer safety by comparing the + // reallocation to the original proposal. const reallocations = offerHandles.map((offerHandle, i) => - makePotentialReallocation(offerHandle, newAllocations[i]), + makeOfferSafeReallocation(offerHandle, newAllocations[i]), + ); + + // 2. Ensure that rights are conserved overall. + const flattened = arr => [].concat(...arr); + const flattenAllocations = allocations => + flattened(allocations.map(allocation => Object.values(allocation))); + + const currentAllocations = offerTable + .getOffers(offerHandles) + .map(({ currentAllocation }) => currentAllocation); + const previousAmounts = flattenAllocations(currentAllocations); + const newAmounts = flattenAllocations(reallocations); + + assert( + areRightsConserved( + getAmountMathForBrand, + previousAmounts, + newAmounts, + ), + details`Rights are not conserved in the proposed reallocation`, ); // 3. Save the reallocations. @@ -540,8 +522,10 @@ const makeZoe = (additionalEndowments = {}, vatPowers = {}) => { 'string', details`expected an inviteDesc string: ${inviteDesc}`, ); + const { customProperties = harden({}) } = options; const inviteHandle = harden({}); + const { installationHandle } = instanceTable.get(instanceHandle); const inviteAmount = inviteAmountMath.make( harden([ { @@ -549,6 +533,7 @@ const makeZoe = (additionalEndowments = {}, vatPowers = {}) => { inviteDesc, handle: inviteHandle, instanceHandle, + installationHandle, }, ]), ); @@ -559,7 +544,9 @@ const makeZoe = (additionalEndowments = {}, vatPowers = {}) => { addNewIssuer: (issuerP, keyword) => issuerTable.getPromiseForIssuerRecord(issuerP).then(issuerRecord => { assertKeywordName(keyword); - const { issuerKeywordRecord } = instanceTable.get(instanceHandle); + const { issuerKeywordRecord, brandKeywordRecord } = instanceTable.get( + instanceHandle, + ); assert( !getKeywords(issuerKeywordRecord).includes(keyword), details`keyword ${keyword} must be unique`, @@ -568,19 +555,32 @@ const makeZoe = (additionalEndowments = {}, vatPowers = {}) => { ...issuerKeywordRecord, [keyword]: issuerRecord.issuer, }; + const newBrandKeywordRecord = { + ...brandKeywordRecord, + [keyword]: issuerRecord.brand, + }; instanceTable.update(instanceHandle, { issuerKeywordRecord: newIssuerKeywordRecord, + brandKeywordRecord: newBrandKeywordRecord, }); return removePurse(issuerRecord); }), + initPublicAPI: publicAPI => { + const { publicAPI: oldPublicAPI } = instanceTable.get(instanceHandle); + assert( + oldPublicAPI === undefined, + details`the publicAPI has already been initialized`, + ); + instanceTable.update(instanceHandle, { publicAPI }); + }, + // eslint-disable-next-line no-use-before-define getZoeService: () => zoeService, // The methods below are pure and have no side-effects // getInviteIssuer: () => inviteIssuer, - getAmountMaths: sparseKeywords => - getAmountMaths(instanceHandle, sparseKeywords), + getOfferNotifier: offerHandle => offerTable.get(offerHandle).notifier, getOfferStatuses: offerHandles => { const { active, inactive } = offerTable.getOfferStatuses(offerHandles); @@ -603,23 +603,17 @@ const makeZoe = (additionalEndowments = {}, vatPowers = {}) => { assertOffersHaveInstanceHandle(harden([offerHandle]), instanceHandle); return removeAmountsAndNotifier(offerTable.get(offerHandle)); }, - getCurrentAllocation: (offerHandle, sparseKeywords) => { + getCurrentAllocation: (offerHandle, brandKeywordRecord) => { assertOffersHaveInstanceHandle(harden([offerHandle]), instanceHandle); - return doGetCurrentAllocation( - instanceHandle, - offerHandle, - sparseKeywords, - ); + return doGetCurrentAllocation(offerHandle, brandKeywordRecord); }, - getCurrentAllocations: (offerHandles, sparseKeywords) => { + getCurrentAllocations: (offerHandles, brandKeywordRecords) => { assertOffersHaveInstanceHandle(offerHandles, instanceHandle); - return offerHandles.map(offerHandle => - contractFacet.getCurrentAllocation(offerHandle, sparseKeywords), - ); + return doGetCurrentAllocations(offerHandles, brandKeywordRecords); }, getInstanceRecord: () => instanceTable.get(instanceHandle), - getIssuerRecord: issuer => - removePurse(issuerTable.get(issuerTable.brandFromIssuer(issuer))), + getBrandForIssuer: issuer => issuerTable.brandFromIssuer(issuer), + getAmountMath: getAmountMathForBrand, }); return contractFacet; }; @@ -647,24 +641,7 @@ const makeZoe = (additionalEndowments = {}, vatPowers = {}) => { * registering it with Zoe. We have a moduleFormat to allow for * different future formats without silent failures. */ - // TODO: we have 2 or 3 dapps (in separate repos) which do { source, - // moduleFormat } = bundleSource(..), then E(zoe).install(source, - // moduleFormat). Those will get the default - // moduleFormat="nestedEvaluate". We need to support those callers, - // even though our new preferred API is just install(bundle). We also - // look for getExport because that's easier to create in the unit - // tests. TODO once we've ugpraded and released all the dapps, consider - // removing this backwards-compatibility feature. - install: async (bundle, oldModuleFormat) => { - if ( - oldModuleFormat === 'nestedEvaluate' || - oldModuleFormat === 'getExport' - ) { - bundle = harden({ - source: bundle, - moduleFormat: oldModuleFormat, - }); - } + install: async bundle => { const installation = await evalContractBundle( bundle, additionalEndowments, @@ -707,30 +684,34 @@ const makeZoe = (additionalEndowments = {}, vatPowers = {}) => { const makeInstanceRecord = issuerRecords => { const issuers = issuerRecords.map(record => record.issuer); + const brands = issuerRecords.map(record => record.brand); const cleanedIssuerKeywordRecord = arrayToObj( issuers, cleanedKeywords, ); + const brandKeywordRecord = arrayToObj(brands, cleanedKeywords); const instanceRecord = harden({ installationHandle, publicAPI: undefined, terms, issuerKeywordRecord: cleanedIssuerKeywordRecord, + brandKeywordRecord, }); instanceTable.create(instanceRecord, instanceHandle); + return Promise.resolve() .then(_ => installation.makeContract(contractFacet)) - .then(({ invite, publicAPI }) => { - // Once the contract is made, we add the publicAPI to the - // contractRecord - instanceTable.update(instanceHandle, { publicAPI }); + .then(invite => { return inviteIssuer.isLive(invite).then(success => { assert( success, details`invites must be issued by the inviteIssuer.`, ); - return invite; + return { + invite, + instanceRecord: instanceTable.get(instanceHandle), + }; }); }); }; @@ -748,14 +729,6 @@ const makeZoe = (additionalEndowments = {}, vatPowers = {}) => { */ getInstanceRecord: instanceTable.get, - /** - * @deprecated renamed to getInstanceRecord - * Credibly retrieves an instance record given an instanceHandle. - * @param {object} instanceHandle - the unique, unforgeable - * identifier (empty object) for the instance - */ - getInstance: instanceTable.get, - /** Get a notifier (see @agoric/notify) for the offer. */ getOfferNotifier: offerHandle => offerTable.get(offerHandle).notifier, @@ -793,19 +766,8 @@ const makeZoe = (additionalEndowments = {}, vatPowers = {}) => { : []; const userKeywords = harden([...giveKeywords, ...wantKeywords]); - const { - extent: [{ instanceHandle, handle: inviteHandle }], - } = inviteAmount; - const { issuerKeywordRecord } = instanceTable.get(instanceHandle); - - const amountMathKeywordRecord = getAmountMaths( - instanceHandle, - getKeywords(issuerKeywordRecord), - ); - const cleanedProposal = cleanProposal( - issuerKeywordRecord, - amountMathKeywordRecord, + getAmountMathForBrand, proposal, ); @@ -815,11 +777,10 @@ const makeZoe = (additionalEndowments = {}, vatPowers = {}) => { // cleaned proposal's amount that should be the same. const giveAmount = cleanedProposal.give[keyword]; const { purse } = issuerTable.get(giveAmount.brand); - // TODO(1130). drop .then line when deposit() is repaired - // https://github.com/Agoric/agoric-sdk/pull/1130 - return E(purse) - .deposit(paymentKeywordRecord[keyword], giveAmount) - .then(_ => giveAmount); + return E(purse).deposit( + paymentKeywordRecord[keyword], + giveAmount, + ); // eslint-disable-next-line no-else-return } else { // payments outside the give: clause are ignored. @@ -829,6 +790,9 @@ const makeZoe = (additionalEndowments = {}, vatPowers = {}) => { } }); + const { + extent: [{ instanceHandle, handle: inviteHandle }], + } = inviteAmount; const offerHandle = harden({}); // recordOffer() creates and stores a record in the offerTable. The @@ -866,7 +830,7 @@ const makeZoe = (additionalEndowments = {}, vatPowers = {}) => { }; const { exit } = cleanedProposal; const [exitKind] = Object.getOwnPropertyNames(exit); - // Automatically cancel on deadline. + // Automatically complete offer after deadline. if (exitKind === 'afterDeadline') { E(exit.afterDeadline.timer).setWakeup( exit.afterDeadline.deadline, @@ -875,22 +839,21 @@ const makeZoe = (additionalEndowments = {}, vatPowers = {}) => { completeOffers(instanceHandle, harden([offerHandle])), }), ); - // Add an object with a cancel method to offerResult in - // order to cancel on demand. + // Add an object with a complete method to offerResult + // in order to complete offer on demand. Note: we cannot + // add the `complete` function to the offerResult + // directly because our marshalling layer only allows + // two kinds of objects: records (no methods and only + // data) and presences (local proxies for objects that + // may have methods). Having a method makes an object + // automatically a presence, but we want the offerResult + // to be a record. } else if (exitKind === 'onDemand') { const completeObj = { complete: () => completeOffers(instanceHandle, harden([offerHandle])), }; offerResult.completeObj = completeObj; - // The property "cancelObj" and method "cancel" are - // deprecated and will be removed in a later version. - // https://github.com/Agoric/agoric-sdk/issues/835 - const cancelObj = { - cancel: () => - completeOffers(instanceHandle, harden([offerHandle])), - }; - offerResult.cancelObj = cancelObj; } else { assert( exitKind === 'waived', @@ -899,7 +862,7 @@ const makeZoe = (additionalEndowments = {}, vatPowers = {}) => { } // if the exitRule.kind is 'waived' the user has no - // possibility of cancelling + // possibility of completing an offer on demand return harden(offerResult); }; return Promise.all(paymentDepositedPs) @@ -913,19 +876,10 @@ const makeZoe = (additionalEndowments = {}, vatPowers = {}) => { offerTable.getOffers(offerHandles).map(removeAmountsAndNotifier), getOffer: offerHandle => removeAmountsAndNotifier(offerTable.get(offerHandle)), - getCurrentAllocation: (offerHandle, sparseKeywords) => { - const { instanceHandle } = offerTable.get(offerHandle); - return doGetCurrentAllocation( - instanceHandle, - offerHandle, - sparseKeywords, - ); - }, - getCurrentAllocations: (offerHandles, sparseKeywords) => { - return offerHandles.map(offerHandle => - zoeService.getCurrentAllocation(offerHandle, sparseKeywords), - ); - }, + getCurrentAllocation: (offerHandle, brandKeywordRecord) => + doGetCurrentAllocation(offerHandle, brandKeywordRecord), + getCurrentAllocations: (offerHandles, brandKeywordRecords) => + doGetCurrentAllocations(offerHandles, brandKeywordRecords), getInstallation: installationHandle => installationTable.get(installationHandle).bundle, }, diff --git a/packages/zoe/test/swingsetTests/zoe-metering/bootstrap.js b/packages/zoe/test/swingsetTests/zoe-metering/bootstrap.js index a349c5794e4..65bc81d9430 100644 --- a/packages/zoe/test/swingsetTests/zoe-metering/bootstrap.js +++ b/packages/zoe/test/swingsetTests/zoe-metering/bootstrap.js @@ -31,14 +31,9 @@ function build(E, log) { log(`instantiating ${testName}`); const inviteIssuer = E(zoe).getInviteIssuer(); const issuerKeywordRecord = harden({ Keyword1: inviteIssuer }); - const invite = await E(zoe).makeInstance( - installId, - issuerKeywordRecord, - ); const { - extent: [{ instanceHandle }], - } = await E(inviteIssuer).getAmountOf(invite); - const { publicAPI } = await E(zoe).getInstanceRecord(instanceHandle); + instanceRecord: { publicAPI }, + } = await E(zoe).makeInstance(installId, issuerKeywordRecord); log(`invoking ${testName}.doTest()`); await E(publicAPI).doTest(); log(`complete`); diff --git a/packages/zoe/test/swingsetTests/zoe-metering/infiniteTestLoop.js b/packages/zoe/test/swingsetTests/zoe-metering/infiniteTestLoop.js index d698dd5f376..a36c6202cbd 100644 --- a/packages/zoe/test/swingsetTests/zoe-metering/infiniteTestLoop.js +++ b/packages/zoe/test/swingsetTests/zoe-metering/infiniteTestLoop.js @@ -1,15 +1,15 @@ import harden from '@agoric/harden'; -export const makeContract = zoe => { - const invite = zoe.makeInvitation(() => {}, 'tester'); - return harden({ - invite, - publicAPI: { +export const makeContract = zcf => { + const invite = zcf.makeInvitation(() => {}, 'tester'); + zcf.initPublicAPI( + harden({ doTest: () => { for (;;) { // Nothing } }, - }, - }); + }), + ); + return invite; }; diff --git a/packages/zoe/test/swingsetTests/zoe-metering/testBuiltins.js b/packages/zoe/test/swingsetTests/zoe-metering/testBuiltins.js index b3b51360da6..a42129e57be 100644 --- a/packages/zoe/test/swingsetTests/zoe-metering/testBuiltins.js +++ b/packages/zoe/test/swingsetTests/zoe-metering/testBuiltins.js @@ -1,13 +1,13 @@ import harden from '@agoric/harden'; -export const makeContract = zoe => { - const invite = zoe.makeInvitation(() => {}, 'tester'); - return harden({ - invite, - publicAPI: { +export const makeContract = zcf => { + const invite = zcf.makeInvitation(() => {}, 'tester'); + zcf.initPublicAPI( + harden({ doTest: () => { new Array(1e9).map(Object.create); }, - }, - }); + }), + ); + return invite; }; diff --git a/packages/zoe/test/swingsetTests/zoe/bootstrap.js b/packages/zoe/test/swingsetTests/zoe/bootstrap.js index 2d67fe5d2a6..0a346bd0c9a 100644 --- a/packages/zoe/test/swingsetTests/zoe/bootstrap.js +++ b/packages/zoe/test/swingsetTests/zoe/bootstrap.js @@ -10,6 +10,8 @@ import publicAuctionBundle from './bundle-publicAuction'; import atomicSwapBundle from './bundle-atomicSwap'; import simpleExchangeBundle from './bundle-simpleExchange'; import autoswapBundle from './bundle-autoswap'; +import sellItemsBundle from './bundle-sellItems'; +import mintAndSellNFTBundle from './bundle-mintAndSellNFT'; /* eslint-enable import/no-unresolved, import/extensions */ const setupBasicMints = () => { @@ -97,6 +99,8 @@ function build(E, log) { atomicSwap: await E(zoe).install(atomicSwapBundle.bundle), simpleExchange: await E(zoe).install(simpleExchangeBundle.bundle), autoswap: await E(zoe).install(autoswapBundle.bundle), + sellItems: await E(zoe).install(sellItemsBundle.bundle), + mintAndSellNFT: await E(zoe).install(mintAndSellNFTBundle.bundle), }; const [testName, startingExtents] = argv; diff --git a/packages/zoe/test/swingsetTests/zoe/test-zoe.js b/packages/zoe/test/swingsetTests/zoe/test-zoe.js index 9019b24a68b..210438e0ec6 100644 --- a/packages/zoe/test/swingsetTests/zoe/test-zoe.js +++ b/packages/zoe/test/swingsetTests/zoe/test-zoe.js @@ -15,6 +15,8 @@ const CONTRACT_FILES = [ 'publicAuction', 'atomicSwap', 'simpleExchange', + 'sellItems', + 'mintAndSellNFT', ]; const generateBundlesP = Promise.all( CONTRACT_FILES.map(async contract => { @@ -179,9 +181,9 @@ const expectedSimpleExchangeOkLog = [ 'The offer has been accepted. Once the contract has been completed, please check your payout', 'The offer has been accepted. Once the contract has been completed, please check your payout', 'bobMoolaPurse: balance {"brand":{},"extent":3}', - 'bobSimoleanPurse: balance {"brand":{},"extent":0}', + 'bobSimoleanPurse: balance {"brand":{},"extent":3}', 'aliceMoolaPurse: balance {"brand":{},"extent":0}', - 'aliceSimoleanPurse: balance {"brand":{},"extent":7}', + 'aliceSimoleanPurse: balance {"brand":{},"extent":4}', ]; test('zoe - simpleExchange - valid inputs', async t => { @@ -206,21 +208,21 @@ const expectedSimpleExchangeNotificationLog = [ '{"buys":[],"sells":[]}', 'The offer has been accepted. Once the contract has been completed, please check your payout', 'bobMoolaPurse: balance {"brand":{},"extent":0}', - 'bobSimoleanPurse: balance {"brand":{},"extent":17}', + 'bobSimoleanPurse: balance {"brand":{},"extent":20}', '{"buys":[{"want":{"Asset":{"brand":{},"extent":8}},"give":{"Price":{"brand":{},"extent":2}}}],"sells":[]}', 'The offer has been accepted. Once the contract has been completed, please check your payout', 'bobMoolaPurse: balance {"brand":{},"extent":3}', - 'bobSimoleanPurse: balance {"brand":{},"extent":15}', + 'bobSimoleanPurse: balance {"brand":{},"extent":18}', '{"buys":[{"want":{"Asset":{"brand":{},"extent":8}},"give":{"Price":{"brand":{},"extent":2}}},{"want":{"Asset":{"brand":{},"extent":20}},"give":{"Price":{"brand":{},"extent":13}}}],"sells":[]}', 'The offer has been accepted. Once the contract has been completed, please check your payout', 'bobMoolaPurse: balance {"brand":{},"extent":3}', - 'bobSimoleanPurse: balance {"brand":{},"extent":2}', + 'bobSimoleanPurse: balance {"brand":{},"extent":5}', '{"buys":[{"want":{"Asset":{"brand":{},"extent":8}},"give":{"Price":{"brand":{},"extent":2}}},{"want":{"Asset":{"brand":{},"extent":20}},"give":{"Price":{"brand":{},"extent":13}}},{"want":{"Asset":{"brand":{},"extent":5}},"give":{"Price":{"brand":{},"extent":2}}}],"sells":[]}', 'The offer has been accepted. Once the contract has been completed, please check your payout', 'bobMoolaPurse: balance {"brand":{},"extent":3}', - 'bobSimoleanPurse: balance {"brand":{},"extent":0}', + 'bobSimoleanPurse: balance {"brand":{},"extent":3}', 'aliceMoolaPurse: balance {"brand":{},"extent":0}', - 'aliceSimoleanPurse: balance {"brand":{},"extent":7}', + 'aliceSimoleanPurse: balance {"brand":{},"extent":4}', ]; test('zoe - simpleExchange - state Update', async t => { @@ -257,3 +259,20 @@ test('zoe - autoswap - valid inputs', async t => { const dump = await main(['autoswapOk', startingExtents]); t.deepEquals(dump.log, expectedAutoswapOkLog); }); + +const expectedSellTicketsOkLog = [ + '=> alice, bob, carol and dave are set up', + 'availableTickets: {"brand":{},"extent":[{"show":"Steven Universe, the Opera","start":"Wed, March 25th 2020 at 8pm","number":1},{"show":"Steven Universe, the Opera","start":"Wed, March 25th 2020 at 8pm","number":2},{"show":"Steven Universe, the Opera","start":"Wed, March 25th 2020 at 8pm","number":3}]}', + 'boughtTicketAmount: {"brand":{},"extent":[{"show":"Steven Universe, the Opera","start":"Wed, March 25th 2020 at 8pm","number":1}]}', + 'after ticket1 purchased: {"brand":{},"extent":[{"show":"Steven Universe, the Opera","start":"Wed, March 25th 2020 at 8pm","number":2},{"show":"Steven Universe, the Opera","start":"Wed, March 25th 2020 at 8pm","number":3}]}', + 'alice earned: {"brand":{},"extent":22}', +]; +test('zoe - sellTickets - valid inputs', async t => { + t.plan(1); + const startingExtents = [ + [0, 0, 0], + [22, 0, 0], + ]; + const dump = await main(['sellTicketsOk', startingExtents]); + t.deepEquals(dump.log, expectedSellTicketsOkLog); +}); diff --git a/packages/zoe/test/swingsetTests/zoe/vat-alice.js b/packages/zoe/test/swingsetTests/zoe/vat-alice.js index 0fbda675fbd..b987385eb46 100644 --- a/packages/zoe/test/swingsetTests/zoe/vat-alice.js +++ b/packages/zoe/test/swingsetTests/zoe/vat-alice.js @@ -1,14 +1,11 @@ import harden from '@agoric/harden'; import { showPurseBalance, setupIssuers, getLocalAmountMath } from '../helpers'; -import { makeGetInstanceHandle } from '../../../src/clientSupport'; const build = async (E, log, zoe, issuers, payments, installations, timer) => { const { moola, simoleans, purses } = await setupIssuers(zoe, issuers); const [moolaPurseP, simoleanPurseP] = purses; const [moolaPayment, simoleanPayment] = payments; const [moolaIssuer, simoleanIssuer] = issuers; - const inviteIssuer = await E(zoe).getInviteIssuer(); - const getInstanceHandle = makeGetInstanceHandle(inviteIssuer); const doAutomaticRefund = async bobP => { log(`=> alice.doCreateAutomaticRefund called`); @@ -17,13 +14,11 @@ const build = async (E, log, zoe, issuers, payments, installations, timer) => { Contribution1: moolaIssuer, Contribution2: simoleanIssuer, }); - const refundInvite = await E(zoe).makeInstance( + const { invite: refundInvite, instanceRecord } = await E(zoe).makeInstance( installId, issuerKeywordRecord, ); - const instanceHandle = await getInstanceHandle(refundInvite); - const instanceRecord = await E(zoe).getInstanceRecord(instanceHandle); const { publicAPI } = instanceRecord; const proposal = harden({ give: { Contribution1: moola(3) }, @@ -59,7 +54,7 @@ const build = async (E, log, zoe, issuers, payments, installations, timer) => { UnderlyingAsset: moolaIssuer, StrikePrice: simoleanIssuer, }); - const writeCallInvite = await E(zoe).makeInstance( + const { invite: writeCallInvite } = await E(zoe).makeInstance( installId, issuerKeywordRecord, ); @@ -95,7 +90,7 @@ const build = async (E, log, zoe, issuers, payments, installations, timer) => { UnderlyingAsset: moolaIssuer, StrikePrice: simoleanIssuer, }); - const writeCallInvite = await E(zoe).makeInstance( + const { invite: writeCallInvite } = await E(zoe).makeInstance( installations.coveredCall, issuerKeywordRecord, ); @@ -135,20 +130,21 @@ const build = async (E, log, zoe, issuers, payments, installations, timer) => { const numBidsAllowed = 3; const issuerKeywordRecord = harden({ Asset: moolaIssuer, - Bid: simoleanIssuer, + Ask: simoleanIssuer, }); const terms = harden({ numBidsAllowed }); - const sellAssetsInvite = await E(zoe).makeInstance( + const { + invite: sellAssetsInvite, + instanceRecord: { publicAPI }, + } = await E(zoe).makeInstance( installations.publicAuction, issuerKeywordRecord, terms, ); - const instanceHandle = await getInstanceHandle(sellAssetsInvite); - const { publicAPI } = await E(zoe).getInstanceRecord(instanceHandle); const proposal = harden({ give: { Asset: moola(1) }, - want: { Bid: simoleans(3) }, + want: { Ask: simoleans(3) }, exit: { onDemand: null }, }); const paymentKeywordRecord = { Asset: moolaPayment }; @@ -172,7 +168,7 @@ const build = async (E, log, zoe, issuers, payments, installations, timer) => { const payout = await payoutP; const moolaPayout = await payout.Asset; - const simoleanPayout = await payout.Bid; + const simoleanPayout = await payout.Ask; await E(moolaPurseP).deposit(moolaPayout); await E(simoleanPurseP).deposit(simoleanPayout); @@ -186,7 +182,7 @@ const build = async (E, log, zoe, issuers, payments, installations, timer) => { Asset: moolaIssuer, Price: simoleanIssuer, }); - const firstOfferInvite = await E(zoe).makeInstance( + const { invite: firstOfferInvite } = await E(zoe).makeInstance( installations.atomicSwap, issuerKeywordRecord, ); @@ -222,12 +218,10 @@ const build = async (E, log, zoe, issuers, payments, installations, timer) => { Asset: moolaIssuer, }); const { simpleExchange } = installations; - const addOrderInvite = await E(zoe).makeInstance( - simpleExchange, - issuerKeywordRecord, - ); - const instanceHandle = await getInstanceHandle(addOrderInvite); - const { publicAPI } = await E(zoe).getInstanceRecord(instanceHandle); + const { + invite: addOrderInvite, + instanceRecord: { publicAPI }, + } = await E(zoe).makeInstance(simpleExchange, issuerKeywordRecord); const aliceSellOrderProposal = harden({ give: { Asset: moola(3) }, @@ -271,12 +265,10 @@ const build = async (E, log, zoe, issuers, payments, installations, timer) => { Asset: moolaIssuer, }); const { simpleExchange } = installations; - const addOrderInvite = await E(zoe).makeInstance( - simpleExchange, - issuerKeywordRecord, - ); - const instanceHandle = await getInstanceHandle(addOrderInvite); - const { publicAPI } = await E(zoe).getInstanceRecord(instanceHandle); + const { + invite: addOrderInvite, + instanceRecord: { publicAPI }, + } = await E(zoe).makeInstance(simpleExchange, issuerKeywordRecord); logStateOnChanges(await E(publicAPI).getNotifier()); @@ -319,12 +311,10 @@ const build = async (E, log, zoe, issuers, payments, installations, timer) => { TokenA: moolaIssuer, TokenB: simoleanIssuer, }); - const addLiquidityInvite = await E(zoe).makeInstance( - installations.autoswap, - issuerKeywordRecord, - ); - const instanceHandle = await getInstanceHandle(addLiquidityInvite); - const { publicAPI } = await E(zoe).getInstanceRecord(instanceHandle); + const { + invite: addLiquidityInvite, + instanceRecord: { publicAPI, handle: instanceHandle }, + } = await E(zoe).makeInstance(installations.autoswap, issuerKeywordRecord); const liquidityIssuer = await E(publicAPI).getLiquidityIssuer(); const liquidityAmountMath = await getLocalAmountMath(liquidityIssuer); const liquidity = liquidityAmountMath.make; @@ -396,6 +386,48 @@ const build = async (E, log, zoe, issuers, payments, installations, timer) => { ); }; + const doSellTickets = async bobP => { + const { mintAndSellNFT } = installations; + const { invite } = await E(zoe).makeInstance(mintAndSellNFT); + + const { outcome } = await E(zoe).offer(invite); + const ticketSeller = await outcome; + + // completeObj exists because of a current limitation in @agoric/marshal : https://github.com/Agoric/agoric-sdk/issues/818 + const { + sellItemsInstanceHandle: ticketSalesInstanceHandle, + payout: payoutP, + completeObj, + } = await E(ticketSeller).sellTokens({ + customExtentProperties: { + show: 'Steven Universe, the Opera', + start: 'Wed, March 25th 2020 at 8pm', + }, + count: 3, + moneyIssuer: moolaIssuer, + sellItemsInstallationHandle: installations.sellItems, + pricePerItem: moola(22), + }); + + await E(bobP).doBuyTickets(ticketSalesInstanceHandle); + + const { publicAPI: ticketSalesPublicAPI } = await E(zoe).getInstanceRecord( + ticketSalesInstanceHandle, + ); + const availableTickets = await E(ticketSalesPublicAPI).getAvailableItems(); + + log('after ticket1 purchased: ', availableTickets); + + await E(completeObj).complete(); + + const payout = await payoutP; + const moneyPayment = await payout.Money; + await E(moolaPurseP).deposit(moneyPayment); + const currentPurseBalance = await E(moolaPurseP).getCurrentAmount(); + + log('alice earned: ', currentPurseBalance); + }; + return harden({ startTest: async (testName, bobP, carolP, daveP) => { switch (testName) { @@ -423,6 +455,9 @@ const build = async (E, log, zoe, issuers, payments, installations, timer) => { case 'autoswapOk': { return doAutoswap(bobP, carolP, daveP); } + case 'sellTicketsOk': { + return doSellTickets(bobP, carolP, daveP); + } default: { throw new Error(`testName ${testName} not recognized`); } diff --git a/packages/zoe/test/swingsetTests/zoe/vat-bob.js b/packages/zoe/test/swingsetTests/zoe/vat-bob.js index 487a0ab641e..37d3cc1341d 100644 --- a/packages/zoe/test/swingsetTests/zoe/vat-bob.js +++ b/packages/zoe/test/swingsetTests/zoe/vat-bob.js @@ -87,12 +87,8 @@ const build = async (E, log, zoe, issuers, payments, installations, timer) => { const { extent: optionExtent } = await E(inviteIssuer).getAmountOf( exclInvite, ); - - const instanceHandle = await getInstanceHandle(exclInvite); - const instanceInfo = await E(zoe).getInstanceRecord(instanceHandle); - assert( - instanceInfo.installationHandle === installations.coveredCall, + optionExtent[0].installationHandle === installations.coveredCall, details`wrong installation`, ); assert( @@ -111,7 +107,9 @@ const build = async (E, log, zoe, issuers, payments, installations, timer) => { ); assert(optionExtent[0].timerAuthority === timer, 'wrong timer'); - const { UnderlyingAsset, StrikePrice } = instanceInfo.issuerKeywordRecord; + const { + issuerKeywordRecord: { UnderlyingAsset, StrikePrice }, + } = await E(zoe).getInstanceRecord(optionExtent[0].instanceHandle); assert( UnderlyingAsset === moolaIssuer, @@ -152,11 +150,8 @@ const build = async (E, log, zoe, issuers, payments, installations, timer) => { const optionAmounts = await E(inviteIssuer).getAmountOf(exclInvite); const optionExtent = optionAmounts.extent; - const instanceInfo = await E(zoe).getInstanceRecord( - optionExtent[0].instanceHandle, - ); assert( - instanceInfo.installationHandle === installations.coveredCall, + optionExtent[0].installationHandle === installations.coveredCall, details`wrong installation`, ); assert( @@ -176,7 +171,9 @@ const build = async (E, log, zoe, issuers, payments, installations, timer) => { details`wrong expiration date`, ); assert(optionExtent[0].timerAuthority === timer, details`wrong timer`); - const { UnderlyingAsset, StrikePrice } = instanceInfo.issuerKeywordRecord; + const { + issuerKeywordRecord: { UnderlyingAsset, StrikePrice }, + } = await E(zoe).getInstanceRecord(optionExtent[0].instanceHandle); assert( UnderlyingAsset === moolaIssuer, details`The underlyingAsset issuer should be the moola issuer`, @@ -193,7 +190,7 @@ const build = async (E, log, zoe, issuers, payments, installations, timer) => { Asset: inviteIssuer, Price: bucksIssuer, }); - const bobSwapInvite = await E(zoe).makeInstance( + const { invite: bobSwapInvite } = await E(zoe).makeInstance( installations.atomicSwap, issuerKeywordRecord, ); @@ -244,7 +241,7 @@ const build = async (E, log, zoe, issuers, payments, installations, timer) => { ); assert( sameStructure( - harden({ Asset: moolaIssuer, Bid: simoleanIssuer }), + harden({ Asset: moolaIssuer, Ask: simoleanIssuer }), issuerKeywordRecord, ), details`issuerKeywordRecord was not as expected`, @@ -449,44 +446,46 @@ const build = async (E, log, zoe, issuers, payments, installations, timer) => { // bob checks the price of 3 moola. The price is 1 simolean const simoleanAmounts = await E(publicAPI).getCurrentPrice( - harden({ TokenA: moola(3) }), + moola(3), + simoleans(0).brand, ); log(`simoleanAmounts `, simoleanAmounts); const buyBInvite = E(publicAPI).makeSwapInvite(); const moolaForSimProposal = harden({ - give: { TokenA: moola(3) }, - want: { TokenB: simoleans(1) }, + give: { In: moola(3) }, + want: { Out: simoleans(1) }, }); - const moolaForSimPayments = harden({ TokenA: moolaPayment }); + const moolaForSimPayments = harden({ In: moolaPayment }); const { payout: moolaForSimPayoutP, outcome: outcomeP } = await E( zoe, ).offer(buyBInvite, moolaForSimProposal, moolaForSimPayments); log(await outcomeP); const moolaForSimPayout = await moolaForSimPayoutP; - const moolaPayout1 = await moolaForSimPayout.TokenA; - const simoleanPayout1 = await moolaForSimPayout.TokenB; + const moolaPayout1 = await moolaForSimPayout.In; + const simoleanPayout1 = await moolaForSimPayout.Out; await E(moolaPurseP).deposit(moolaPayout1); await E(simoleanPurseP).deposit(simoleanPayout1); // Bob looks up the price of 3 simoleans. It's 5 moola const moolaAmounts = await E(publicAPI).getCurrentPrice( - harden({ TokenB: simoleans(3) }), + simoleans(3), + moola(0).brand, ); log(`moolaAmounts `, moolaAmounts); // Bob makes another offer and swaps const bobSimsForMoolaProposal = harden({ - want: { TokenA: moola(5) }, - give: { TokenB: simoleans(3) }, + want: { Out: moola(5) }, + give: { In: simoleans(3) }, }); await E(simoleanPurseP).deposit(simoleanPayment); const bobSimoleanPayment = await E(simoleanPurseP).withdraw(simoleans(3)); - const simsForMoolaPayments = harden({ TokenB: bobSimoleanPayment }); + const simsForMoolaPayments = harden({ In: bobSimoleanPayment }); const invite2 = E(publicAPI).makeSwapInvite(); const { @@ -501,8 +500,8 @@ const build = async (E, log, zoe, issuers, payments, installations, timer) => { log(await simsForMoolaOutcomeP); const simsForMoolaPayout = await bobSimsForMoolaPayoutP; - const moolaPayout2 = await simsForMoolaPayout.TokenA; - const simoleanPayout2 = await simsForMoolaPayout.TokenB; + const moolaPayout2 = await simsForMoolaPayout.Out; + const simoleanPayout2 = await simsForMoolaPayout.In; await E(moolaPurseP).deposit(moolaPayout2); await E(simoleanPurseP).deposit(simoleanPayout2); @@ -510,6 +509,48 @@ const build = async (E, log, zoe, issuers, payments, installations, timer) => { await showPurseBalance(moolaPurseP, 'bobMoolaPurse', log); await showPurseBalance(simoleanPurseP, 'bobSimoleanPurse', log); }, + doBuyTickets: async ticketSalesInstanceHandle => { + const { publicAPI: ticketSalesPublicAPI, terms } = await E( + zoe, + ).getInstanceRecord(ticketSalesInstanceHandle); + const ticketIssuer = await E(ticketSalesPublicAPI).getItemsIssuer(); + const ticketAmountMath = await E(ticketIssuer).getAmountMath(); + + // Bob makes an invite + const invite = await E(ticketSalesPublicAPI).makeBuyerInvite(); + + const availableTickets = await E( + ticketSalesPublicAPI, + ).getAvailableItems(); + log('availableTickets: ', availableTickets); + + // find the extent corresponding to ticket #1 + const ticket1Extent = availableTickets.extent.find( + ticket => ticket.number === 1, + ); + // make the corresponding amount + const ticket1Amount = await E(ticketAmountMath).make( + harden([ticket1Extent]), + ); + + const proposal = harden({ + give: { Money: terms.pricePerItem }, + want: { Items: ticket1Amount }, + }); + + const paymentKeywordRecord = harden({ Money: moolaPayment }); + + const { payout: payoutP } = await E(zoe).offer( + invite, + proposal, + paymentKeywordRecord, + ); + const payout = await payoutP; + const boughtTicketAmount = await E(ticketIssuer).getAmountOf( + payout.Items, + ); + log('boughtTicketAmount: ', boughtTicketAmount); + }, }); }; diff --git a/packages/zoe/test/swingsetTests/zoe/vat-carol.js b/packages/zoe/test/swingsetTests/zoe/vat-carol.js index ea3a487740d..6cad319fa15 100644 --- a/packages/zoe/test/swingsetTests/zoe/vat-carol.js +++ b/packages/zoe/test/swingsetTests/zoe/vat-carol.js @@ -26,7 +26,7 @@ const build = async (E, log, zoe, issuers, payments, installations) => { ); assert( sameStructure( - harden({ Asset: moolaIssuer, Bid: simoleanIssuer }), + harden({ Asset: moolaIssuer, Ask: simoleanIssuer }), issuerKeywordRecord, ), details`issuerKeywordRecord were not as expected`, diff --git a/packages/zoe/test/swingsetTests/zoe/vat-dave.js b/packages/zoe/test/swingsetTests/zoe/vat-dave.js index d4ae170ad7b..46b17fe2ab2 100644 --- a/packages/zoe/test/swingsetTests/zoe/vat-dave.js +++ b/packages/zoe/test/swingsetTests/zoe/vat-dave.js @@ -36,7 +36,7 @@ const build = async (E, log, zoe, issuers, payments, installations, timer) => { ); assert( sameStructure( - harden({ Asset: moolaIssuer, Bid: simoleanIssuer }), + harden({ Asset: moolaIssuer, Ask: simoleanIssuer }), issuerKeywordRecord, ), details`issuerKeywordRecord were not as expected`, diff --git a/packages/zoe/test/unitTests/contractSupport/test-bondingCurves.js b/packages/zoe/test/unitTests/contractSupport/test-bondingCurves.js index 86a30108562..509b725639e 100644 --- a/packages/zoe/test/unitTests/contractSupport/test-bondingCurves.js +++ b/packages/zoe/test/unitTests/contractSupport/test-bondingCurves.js @@ -2,18 +2,18 @@ import { test } from 'tape-promise/tape'; import { - getCurrentPrice, + getInputPrice, calcLiqExtentToMint, } from '../../../src/contractSupport'; const testGetPrice = (t, input, expectedOutput) => { - const output = getCurrentPrice(input); + const output = getInputPrice(input); t.deepEquals(output, expectedOutput); }; -// If these tests of `getCurrentPrice` fail, it would indicate that we have +// If these tests of `getInputPrice` fail, it would indicate that we have // diverged from the calculation in the Uniswap paper. -test('getCurrentPrice ok 1', t => { +test('getInputPrice ok 1', t => { t.plan(1); try { const input = { @@ -21,18 +21,14 @@ test('getCurrentPrice ok 1', t => { outputReserve: 0, inputExtent: 1, }; - const expectedOutput = { - outputExtent: 0, - newInputReserve: 1, - newOutputReserve: 0, - }; + const expectedOutput = 0; testGetPrice(t, input, expectedOutput); } catch (e) { t.assert(false, e); } }); -test('getCurrentPrice ok 2', t => { +test('getInputPrice ok 2', t => { t.plan(1); try { const input = { @@ -40,18 +36,14 @@ test('getCurrentPrice ok 2', t => { outputReserve: 3028, inputExtent: 1398, }; - const expectedOutput = { - outputExtent: 572, - newInputReserve: 7382, - newOutputReserve: 2456, - }; + const expectedOutput = 572; testGetPrice(t, input, expectedOutput); } catch (e) { t.assert(false, e); } }); -test('getCurrentPrice ok 3', t => { +test('getInputPrice ok 3', t => { t.plan(1); try { const input = { @@ -59,18 +51,14 @@ test('getCurrentPrice ok 3', t => { outputReserve: 7743, inputExtent: 6635, }; - const expectedOutput = { - outputExtent: 3466, - newInputReserve: 14795, - newOutputReserve: 4277, - }; + const expectedOutput = 3466; testGetPrice(t, input, expectedOutput); } catch (e) { t.assert(false, e); } }); -test('getCurrentPrice reverse x and y amounts', t => { +test('getInputPrice reverse x and y amounts', t => { t.plan(1); try { // Note: this is now the same test as the one above because we are @@ -80,18 +68,14 @@ test('getCurrentPrice reverse x and y amounts', t => { outputReserve: 7743, inputExtent: 6635, }; - const expectedOutput = { - outputExtent: 3466, - newInputReserve: 14795, - newOutputReserve: 4277, - }; + const expectedOutput = 3466; testGetPrice(t, input, expectedOutput); } catch (e) { t.assert(false, e); } }); -test('getCurrentPrice ok 4', t => { +test('getInputPrice ok 4', t => { t.plan(1); try { const input = { @@ -99,18 +83,14 @@ test('getCurrentPrice ok 4', t => { outputReserve: 10, inputExtent: 1000, }; - const expectedOutput = { - outputExtent: 9, - newInputReserve: 1010, - newOutputReserve: 1, - }; + const expectedOutput = 9; testGetPrice(t, input, expectedOutput); } catch (e) { t.assert(false, e); } }); -test('getCurrentPrice ok 5', t => { +test('getInputPrice ok 5', t => { t.plan(1); try { const input = { @@ -118,18 +98,14 @@ test('getCurrentPrice ok 5', t => { outputReserve: 50, inputExtent: 17, }; - const expectedOutput = { - outputExtent: 7, - newInputReserve: 117, - newOutputReserve: 43, - }; + const expectedOutput = 7; testGetPrice(t, input, expectedOutput); } catch (e) { t.assert(false, e); } }); -test('getCurrentPrice ok 6', t => { +test('getInputPrice ok 6', t => { t.plan(1); try { const input = { @@ -137,11 +113,7 @@ test('getCurrentPrice ok 6', t => { inputReserve: 43, inputExtent: 7, }; - const expectedOutput = { - outputExtent: 16, - newInputReserve: 50, - newOutputReserve: 101, - }; + const expectedOutput = 16; testGetPrice(t, input, expectedOutput); } catch (e) { t.assert(false, e); diff --git a/packages/zoe/test/unitTests/contractSupport/test-zoeHelpers.js b/packages/zoe/test/unitTests/contractSupport/test-zoeHelpers.js index b5827135893..da69aada338 100644 --- a/packages/zoe/test/unitTests/contractSupport/test-zoeHelpers.js +++ b/packages/zoe/test/unitTests/contractSupport/test-zoeHelpers.js @@ -2,6 +2,7 @@ import { test } from 'tape-promise/tape'; import harden from '@agoric/harden'; +import makeStore from '@agoric/store'; import { setup } from '../setupBasicMints'; import { @@ -26,21 +27,57 @@ test('ZoeHelpers messages', t => { } }); +function makeMockZoeBuilder() { + const offers = makeStore(); + const allocs = makeStore(); + let instanceRecord; + const amountMathToBrand = makeStore(); + const completedHandles = []; + const reallocatedAmountObjs = []; + const reallocatedHandles = []; + let isOfferActive = true; + + return harden({ + addOffer: (keyword, offer) => offers.init(keyword, offer), + addAllocation: (keyword, alloc) => allocs.init(keyword, alloc), + setInstanceRecord: newRecord => (instanceRecord = newRecord), + addBrand: issuerRecord => + amountMathToBrand.init(issuerRecord.brand, issuerRecord.amountMath), + setOffersInactive: () => (isOfferActive = false), + build: () => + harden({ + getInstanceRecord: () => instanceRecord, + getAmountMath: amountMath => amountMathToBrand.get(amountMath), + getZoeService: () => {}, + isOfferActive: () => isOfferActive, + getOffer: handle => offers.get(handle), + getCurrentAllocation: handle => allocs.get(handle), + reallocate: (handles, amountObjs) => { + reallocatedHandles.push(...handles); + reallocatedAmountObjs.push(...amountObjs); + }, + complete: handles => + handles.map(handle => completedHandles.push(handle)), + getReallocatedAmountObjs: () => reallocatedAmountObjs, + getReallocatedHandles: () => reallocatedHandles, + getCompletedHandles: () => completedHandles, + }), + }); +} + test('ZoeHelpers assertKeywords', t => { t.plan(5); const { moolaR, simoleanR } = setup(); try { - const mockZCF = harden({ - getInstanceRecord: () => - harden({ - issuerKeywordRecord: { - Asset: moolaR.issuer, - Price: simoleanR.issuer, - }, - }), - getAmountMaths: () => {}, - getZoeService: () => {}, + const mockZCFBuilder = makeMockZoeBuilder(); + mockZCFBuilder.setInstanceRecord({ + issuerKeywordRecord: { + Asset: moolaR.issuer, + Price: simoleanR.issuer, + }, }); + + const mockZCF = mockZCFBuilder.build(); const { assertKeywords } = makeZoeHelpers(mockZCF); t.doesNotThrow( () => assertKeywords(['Asset', 'Price']), @@ -72,49 +109,38 @@ test('ZoeHelpers assertKeywords', t => { test('ZoeHelpers rejectIfNotProposal', t => { t.plan(8); - const { moolaR, simoleanR, moola, simoleans } = setup(); - const completedOfferHandles = []; + const { moola, simoleans } = setup(); const offerHandles = harden([{}, {}, {}, {}, {}, {}, {}]); try { - const mockZCF = harden({ - getInstanceRecord: () => - harden({ - issuerKeywordRecord: { - Asset: moolaR.issuer, - Price: simoleanR.issuer, - }, - }), - getAmountMaths: () => {}, - getZoeService: () => {}, - getOffer: handle => { - if (offerHandles.indexOf(handle) === 4) { - return harden({ - proposal: { - want: { Asset: moola(4) }, - give: { Price: simoleans(16) }, - exit: { Waived: null }, - }, - }); - } - if (offerHandles.indexOf(handle) === 5) { - return harden({ - proposal: { - want: { Asset2: moola(4) }, - give: { Price: simoleans(16) }, - exit: { waived: null }, - }, - }); - } - return harden({ - proposal: { - want: { Asset: moola(4) }, - give: { Price: simoleans(16) }, - exit: { onDemand: null }, - }, - }); + const mockZCFBuilder = makeMockZoeBuilder(); + mockZCFBuilder.addOffer(offerHandles[4], { + proposal: { + want: { Asset: moola(4) }, + give: { Price: simoleans(16) }, + exit: { Waived: null }, }, - complete: handles => completedOfferHandles.push(...handles), }); + mockZCFBuilder.addOffer(offerHandles[5], { + proposal: { + want: { Asset2: moola(4) }, + give: { Price: simoleans(16) }, + exit: { waived: null }, + }, + }); + + const otherOffers = harden({ + proposal: { + want: { Asset: moola(4) }, + give: { Price: simoleans(16) }, + exit: { onDemand: null }, + }, + }); + // TODO: perhaps mockZCFBuilder could have a default Offer? + mockZCFBuilder.addOffer(offerHandles[0], otherOffers); + mockZCFBuilder.addOffer(offerHandles[1], otherOffers); + mockZCFBuilder.addOffer(offerHandles[2], otherOffers); + mockZCFBuilder.addOffer(offerHandles[3], otherOffers); + const mockZCF = mockZCFBuilder.build(); const { rejectIfNotProposal } = makeZoeHelpers(mockZCF); // Vary expected. t.doesNotThrow(() => @@ -164,7 +190,7 @@ test('ZoeHelpers rejectIfNotProposal', t => { `had the wrong exit rule`, ); t.deepEquals( - completedOfferHandles, + mockZCF.getCompletedHandles(), [offerHandles[1], offerHandles[2], offerHandles[3]], `offers 1, 2, 3, (zero-indexed) should be completed`, ); @@ -197,7 +223,7 @@ test('ZoeHelpers rejectIfNotProposal', t => { `had the wrong want`, ); t.deepEquals( - completedOfferHandles, + mockZCF.getCompletedHandles(), [ offerHandles[1], offerHandles[2], @@ -214,32 +240,23 @@ test('ZoeHelpers rejectIfNotProposal', t => { test('ZoeHelpers checkIfProposal', t => { t.plan(3); - const { moolaR, simoleanR, moola, simoleans } = setup(); + const { moola, simoleans } = setup(); + const handle = {}; try { - const mockZCF = harden({ - getInstanceRecord: () => - harden({ - issuerKeywordRecord: { - Asset: moolaR.issuer, - Price: simoleanR.issuer, - }, - }), - getAmountMaths: () => {}, - getZoeService: () => {}, - getOffer: _handle => { - return harden({ - proposal: { - want: { Asset: moola(4) }, - give: { Price: simoleans(16) }, - exit: { onDemand: null }, - }, - }); + const mockZCFBuilder = makeMockZoeBuilder(); + mockZCFBuilder.addOffer(handle, { + proposal: { + want: { Asset: moola(4) }, + give: { Price: simoleans(16) }, + exit: { onDemand: null }, }, }); + const mockZCF = mockZCFBuilder.build(); + const { checkIfProposal } = makeZoeHelpers(mockZCF); t.ok( checkIfProposal( - harden({}), + handle, harden({ want: { Asset: null }, give: { Price: null }, @@ -250,7 +267,7 @@ test('ZoeHelpers checkIfProposal', t => { ); t.notOk( checkIfProposal( - harden({}), + handle, harden({ want: { Asset2: null }, give: { Price: null }, @@ -259,7 +276,7 @@ test('ZoeHelpers checkIfProposal', t => { `want was not as expected`, ); t.ok( - checkIfProposal(harden({}), harden({})), + checkIfProposal(handle, harden({})), `having no expectations passes trivially`, ); } catch (e) { @@ -269,32 +286,23 @@ test('ZoeHelpers checkIfProposal', t => { test('ZoeHelpers checkIfProposal multiple keys', t => { t.plan(2); - const { moolaR, simoleanR, bucksR, moola, simoleans, bucks } = setup(); - const mockZCF = harden({ - getInstanceRecord: () => - harden({ - issuerKeywordRecord: { - Asset: moolaR.issuer, - Fee: bucksR.issuer, - Price: simoleanR.issuer, - }, - }), - getAmountMaths: () => {}, - getZoeService: () => {}, - getOffer: _handle => { - return harden({ - proposal: { - want: { Asset: moola(4), Fee: bucks(1) }, - give: { Price: simoleans(16) }, - exit: { onDemand: null }, - }, - }); + const { moola, simoleans, bucks } = setup(); + const handle = {}; + const mockZCFBuilder = makeMockZoeBuilder(); + mockZCFBuilder.addOffer(handle, { + proposal: { + want: { Asset: moola(4), Fee: bucks(1) }, + give: { Price: simoleans(16) }, + exit: { onDemand: null }, }, }); + mockZCFBuilder.addAllocation(handle, { Asset: moola(10) }); + const mockZCF = mockZCFBuilder.build(); + const { checkIfProposal } = makeZoeHelpers(mockZCF); t.ok( checkIfProposal( - harden({}), + handle, harden({ want: { Asset: null, Fee: null }, give: { Price: null }, @@ -305,7 +313,7 @@ test('ZoeHelpers checkIfProposal multiple keys', t => { ); t.ok( checkIfProposal( - harden({}), + handle, harden({ want: { Fee: null, Asset: null }, give: { Price: null }, @@ -318,17 +326,9 @@ test('ZoeHelpers checkIfProposal multiple keys', t => { test('ZoeHelpers getActiveOffers', t => { t.plan(1); - const { moolaR, simoleanR } = setup(); try { + // uses its own mock because all it needs is a variant getOffers. const mockZCF = harden({ - getInstanceRecord: () => - harden({ - issuerKeywordRecord: { - Asset: moolaR.issuer, - Price: simoleanR.issuer, - }, - }), - getAmountMaths: () => {}, getZoeService: () => {}, getOfferStatuses: handles => { const [firstHandle, restHandles] = handles; @@ -354,18 +354,9 @@ test('ZoeHelpers getActiveOffers', t => { test('ZoeHelpers rejectOffer', t => { t.plan(4); - const { moolaR, simoleanR } = setup(); const completedOfferHandles = []; try { const mockZCF = harden({ - getInstanceRecord: () => - harden({ - issuerKeywordRecord: { - Asset: moolaR.issuer, - Price: simoleanR.issuer, - }, - }), - getAmountMaths: () => {}, getZoeService: () => {}, complete: handles => completedOfferHandles.push(...handles), }); @@ -388,136 +379,43 @@ test('ZoeHelpers rejectOffer', t => { } }); -test('ZoeHelpers canTradeWith', t => { - t.plan(2); - const { moolaR, simoleanR, moola, simoleans } = setup(); - const leftOfferHandle = harden({}); - const rightOfferHandle = harden({}); - const cantTradeRightOfferHandle = harden({}); - try { - const mockZCF = harden({ - getInstanceRecord: () => - harden({ - issuerKeywordRecord: { - Asset: moolaR.issuer, - Price: simoleanR.issuer, - }, - keywords: ['Asset', 'Price'], - }), - getAmountMaths: () => - harden({ Asset: moolaR.amountMath, Price: simoleanR.amountMath }), - getZoeService: () => {}, - getOffer: handle => { - if (handle === leftOfferHandle) { - return harden({ - proposal: { - give: { Asset: moola(10) }, - want: { Price: simoleans(4) }, - exit: { onDemand: null }, - }, - }); - } - if (handle === rightOfferHandle) { - return harden({ - proposal: { - give: { Price: simoleans(6) }, - want: { Asset: moola(7) }, - exit: { onDemand: null }, - }, - }); - } - if (handle === cantTradeRightOfferHandle) { - return harden({ - proposal: { - give: { Price: simoleans(6) }, - want: { Asset: moola(100) }, - exit: { onDemand: null }, - }, - }); - } - throw new Error('unexpected handle'); - }, - }); - const { canTradeWith } = makeZoeHelpers(mockZCF); - t.ok(canTradeWith(leftOfferHandle, rightOfferHandle)); - t.notOk(canTradeWith(leftOfferHandle, cantTradeRightOfferHandle)); - } catch (e) { - t.assert(false, e); - } -}); - test('ZoeHelpers swap ok', t => { t.plan(4); - const { moolaR, simoleanR, moola, simoleans, amountMaths } = setup(); + const { moolaR, simoleanR, moola, simoleans } = setup(); const leftOfferHandle = harden({}); const rightOfferHandle = harden({}); const cantTradeRightOfferHandle = harden({}); - const reallocatedHandles = []; - const reallocatedAmountObjs = []; - const completedHandles = []; try { - const mockZCF = harden({ - getInstanceRecord: () => - harden({ - issuerKeywordRecord: { - Asset: moolaR.issuer, - Price: simoleanR.issuer, - }, - keywords: ['Asset', 'Price'], - }), - getAmountMathForBrand: brand => amountMaths.get(brand.getAllegedName()), - getAmountMaths: () => - harden({ Asset: moolaR.amountMath, Price: simoleanR.amountMath }), - getZoeService: () => {}, - isOfferActive: () => true, - getCurrentAllocation: handle => { - if (handle === leftOfferHandle) { - return 'leftInviteAmounts'; - } - if (handle === rightOfferHandle) { - return 'rightInviteAmounts'; - } - if (handle === cantTradeRightOfferHandle) { - return 'cantTradeRightInviteAmounts'; - } - throw new Error('unexpected handle'); + const mockZCFBuilder = makeMockZoeBuilder(); + mockZCFBuilder.addBrand(moolaR); + mockZCFBuilder.addBrand(simoleanR); + mockZCFBuilder.addAllocation(leftOfferHandle, { Asset: moola(10) }); + mockZCFBuilder.addAllocation(rightOfferHandle, { Price: simoleans(6) }); + mockZCFBuilder.addAllocation(cantTradeRightOfferHandle, { + Price: simoleans(6), + }); + mockZCFBuilder.addOffer(leftOfferHandle, { + proposal: { + give: { Asset: moola(10) }, + want: { Price: simoleans(4) }, + exit: { onDemand: null }, }, - getOffer: handle => { - if (handle === leftOfferHandle) { - return harden({ - proposal: { - give: { Asset: moola(10) }, - want: { Price: simoleans(4) }, - exit: { onDemand: null }, - }, - }); - } - if (handle === rightOfferHandle) { - return harden({ - proposal: { - give: { Price: simoleans(6) }, - want: { Asset: moola(7) }, - exit: { onDemand: null }, - }, - }); - } - if (handle === cantTradeRightOfferHandle) { - return harden({ - proposal: { - give: { Price: simoleans(6) }, - want: { Asset: moola(100) }, - exit: { onDemand: null }, - }, - }); - } - throw new Error('unexpected handle'); + }); + mockZCFBuilder.addOffer(rightOfferHandle, { + proposal: { + give: { Price: simoleans(6) }, + want: { Asset: moola(7) }, + exit: { onDemand: null }, }, - reallocate: (handles, amountObjs) => { - reallocatedHandles.push(...handles); - reallocatedAmountObjs.push(...amountObjs); + }); + mockZCFBuilder.addOffer(cantTradeRightOfferHandle, { + proposal: { + give: { Price: simoleans(6) }, + want: { Asset: moola(100) }, + exit: { onDemand: null }, }, - complete: handles => completedHandles.push(...handles), }); + const mockZCF = mockZCFBuilder.build(); const { swap } = makeZoeHelpers(mockZCF); t.ok( swap( @@ -527,17 +425,20 @@ test('ZoeHelpers swap ok', t => { ), ); t.deepEquals( - reallocatedHandles, + mockZCF.getReallocatedHandles(), harden([leftOfferHandle, rightOfferHandle]), `both handles reallocated`, ); t.deepEquals( - reallocatedAmountObjs, - harden(['rightInviteAmounts', 'leftInviteAmounts']), + mockZCF.getReallocatedAmountObjs(), + [ + { Asset: moola(3), Price: simoleans(4) }, + { Price: simoleans(2), Asset: moola(7) }, + ], `amounts reallocated passed to reallocate were as expected`, ); t.deepEquals( - completedHandles, + mockZCF.getCompletedHandles(), harden([leftOfferHandle, rightOfferHandle]), `both handles were completed`, ); @@ -548,66 +449,35 @@ test('ZoeHelpers swap ok', t => { test('ZoeHelpers swap keep inactive', t => { t.plan(4); - const { moolaR, simoleanR, moola, simoleans } = setup(); + const { moola, simoleans } = setup(); const leftOfferHandle = harden({}); const rightOfferHandle = harden({}); const cantTradeRightOfferHandle = harden({}); - const reallocatedHandles = []; - const reallocatedAmountObjs = []; - const completedHandles = []; try { - const mockZCF = harden({ - getInstanceRecord: () => - harden({ - issuerKeywordRecord: { - Asset: moolaR.issuer, - Price: simoleanR.issuer, - }, - keywords: ['Asset', 'Price'], - }), - getAmountMaths: () => - harden({ Asset: moolaR.amountMath, Price: simoleanR.amountMath }), - getZoeService: () => {}, - isOfferActive: () => false, - getOffer: handle => { - if (handle === leftOfferHandle) { - return harden({ - proposal: { - give: { Asset: moola(10) }, - want: { Price: simoleans(4) }, - exit: { onDemand: null }, - }, - amounts: 'leftInviteAmounts', - }); - } - if (handle === rightOfferHandle) { - return harden({ - proposal: { - give: { Price: simoleans(6) }, - want: { Asset: moola(7) }, - exit: { onDemand: null }, - }, - amounts: 'rightInviteAmounts', - }); - } - if (handle === cantTradeRightOfferHandle) { - return harden({ - proposal: { - give: { Price: simoleans(6) }, - want: { Asset: moola(100) }, - exit: { onDemand: null }, - }, - amounts: 'cantTradeRightInviteAmounts', - }); - } - throw new Error('unexpected handle'); + const mockZCFBuilder = makeMockZoeBuilder(); + mockZCFBuilder.addOffer(leftOfferHandle, { + proposal: { + give: { Asset: moola(10) }, + want: { Price: simoleans(4) }, + exit: { onDemand: null }, }, - reallocate: (handles, amountObjs) => { - reallocatedHandles.push(...handles); - reallocatedAmountObjs.push(...amountObjs); + }); + mockZCFBuilder.addOffer(rightOfferHandle, { + proposal: { + give: { Price: simoleans(6) }, + want: { Asset: moola(7) }, + exit: { onDemand: null }, }, - complete: handles => handles.map(handle => completedHandles.push(handle)), }); + mockZCFBuilder.addOffer(cantTradeRightOfferHandle, { + proposal: { + give: { Price: simoleans(6) }, + want: { Asset: moola(100) }, + exit: { onDemand: null }, + }, + }); + mockZCFBuilder.setOffersInactive(); + const mockZCF = mockZCFBuilder.build(); const { swap } = makeZoeHelpers(mockZCF); t.throws( () => @@ -619,10 +489,12 @@ test('ZoeHelpers swap keep inactive', t => { /Error: prior offer no longer available/, `throws if keepHandle offer is not active`, ); + const reallocatedHandles = mockZCF.getReallocatedHandles(); t.deepEquals(reallocatedHandles, harden([]), `nothing reallocated`); + const reallocatedAmountObjs = mockZCF.getReallocatedAmountObjs(); t.deepEquals(reallocatedAmountObjs, harden([]), `no amounts reallocated`); t.deepEquals( - completedHandles, + mockZCF.getCompletedHandles(), harden([rightOfferHandle]), `only tryHandle (right) was completed`, ); @@ -636,76 +508,53 @@ test(`ZoeHelpers swap - can't trade with`, t => { const { moolaR, simoleanR, moola, simoleans } = setup(); const leftOfferHandle = harden({}); const rightOfferHandle = harden({}); - const cantTradeRightOfferHandle = harden({}); - const reallocatedHandles = []; - const reallocatedAmountObjs = []; - const completedHandles = []; + const cantTradeHandle = harden({}); + try { - const mockZCF = harden({ - getInstanceRecord: () => - harden({ - issuerKeywordRecord: { - Asset: moolaR.issuer, - Price: simoleanR.issuer, - }, - keywords: ['Asset', 'Price'], - }), - getAmountMaths: () => - harden({ Asset: moolaR.amountMath, Price: simoleanR.amountMath }), - getZoeService: () => {}, - isOfferActive: () => true, - getOffer: handle => { - if (handle === leftOfferHandle) { - return harden({ - proposal: { - give: { Asset: moola(10) }, - want: { Price: simoleans(4) }, - exit: { onDemand: null }, - }, - amounts: 'leftInviteAmounts', - }); - } - if (handle === rightOfferHandle) { - return harden({ - proposal: { - give: { Price: simoleans(6) }, - want: { Asset: moola(7) }, - exit: { onDemand: null }, - }, - amounts: 'rightInviteAmounts', - }); - } - if (handle === cantTradeRightOfferHandle) { - return harden({ - proposal: { - give: { Price: simoleans(6) }, - want: { Asset: moola(100) }, - exit: { onDemand: null }, - }, - amounts: 'cantTradeRightInviteAmounts', - }); - } - throw new Error('unexpected handle'); + const mockZCFBuilder = makeMockZoeBuilder(); + mockZCFBuilder.addBrand(moolaR); + mockZCFBuilder.addBrand(simoleanR); + mockZCFBuilder.addOffer(leftOfferHandle, { + proposal: { + give: { Asset: moola(10) }, + want: { Price: simoleans(4) }, + exit: { onDemand: null }, }, - reallocate: (handles, amountObjs) => { - reallocatedHandles.push(...handles); - reallocatedAmountObjs.push(...amountObjs); + }); + mockZCFBuilder.addOffer(rightOfferHandle, { + proposal: { + give: { Price: simoleans(6) }, + want: { Asset: moola(7) }, + exit: { onDemand: null }, }, - complete: handles => handles.map(handle => completedHandles.push(handle)), }); - const { swap } = makeZoeHelpers(mockZCF); + mockZCFBuilder.addOffer(cantTradeHandle, { + proposal: { + give: { Price: simoleans(6) }, + want: { Asset: moola(100) }, + exit: { onDemand: null }, + }, + }); + mockZCFBuilder.addAllocation(leftOfferHandle, { Asset: moola(10) }); + mockZCFBuilder.addAllocation(rightOfferHandle, { Price: simoleans(6) }); + mockZCFBuilder.addAllocation(cantTradeHandle, { Price: simoleans(6) }); + const mockZcf = mockZCFBuilder.build(); + const { swap } = makeZoeHelpers(mockZcf); t.throws( () => swap( leftOfferHandle, - cantTradeRightOfferHandle, + cantTradeHandle, 'prior offer no longer available', ), /Error: The offer was invalid. Please check your refund./, `throws if can't trade with left and right`, ); + const reallocatedHandles = mockZcf.getReallocatedHandles(); t.deepEquals(reallocatedHandles, harden([]), `nothing reallocated`); + const reallocatedAmountObjs = mockZcf.getReallocatedAmountObjs(); t.deepEquals(reallocatedAmountObjs, harden([]), `no amounts reallocated`); + const completedHandles = mockZcf.getCompletedHandles(); t.deepEquals( completedHandles, harden([rightOfferHandle]), @@ -730,7 +579,6 @@ test('ZoeHelpers makeEmptyOffer', async t => { Price: simoleanR.issuer, }, }), - getAmountMaths: () => {}, getZoeService: () => harden({ offer: invite => { @@ -751,3 +599,236 @@ test('ZoeHelpers makeEmptyOffer', async t => { t.assert(false, e); } }); + +test('ZoeHelpers isOfferSafe', t => { + t.plan(5); + const { moolaR, simoleanR, moola, simoleans } = setup(); + const leftOfferHandle = harden({}); + const rightOfferHandle = harden({}); + const cantTradeRightOfferHandle = harden({}); + const reallocatedHandles = []; + const reallocatedAmountObjs = []; + const completedHandles = []; + try { + const mockZCFBuilder = makeMockZoeBuilder(); + mockZCFBuilder.addBrand(moolaR); + mockZCFBuilder.addBrand(simoleanR); + mockZCFBuilder.addAllocation(leftOfferHandle, { Asset: moola(10) }); + mockZCFBuilder.addAllocation(rightOfferHandle, { Price: simoleans(6) }); + mockZCFBuilder.addAllocation(cantTradeRightOfferHandle, { + Price: simoleans(6), + }); + mockZCFBuilder.addOffer(leftOfferHandle, { + proposal: { + give: { Asset: moola(10) }, + want: { Price: simoleans(4) }, + exit: { onDemand: null }, + }, + }); + const mockZCF = mockZCFBuilder.build(); + const { isOfferSafe } = makeZoeHelpers(mockZCF); + t.ok( + isOfferSafe(leftOfferHandle, { + Asset: moola(0), + Price: simoleans(4), + }), + `giving someone exactly what they want is offer safe`, + ); + t.notOk( + isOfferSafe(leftOfferHandle, { + Asset: moola(0), + Price: simoleans(3), + }), + `giving someone less than what they want and not what they gave is not offer safe`, + ); + t.deepEquals(reallocatedHandles, harden([]), `nothing reallocated`); + t.deepEquals(reallocatedAmountObjs, harden([]), `no amounts reallocated`); + t.deepEquals(completedHandles, harden([]), `no offers completed`); + } catch (e) { + t.assert(false, e); + } +}); + +test('ZoeHelpers satisfies', t => { + t.plan(6); + const { moolaR, simoleanR, moola, simoleans } = setup(); + const leftOfferHandle = harden({}); + const rightOfferHandle = harden({}); + const cantTradeRightOfferHandle = harden({}); + const reallocatedHandles = []; + const reallocatedAmountObjs = []; + const completedHandles = []; + try { + const mockZCFBuilder = makeMockZoeBuilder(); + mockZCFBuilder.addBrand(moolaR); + mockZCFBuilder.addBrand(simoleanR); + mockZCFBuilder.addAllocation(leftOfferHandle, { Asset: moola(10) }); + mockZCFBuilder.addAllocation(rightOfferHandle, { Price: simoleans(6) }); + mockZCFBuilder.addAllocation(cantTradeRightOfferHandle, { + Price: simoleans(6), + }); + mockZCFBuilder.addOffer(leftOfferHandle, { + proposal: { + give: { Asset: moola(10) }, + want: { Price: simoleans(4) }, + exit: { onDemand: null }, + }, + }); + const mockZCF = mockZCFBuilder.build(); + const { satisfies } = makeZoeHelpers(mockZCF); + t.ok( + satisfies(leftOfferHandle, { + Asset: moola(0), + Price: simoleans(4), + }), + `giving someone exactly what they want satisifies wants`, + ); + t.notOk( + satisfies(leftOfferHandle, { + Asset: moola(10), + Price: simoleans(3), + }), + `giving someone less than what they want even with a refund doesn't satisfy wants`, + ); + t.notOk( + satisfies(leftOfferHandle, { + Asset: moola(0), + Price: simoleans(3), + }), + `giving someone less than what they want even with a refund doesn't satisfy wants`, + ); + t.deepEquals(reallocatedHandles, harden([]), `nothing reallocated`); + t.deepEquals(reallocatedAmountObjs, harden([]), `no amounts reallocated`); + t.deepEquals(completedHandles, harden([]), `no offers completed`); + } catch (e) { + t.assert(false, e); + } +}); + +test('ZoeHelpers trade ok', t => { + t.plan(4); + const { moolaR, simoleanR, moola, simoleans } = setup(); + const leftOfferHandle = harden({}); + const rightOfferHandle = harden({}); + try { + const mockZCFBuilder = makeMockZoeBuilder(); + mockZCFBuilder.addBrand(moolaR); + mockZCFBuilder.addBrand(simoleanR); + mockZCFBuilder.addAllocation(leftOfferHandle, { Asset: moola(10) }); + mockZCFBuilder.addAllocation(rightOfferHandle, { Money: simoleans(6) }); + mockZCFBuilder.addOffer(leftOfferHandle, { + proposal: { + give: { Asset: moola(10) }, + want: { Bid: simoleans(4) }, + exit: { onDemand: null }, + }, + }); + mockZCFBuilder.addOffer(rightOfferHandle, { + proposal: { + give: { Money: simoleans(6) }, + want: { Items: moola(7) }, + exit: { onDemand: null }, + }, + }); + const mockZCF = mockZCFBuilder.build(); + const { trade } = makeZoeHelpers(mockZCF); + t.doesNotThrow(() => + trade( + { + offerHandle: leftOfferHandle, + gains: { Bid: simoleans(4) }, + losses: { Asset: moola(7) }, + }, + { + offerHandle: rightOfferHandle, + gains: { Items: moola(7) }, + losses: { Money: simoleans(4) }, + }, + ), + ); + t.deepEquals( + mockZCF.getReallocatedHandles(), + harden([leftOfferHandle, rightOfferHandle]), + `both handles reallocated`, + ); + t.deepEquals( + mockZCF.getReallocatedAmountObjs(), + [ + { Asset: moola(3), Bid: simoleans(4) }, + { Money: simoleans(2), Items: moola(7) }, + ], + `amounts reallocated passed to reallocate were as expected`, + ); + t.deepEquals( + mockZCF.getCompletedHandles(), + harden([]), + `no handles were completed`, + ); + } catch (e) { + t.assert(false, e); + } +}); + +test('ZoeHelpers trade sameHandle', t => { + t.plan(4); + const { moolaR, simoleanR, moola, simoleans } = setup(); + const leftOfferHandle = harden({}); + const rightOfferHandle = harden({}); + try { + const mockZCFBuilder = makeMockZoeBuilder(); + mockZCFBuilder.addBrand(moolaR); + mockZCFBuilder.addBrand(simoleanR); + mockZCFBuilder.addAllocation(leftOfferHandle, { Asset: moola(10) }); + mockZCFBuilder.addAllocation(rightOfferHandle, { Money: simoleans(6) }); + mockZCFBuilder.addOffer(leftOfferHandle, { + proposal: { + give: { Asset: moola(10) }, + want: { Bid: simoleans(4) }, + exit: { onDemand: null }, + }, + }); + mockZCFBuilder.addOffer(rightOfferHandle, { + proposal: { + give: { Money: simoleans(6) }, + want: { Items: moola(7) }, + exit: { onDemand: null }, + }, + }); + const mockZCF = mockZCFBuilder.build(); + const { trade } = makeZoeHelpers(mockZCF); + t.throws( + () => + trade( + { + offerHandle: leftOfferHandle, + gains: { Bid: simoleans(4) }, + losses: { Asset: moola(7) }, + }, + { + offerHandle: leftOfferHandle, + gains: { Items: moola(7) }, + losses: { Money: simoleans(4) }, + }, + ), + /an offer cannot trade with itself/, + `safe offer trading with itself fails with nice error message`, + ); + t.deepEquals( + mockZCF.getReallocatedHandles(), + harden([]), + `no handles reallocated`, + ); + t.deepEquals( + mockZCF.getReallocatedAmountObjs(), + [], + `no amounts reallocated`, + ); + t.deepEquals( + mockZCF.getCompletedHandles(), + harden([]), + `no handles were completed`, + ); + } catch (e) { + t.assert(false, e); + } +}); diff --git a/packages/zoe/test/unitTests/contracts/brokenAutoRefund.js b/packages/zoe/test/unitTests/contracts/brokenAutoRefund.js index 50f2e034021..3f3152d5be6 100644 --- a/packages/zoe/test/unitTests/contracts/brokenAutoRefund.js +++ b/packages/zoe/test/unitTests/contracts/brokenAutoRefund.js @@ -1,24 +1,20 @@ // @ts-check -import rawHarden from '@agoric/harden'; - -// TODO: Until we have a version of harden that exports its type. -const harden = /** @type {(x: T) => T} */ (rawHarden); +import harden from '@agoric/harden'; /** * This is a a broken contact to test zoe's error handling * @type {import('@agoric/zoe').MakeContract} zoe - the contract facet of zoe */ -export const makeContract = harden(zcf => { +const makeContract = zcf => { const refundOfferHook = offerHandle => { zcf.complete(harden([offerHandle])); return `The offer was accepted`; }; const makeRefundInvite = () => zcf.makeInvitation(refundOfferHook, 'getRefund'); + // should be makeRefundInvite(). Intentionally wrong to provoke an error. + return makeRefundInvite; +}; - return harden({ - // should be makeRefundInvite(). Intentionally wrong to provoke an error. - invite: makeRefundInvite, - publicAPI: {}, - }); -}); +harden(makeContract); +export { makeContract }; diff --git a/packages/zoe/test/unitTests/contracts/grifter.js b/packages/zoe/test/unitTests/contracts/grifter.js index 180e3547e21..6cd0150ae46 100644 --- a/packages/zoe/test/unitTests/contracts/grifter.js +++ b/packages/zoe/test/unitTests/contracts/grifter.js @@ -26,7 +26,7 @@ export const makeContract = harden( const stepOne = [wantProposal, vicProposal]; // safe because it doesn't change want, so winningsOK looks true const offerHandles = [firstOfferHandle, offerHandle]; - zcf.reallocate(offerHandles, stepOne, ['Price']); + zcf.reallocate(offerHandles, stepOne); zcf.complete(harden(offerHandles)); }, 'tantalizing offer', @@ -42,11 +42,9 @@ export const makeContract = harden( want: { Price: null }, }); - return harden({ - invite: zcf.makeInvitation( - checkHook(makeAccompliceInvite, firstOfferExpected), - 'firstOffer', - ), - }); + return zcf.makeInvitation( + checkHook(makeAccompliceInvite, firstOfferExpected), + 'firstOffer', + ); }, ); diff --git a/packages/zoe/test/unitTests/contracts/test-atomicSwap.js b/packages/zoe/test/unitTests/contracts/test-atomicSwap.js index 912f500c598..b81a011f828 100644 --- a/packages/zoe/test/unitTests/contracts/test-atomicSwap.js +++ b/packages/zoe/test/unitTests/contracts/test-atomicSwap.js @@ -45,7 +45,7 @@ test('zoe - atomicSwap', async t => { Asset: moolaIssuer, Price: simoleanIssuer, }); - const aliceInvite = await zoe.makeInstance( + const { invite: aliceInvite } = await zoe.makeInstance( installationHandle, issuerKeywordRecord, ); @@ -175,7 +175,7 @@ test('zoe - non-fungible atomicSwap', async t => { Asset: ccIssuer, Price: rpgIssuer, }); - const aliceInvite = await zoe.makeInstance( + const { invite: aliceInvite } = await zoe.makeInstance( installationHandle, issuerKeywordRecord, ); @@ -269,3 +269,122 @@ test('zoe - non-fungible atomicSwap', async t => { t.deepEquals(bobCcPurse.getCurrentAmount().extent, ['calico #37']); t.deepEquals(bobRpgPurse.getCurrentAmount().extent, []); }); + +// Checking handling of duplicate issuers. I'd have preferred a raffle contract +test('zoe - atomicSwap like-for-like', async t => { + t.plan(13); + const { moolaIssuer, moolaMint, moola } = setup(); + const zoe = makeZoe(); + const inviteIssuer = zoe.getInviteIssuer(); + + // pack the contract + const bundle = await bundleSource(atomicSwapRoot); + // install the contract + const installationHandle = await zoe.install(bundle); + + // Setup Alice + const aliceMoolaPayment = moolaMint.mintPayment(moola(3)); + const aliceMoolaPurse = moolaIssuer.makeEmptyPurse(); + + // Setup Bob + const bobMoolaPayment = moolaMint.mintPayment(moola(7)); + const bobMoolaPurse = moolaIssuer.makeEmptyPurse(); + + // 1: Alice creates an atomicSwap instance + const issuerKeywordRecord = harden({ + Asset: moolaIssuer, + Price: moolaIssuer, + }); + const { invite: aliceInvite } = await zoe.makeInstance( + installationHandle, + issuerKeywordRecord, + ); + + // 2: Alice escrows with zoe + const aliceProposal = harden({ + give: { Asset: moola(3) }, + want: { Price: moola(7) }, + exit: { onDemand: null }, + }); + const alicePayments = { Asset: aliceMoolaPayment }; + + // 3: Alice makes the first offer in the swap. + const { payout: alicePayoutP, outcome: bobInviteP } = await zoe.offer( + aliceInvite, + aliceProposal, + alicePayments, + ); + + // 4: Alice spreads the invite far and wide with instructions + // on how to use it and Bob decides he wants to be the + // counter-party. + + const bobExclusiveInvite = await inviteIssuer.claim(bobInviteP); + const { + extent: [bobInviteExtent], + } = await inviteIssuer.getAmountOf(bobExclusiveInvite); + + const { + installationHandle: bobInstallationId, + issuerKeywordRecord: bobIssuers, + } = zoe.getInstanceRecord(bobInviteExtent.instanceHandle); + + t.equals(bobInstallationId, installationHandle, 'bobInstallationId'); + t.deepEquals(bobIssuers, { Asset: moolaIssuer, Price: moolaIssuer }); + t.deepEquals(bobInviteExtent.asset, moola(3)); + t.deepEquals(bobInviteExtent.price, moola(7)); + + const bobProposal = harden({ + give: { Price: moola(7) }, + want: { Asset: moola(3) }, + exit: { onDemand: null }, + }); + const bobPayments = { Price: bobMoolaPayment }; + + // 5: Bob makes an offer + const { payout: bobPayoutP, outcome: bobOutcomeP } = await zoe.offer( + bobExclusiveInvite, + bobProposal, + bobPayments, + ); + + t.equals( + await bobOutcomeP, + 'The offer has been accepted. Once the contract has been completed, please check your payout', + ); + const bobPayout = await bobPayoutP; + const alicePayout = await alicePayoutP; + + const bobAssetPayout = await bobPayout.Asset; + const bobPricePayout = await bobPayout.Price; + + const aliceAssetPayout = await alicePayout.Asset; + const alicePricePayout = await alicePayout.Price; + + // Alice gets what Alice wanted + t.deepEquals( + await moolaIssuer.getAmountOf(alicePricePayout), + aliceProposal.want.Price, + ); + + // Alice didn't get any of what Alice put in + t.deepEquals(await moolaIssuer.getAmountOf(aliceAssetPayout), moola(0)); + + // Alice deposits her payout to ensure she can + const aliceAssetAmount = await aliceMoolaPurse.deposit(aliceAssetPayout); + t.equals(aliceAssetAmount.extent, 0); + const alicePriceAmount = await aliceMoolaPurse.deposit(alicePricePayout); + t.equals(alicePriceAmount.extent, 7); + + // Bob deposits his original payments to ensure he can + const bobAssetAmount = await bobMoolaPurse.deposit(bobAssetPayout); + t.equals(bobAssetAmount.extent, 3); + const bobPriceAmount = await bobMoolaPurse.deposit(bobPricePayout); + t.equals(bobPriceAmount.extent, 0); + + // Assert that the correct payouts were received. + // Alice had 3 moola from Asset and 0 from Price. + // Bob had 0 moola from Asset and 7 from Price. + t.equals(aliceMoolaPurse.getCurrentAmount().extent, 7); + t.equals(bobMoolaPurse.getCurrentAmount().extent, 3); +}); diff --git a/packages/zoe/test/unitTests/contracts/test-automaticRefund.js b/packages/zoe/test/unitTests/contracts/test-automaticRefund.js index 6d36aecd0f7..4b9cf75834d 100644 --- a/packages/zoe/test/unitTests/contracts/test-automaticRefund.js +++ b/packages/zoe/test/unitTests/contracts/test-automaticRefund.js @@ -1,3 +1,4 @@ +// eslint-disable-next-line import/no-extraneous-dependencies import '@agoric/install-ses'; // eslint-disable-next-line import/no-extraneous-dependencies import { test } from 'tape-promise/tape'; @@ -7,7 +8,6 @@ import bundleSource from '@agoric/bundle-source'; import harden from '@agoric/harden'; import { makeZoe } from '../../../src/zoe'; -// TODO: Remove setupBasicMints and rename setupBasicMints2 import { setup } from '../setupBasicMints'; import { makeGetInstanceHandle } from '../../../src/clientSupport'; import { setupNonFungible } from '../setupNonFungibleMints'; @@ -29,7 +29,7 @@ test('zoe - simplest automaticRefund', async t => { // 1: Alice creates an automatic refund instance const issuerKeywordRecord = harden({ Contribution: moolaR.issuer }); - const invite = await zoe.makeInstance( + const { invite } = await zoe.makeInstance( installationHandle, issuerKeywordRecord, ); @@ -78,7 +78,7 @@ test('zoe - automaticRefund same issuer', async t => { Contribution1: moolaR.issuer, Contribution2: moolaR.issuer, }); - const invite = await zoe.makeInstance( + const { invite } = await zoe.makeInstance( installationHandle, issuerKeywordRecord, ); @@ -137,12 +137,10 @@ test('zoe with automaticRefund', async t => { Contribution1: moolaR.issuer, Contribution2: simoleanR.issuer, }); - const aliceInvite = await zoe.makeInstance( - installationHandle, - issuerKeywordRecord, - ); - const instanceHandle = await getInstanceHandle(aliceInvite); - const { publicAPI } = zoe.getInstanceRecord(instanceHandle); + const { + invite: aliceInvite, + instanceRecord: { publicAPI }, + } = await zoe.makeInstance(installationHandle, issuerKeywordRecord); // 2: Alice escrows with zoe const aliceProposal = harden({ @@ -283,14 +281,14 @@ test('multiple instances of automaticRefund for the same Zoe', async t => { }); const inviteIssuer = zoe.getInviteIssuer(); const getInstanceHandle = makeGetInstanceHandle(inviteIssuer); - const aliceInvite1 = await zoe.makeInstance( + const { invite: aliceInvite1 } = await zoe.makeInstance( installationHandle, issuerKeywordRecord, ); const instanceHandle1 = await getInstanceHandle(aliceInvite1); const { publicAPI: publicAPI1 } = zoe.getInstanceRecord(instanceHandle1); - const aliceInvite2 = await zoe.makeInstance( + const { invite: aliceInvite2 } = await zoe.makeInstance( installationHandle, issuerKeywordRecord, ); @@ -299,7 +297,7 @@ test('multiple instances of automaticRefund for the same Zoe', async t => { } = await inviteIssuer.getAmountOf(aliceInvite2); const { publicAPI: publicAPI2 } = zoe.getInstanceRecord(instanceHandle2); - const aliceInvite3 = await zoe.makeInstance( + const { invite: aliceInvite3 } = await zoe.makeInstance( installationHandle, issuerKeywordRecord, ); @@ -367,7 +365,7 @@ test('multiple instances of automaticRefund for the same Zoe', async t => { } }); -test('zoe - alice cancels after completion', async t => { +test('zoe - alice tries to complete after completion has already occurred', async t => { t.plan(5); try { // Setup zoe and mints @@ -386,7 +384,7 @@ test('zoe - alice cancels after completion', async t => { ContributionA: moolaR.issuer, ContributionB: simoleanR.issuer, }); - const invite = await zoe.makeInstance( + const { invite } = await zoe.makeInstance( installationHandle, issuerKeywordRecord, ); @@ -455,7 +453,7 @@ test('zoe - automaticRefund non-fungible', async t => { // 1: Alice creates an automatic refund instance const issuerKeywordRecord = harden({ Contribution: ccIssuer }); - const invite = await zoe.makeInstance( + const { invite } = await zoe.makeInstance( installationHandle, issuerKeywordRecord, ); diff --git a/packages/zoe/test/unitTests/contracts/test-autoswap.js b/packages/zoe/test/unitTests/contracts/test-autoswap.js index c080d0615fc..042f13dd905 100644 --- a/packages/zoe/test/unitTests/contracts/test-autoswap.js +++ b/packages/zoe/test/unitTests/contracts/test-autoswap.js @@ -46,12 +46,10 @@ test('autoSwap with valid offers', async t => { TokenA: moolaIssuer, TokenB: simoleanIssuer, }); - const aliceInvite = await zoe.makeInstance( - installationHandle, - issuerKeywordRecord, - ); - const instanceHandle = await getInstanceHandle(aliceInvite); - const { publicAPI } = zoe.getInstanceRecord(instanceHandle); + const { + invite: aliceInvite, + instanceRecord: { publicAPI }, + } = await zoe.makeInstance(installationHandle, issuerKeywordRecord); const liquidityIssuer = publicAPI.getLiquidityIssuer(); const liquidity = liquidityIssuer.getAmountMath().make; @@ -72,7 +70,7 @@ test('autoSwap with valid offers', async t => { outcome: liquidityOkP, } = await zoe.offer(aliceInvite, aliceProposal, alicePayments); - t.equals(await liquidityOkP, 'Added liquidity.'); + t.equals(await liquidityOkP, 'Added liquidity.', `added liquidity message`); const liquidityPayments = await aliceAddLiquidityPayoutP; const liquidityPayout = await liquidityPayments.Liquidity; @@ -80,12 +78,17 @@ test('autoSwap with valid offers', async t => { t.deepEquals( await liquidityIssuer.getAmountOf(liquidityPayout), liquidity(10), + `liquidity payout`, + ); + t.deepEquals( + publicAPI.getPoolAllocation(), + { + TokenA: moola(10), + TokenB: simoleans(5), + Liquidity: liquidity(0), + }, + `pool allocation`, ); - t.deepEquals(publicAPI.getPoolAllocation(), { - TokenA: moola(10), - TokenB: simoleans(5), - Liquidity: liquidity(0), - }); // Alice creates an invite for autoswap and sends it to Bob const bobInvite = publicAPI.makeSwapInvite(); @@ -97,21 +100,22 @@ test('autoSwap with valid offers', async t => { publicAPI: bobAutoswap, installationHandle: bobInstallationId, } = zoe.getInstanceRecord(bobInstanceHandle); - t.equals(bobInstallationId, installationHandle); + t.equals(bobInstallationId, installationHandle, `installationHandle`); // Bob looks up the price of 3 moola in simoleans const simoleanAmounts = bobAutoswap.getCurrentPrice( - harden({ TokenA: moola(3) }), + moola(3), + simoleans(0).brand, ); - t.deepEquals(simoleanAmounts, simoleans(1)); + t.deepEquals(simoleanAmounts, simoleans(1), `currentPrice`); // Bob escrows const bobMoolaForSimProposal = harden({ - want: { TokenB: simoleans(1) }, - give: { TokenA: moola(3) }, + want: { Out: simoleans(1) }, + give: { In: moola(3) }, }); - const bobMoolaForSimPayments = harden({ TokenA: bobMoolaPayment }); + const bobMoolaForSimPayments = harden({ In: bobMoolaPayment }); const { payout: bobPayoutP, outcome: offerOkP } = await zoe.offer( bobExclInvite, @@ -120,37 +124,47 @@ test('autoSwap with valid offers', async t => { ); // Bob swaps - t.equal(await offerOkP, 'Swap successfully completed.'); + t.equal(await offerOkP, 'Swap successfully completed.', `swap message 1`); const bobPayout = await bobPayoutP; - const bobMoolaPayout1 = await bobPayout.TokenA; - const bobSimoleanPayout1 = await bobPayout.TokenB; + const bobMoolaPayout1 = await bobPayout.In; + const bobSimoleanPayout1 = await bobPayout.Out; - t.deepEqual(await moolaIssuer.getAmountOf(bobMoolaPayout1), moola(0)); + t.deepEqual( + await moolaIssuer.getAmountOf(bobMoolaPayout1), + moola(0), + `bob moola`, + ); t.deepEqual( await simoleanIssuer.getAmountOf(bobSimoleanPayout1), simoleans(1), + `bob simoleans`, + ); + t.deepEquals( + bobAutoswap.getPoolAllocation(), + { + TokenA: moola(13), + TokenB: simoleans(4), + Liquidity: liquidity(0), + }, + `pool allocation`, ); - t.deepEquals(bobAutoswap.getPoolAllocation(), { - TokenA: moola(13), - TokenB: simoleans(4), - Liquidity: liquidity(0), - }); // Bob looks up the price of 3 simoleans const moolaAmounts = bobAutoswap.getCurrentPrice( - harden({ TokenB: simoleans(3) }), + simoleans(3), + moola(0).brand, ); - t.deepEquals(moolaAmounts, moola(5)); + t.deepEquals(moolaAmounts, moola(5), `price 2`); // Bob makes another offer and swaps const bobSecondInvite = bobAutoswap.makeSwapInvite(); const bobSimsForMoolaProposal = harden({ - want: { TokenA: moola(5) }, - give: { TokenB: simoleans(3) }, + want: { Out: moola(5) }, + give: { In: simoleans(3) }, }); - const simsForMoolaPayments = harden({ TokenB: bobSimoleanPayment }); + const simsForMoolaPayments = harden({ In: bobSimoleanPayment }); const { payout: bobSimsForMoolaPayoutP, @@ -161,11 +175,15 @@ test('autoSwap with valid offers', async t => { simsForMoolaPayments, ); - t.equal(await simsForMoolaOkP, 'Swap successfully completed.'); + t.equal( + await simsForMoolaOkP, + 'Swap successfully completed.', + `swap message 2`, + ); const bobSimsForMoolaPayout = await bobSimsForMoolaPayoutP; - const bobMoolaPayout2 = await bobSimsForMoolaPayout.TokenA; - const bobSimoleanPayout2 = await bobSimsForMoolaPayout.TokenB; + const bobMoolaPayout2 = await bobSimsForMoolaPayout.Out; + const bobSimoleanPayout2 = await bobSimsForMoolaPayout.In; t.deepEqual(await moolaIssuer.getAmountOf(bobMoolaPayout2), moola(5)); t.deepEqual( @@ -254,12 +272,10 @@ test('autoSwap - test fee', async t => { TokenA: moolaIssuer, TokenB: simoleanIssuer, }); - const aliceAddLiquidityInvite = await zoe.makeInstance( - installationHandle, - issuerKeywordRecord, - ); - const instanceHandle = await getInstanceHandle(aliceAddLiquidityInvite); - const { publicAPI } = zoe.getInstanceRecord(instanceHandle); + const { + invite: aliceAddLiquidityInvite, + instanceRecord: { publicAPI }, + } = await zoe.makeInstance(installationHandle, issuerKeywordRecord); const liquidityIssuer = publicAPI.getLiquidityIssuer(); const liquidity = liquidityIssuer.getAmountMath().make; @@ -310,16 +326,17 @@ test('autoSwap - test fee', async t => { // Bob looks up the price of 1000 moola in simoleans const simoleanAmounts = bobAutoswap.getCurrentPrice( - harden({ TokenA: moola(1000) }), + moola(1000), + simoleans(0).brand, ); - t.deepEquals(simoleanAmounts, simoleans(906)); + t.deepEquals(simoleanAmounts, simoleans(906), `simoleans out`); // Bob escrows const bobMoolaForSimProposal = harden({ - give: { TokenA: moola(1000) }, - want: { TokenB: simoleans(0) }, + give: { In: moola(1000) }, + want: { Out: simoleans(0) }, }); - const bobMoolaForSimPayments = harden({ TokenA: bobMoolaPayment }); + const bobMoolaForSimPayments = harden({ In: bobMoolaPayment }); // Bob swaps const { payout: bobPayoutP, outcome: offerOkP } = await zoe.offer( @@ -331,8 +348,8 @@ test('autoSwap - test fee', async t => { t.equal(await offerOkP, 'Swap successfully completed.'); const bobPayout = await bobPayoutP; - const bobMoolaPayout = await bobPayout.TokenA; - const bobSimoleanPayout = await bobPayout.TokenB; + const bobMoolaPayout = await bobPayout.In; + const bobSimoleanPayout = await bobPayout.Out; t.deepEqual(await moolaIssuer.getAmountOf(bobMoolaPayout), moola(0)); t.deepEqual( diff --git a/packages/zoe/test/unitTests/contracts/test-barter.js b/packages/zoe/test/unitTests/contracts/test-barter.js new file mode 100644 index 00000000000..c232b573881 --- /dev/null +++ b/packages/zoe/test/unitTests/contracts/test-barter.js @@ -0,0 +1,145 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import '@agoric/install-ses'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { test } from 'tape-promise/tape'; +// eslint-disable-next-line import/no-extraneous-dependencies +import bundleSource from '@agoric/bundle-source'; + +import harden from '@agoric/harden'; + +import { makeZoe } from '../../../src/zoe'; +import { setup } from '../setupBasicMints'; +import { makeGetInstanceHandle } from '../../../src/clientSupport'; + +const barter = `${__dirname}/../../../src/contracts/barterExchange`; + +test('barter with valid offers', async t => { + t.plan(9); + const { + moolaIssuer, + simoleanIssuer, + moolaMint, + simoleanMint, + amountMaths, + moola, + simoleans, + } = setup(); + const zoe = makeZoe(); + const inviteIssuer = zoe.getInviteIssuer(); + const getInstanceHandle = makeGetInstanceHandle(inviteIssuer); + + // Pack the contract. + const bundle = await bundleSource(barter); + + const installationHandle = await zoe.install(bundle); + + // Setup Alice + const aliceMoolaPayment = moolaMint.mintPayment(moola(3)); + const aliceMoolaPurse = moolaIssuer.makeEmptyPurse(); + const aliceSimoleanPurse = simoleanIssuer.makeEmptyPurse(); + + // Setup Bob + const bobSimoleanPayment = simoleanMint.mintPayment(simoleans(7)); + const bobMoolaPurse = moolaIssuer.makeEmptyPurse(); + const bobSimoleanPurse = simoleanIssuer.makeEmptyPurse(); + + // 1: Simon creates a barter instance and spreads the invite far and + // wide with instructions on how to use it. + const { invite: simonInvite } = await zoe.makeInstance(installationHandle, { + Asset: moolaIssuer, + Price: simoleanIssuer, + }); + const instanceHandle = await getInstanceHandle(simonInvite); + const { publicAPI } = zoe.getInstanceRecord(instanceHandle); + + const aliceInvite = publicAPI.makeInvite(); + + // 2: Alice escrows with zoe to create a sell order. She wants to + // sell 3 moola and wants to receive at least 4 simoleans in + // return. + const aliceSellOrderProposal = harden({ + give: { In: moola(3) }, + want: { Out: simoleans(4) }, + exit: { onDemand: null }, + }); + const alicePayments = { In: aliceMoolaPayment }; + // 4: Alice adds her sell order to the exchange + const { payout: alicePayoutP, outcome: aliceOutcomeP } = await zoe.offer( + aliceInvite, + aliceSellOrderProposal, + alicePayments, + ); + + const bobInvite = publicAPI.makeInvite(); + + // 5: Bob decides to join. + const bobExclusiveInvite = await inviteIssuer.claim(bobInvite); + + const { installationHandle: bobInstallationId } = zoe.getInstanceRecord( + instanceHandle, + ); + + t.equals(bobInstallationId, installationHandle); + + // Bob creates a buy order, saying that he wants exactly 3 moola, + // and is willing to pay up to 7 simoleans. + const bobBuyOrderProposal = harden({ + give: { In: simoleans(7) }, + want: { Out: moola(3) }, + exit: { onDemand: null }, + }); + const bobPayments = { In: bobSimoleanPayment }; + + // 6: Bob escrows with zoe + // 8: Bob submits the buy order to the exchange + const { payout: bobPayoutP, outcome: bobOutcomeP } = await zoe.offer( + bobExclusiveInvite, + bobBuyOrderProposal, + bobPayments, + ); + + t.equals( + await bobOutcomeP, + 'The offer has been accepted. Once the contract has been completed, please check your payout', + ); + t.equals( + await aliceOutcomeP, + 'The offer has been accepted. Once the contract has been completed, please check your payout', + ); + const bobPayout = await bobPayoutP; + const alicePayout = await alicePayoutP; + + const bobMoolaPayout = await bobPayout.Out; + const bobSimoleanPayout = await bobPayout.In; + const aliceMoolaPayout = await alicePayout.In; + const aliceSimoleanPayout = await alicePayout.Out; + + // Alice gets paid at least what she wanted + t.ok( + amountMaths + .get('simoleans') + .isGTE( + await simoleanIssuer.getAmountOf(aliceSimoleanPayout), + aliceSellOrderProposal.want.Out, + ), + ); + + // Alice sold all of her moola + t.deepEquals(await moolaIssuer.getAmountOf(aliceMoolaPayout), moola(0)); + + // 13: Alice deposits her payout to ensure she can + await aliceMoolaPurse.deposit(aliceMoolaPayout); + await aliceSimoleanPurse.deposit(aliceSimoleanPayout); + + // 14: Bob deposits his original payments to ensure he can + await bobMoolaPurse.deposit(bobMoolaPayout); + await bobSimoleanPurse.deposit(bobSimoleanPayout); + + // Assert that the correct payout were received. + // Alice had 3 moola and 0 simoleans. + // Bob had 0 moola and 7 simoleans. + t.equals(aliceMoolaPurse.getCurrentAmount().extent, 0); + t.equals(aliceSimoleanPurse.getCurrentAmount().extent, 4); + t.equals(bobMoolaPurse.getCurrentAmount().extent, 3); + t.equals(bobSimoleanPurse.getCurrentAmount().extent, 3); +}); diff --git a/packages/zoe/test/unitTests/contracts/test-brokenContract.js b/packages/zoe/test/unitTests/contracts/test-brokenContract.js index 3073d719bd6..d7bbaab381c 100644 --- a/packages/zoe/test/unitTests/contracts/test-brokenContract.js +++ b/packages/zoe/test/unitTests/contracts/test-brokenContract.js @@ -7,7 +7,6 @@ import bundleSource from '@agoric/bundle-source'; import harden from '@agoric/harden'; import { makeZoe } from '../../../src/zoe'; -// TODO: Remove setupBasicMints and rename setupBasicMints2 import { setup } from '../setupBasicMints'; const automaticRefundRoot = `${__dirname}/brokenAutoRefund`; diff --git a/packages/zoe/test/unitTests/contracts/test-coveredCall.js b/packages/zoe/test/unitTests/contracts/test-coveredCall.js index a622c3eed29..2e229869bb2 100644 --- a/packages/zoe/test/unitTests/contracts/test-coveredCall.js +++ b/packages/zoe/test/unitTests/contracts/test-coveredCall.js @@ -41,7 +41,7 @@ test('zoe - coveredCall', async t => { StrikePrice: simoleanR.issuer, }); // separate issuerKeywordRecord from contract-specific terms - const aliceInvite = await zoe.makeInstance( + const { invite: aliceInvite } = await zoe.makeInstance( coveredCallInstallationHandle, issuerKeywordRecord, ); @@ -165,7 +165,7 @@ test(`zoe - coveredCall - alice's deadline expires, cancelling alice and bob`, a UnderlyingAsset: moolaR.issuer, StrikePrice: simoleanR.issuer, }); - const aliceInvite = await zoe.makeInstance( + const { invite: aliceInvite } = await zoe.makeInstance( coveredCallInstallationHandle, issuerKeywordRecord, ); @@ -313,7 +313,7 @@ test('zoe - coveredCall with swap for invite', async t => { UnderlyingAsset: moolaR.issuer, StrikePrice: simoleanR.issuer, }); - const aliceInvite = await zoe.makeInstance( + const { invite: aliceInvite } = await zoe.makeInstance( coveredCallInstallationHandle, issuerKeywordRecord, ); @@ -372,7 +372,7 @@ test('zoe - coveredCall with swap for invite', async t => { Asset: inviteIssuer, Price: bucksR.issuer, }); - const bobSwapInvite = await zoe.makeInstance( + const { invite: bobSwapInvite } = await zoe.makeInstance( swapInstallationId, swapIssuerKeywordRecord, ); @@ -573,7 +573,7 @@ test('zoe - coveredCall with coveredCall for invite', async t => { UnderlyingAsset: moolaR.issuer, StrikePrice: simoleanR.issuer, }); - const aliceCoveredCallInvite = await zoe.makeInstance( + const { invite: aliceCoveredCallInvite } = await zoe.makeInstance( coveredCallInstallationHandle, issuerKeywordRecord, ); @@ -635,7 +635,7 @@ test('zoe - coveredCall with coveredCall for invite', async t => { UnderlyingAsset: inviteIssuer, StrikePrice: bucksR.issuer, }); - const bobInviteForSecondCoveredCall = await zoe.makeInstance( + const { invite: bobInviteForSecondCoveredCall } = await zoe.makeInstance( coveredCallInstallationHandle, issuerKeywordRecord2, ); @@ -857,7 +857,7 @@ test('zoe - coveredCall non-fungible', async t => { StrikePrice: rpgIssuer, }); // separate issuerKeywordRecord from contract-specific terms - const aliceInvite = await zoe.makeInstance( + const { invite: aliceInvite } = await zoe.makeInstance( coveredCallInstallationHandle, issuerKeywordRecord, ); diff --git a/packages/zoe/test/unitTests/contracts/test-grifter.js b/packages/zoe/test/unitTests/contracts/test-grifter.js index 9a84a285217..23a0e4a66b6 100644 --- a/packages/zoe/test/unitTests/contracts/test-grifter.js +++ b/packages/zoe/test/unitTests/contracts/test-grifter.js @@ -7,7 +7,6 @@ import bundleSource from '@agoric/bundle-source'; import harden from '@agoric/harden'; import { makeZoe } from '../../..'; -// TODO: Remove setupBasicMints and rename setupBasicMints2 import { setup } from '../setupBasicMints'; const grifterRoot = `${__dirname}/grifter`; @@ -26,7 +25,7 @@ test('zoe - grifter tries to steal; prevented by offer safety', async t => { Price: moolaR.issuer, }); - const malloryInvite = zoe.makeInstance( + const { invite: malloryInvite } = await zoe.makeInstance( installationHandle, issuerKeywordRecord, ); @@ -56,7 +55,7 @@ test('zoe - grifter tries to steal; prevented by offer safety', async t => { t.rejects( vicOutcomeP, - /The proposed reallocation was not offer safe/, + /The reallocation was not offer safe/, `vicOffer is rejected`, ); }); diff --git a/packages/zoe/test/unitTests/contracts/test-mintPayments.js b/packages/zoe/test/unitTests/contracts/test-mintPayments.js index 0eee3008433..fb4d34769d7 100644 --- a/packages/zoe/test/unitTests/contracts/test-mintPayments.js +++ b/packages/zoe/test/unitTests/contracts/test-mintPayments.js @@ -8,7 +8,6 @@ import { E } from '@agoric/eventual-send'; import harden from '@agoric/harden'; import produceIssuer from '@agoric/ertp'; -import { makeGetInstanceHandle } from '../../../src/clientSupport'; import { makeZoe } from '../../../src/zoe'; const mintPaymentsRoot = `${__dirname}/../../../src/contracts/mintPayments`; @@ -21,15 +20,14 @@ test('zoe - mint payments', async t => { const bundle = await bundleSource(mintPaymentsRoot); const installationHandle = await E(zoe).install(bundle); const inviteIssuer = await E(zoe).getInviteIssuer(); - const getInstanceHandle = makeGetInstanceHandle(inviteIssuer); // Alice creates a contract instance - const adminInvite = await E(zoe).makeInstance(installationHandle); - const instanceHandle = await getInstanceHandle(adminInvite); + const { + instanceRecord: { publicAPI }, + } = await E(zoe).makeInstance(installationHandle); // Bob wants to get 1000 tokens so he gets an invite and makes an // offer - const { publicAPI } = await E(zoe).getInstanceRecord(instanceHandle); const invite = await E(publicAPI).makeInvite(); t.ok(await E(inviteIssuer).isLive(invite), `valid invite`); const { payout: payoutP } = await E(zoe).offer(invite); @@ -62,7 +60,6 @@ test('zoe - mint payments with unrelated give and want', async t => { const bundle = await bundleSource(mintPaymentsRoot); const installationHandle = await E(zoe).install(bundle); const inviteIssuer = await E(zoe).getInviteIssuer(); - const getInstanceHandle = makeGetInstanceHandle(inviteIssuer); const moolaBundle = produceIssuer('moola'); const simoleanBundle = produceIssuer('simolean'); @@ -72,15 +69,12 @@ test('zoe - mint payments with unrelated give and want', async t => { Asset: moolaBundle.issuer, Price: simoleanBundle.issuer, }); - const adminInvite = await E(zoe).makeInstance( - installationHandle, - issuerKeywordRecord, - ); - const instanceHandle = await getInstanceHandle(adminInvite); + const { + instanceRecord: { publicAPI }, + } = await E(zoe).makeInstance(installationHandle, issuerKeywordRecord); // Bob wants to get 1000 tokens so he gets an invite and makes an // offer - const { publicAPI } = await E(zoe).getInstanceRecord(instanceHandle); const invite = await E(publicAPI).makeInvite(); t.ok(await E(inviteIssuer).isLive(invite), `valid invite`); const proposal = harden({ diff --git a/packages/zoe/test/unitTests/contracts/test-multipoolAutoswap.js b/packages/zoe/test/unitTests/contracts/test-multipoolAutoswap.js index cbc56c99d82..031eaf2e4f1 100644 --- a/packages/zoe/test/unitTests/contracts/test-multipoolAutoswap.js +++ b/packages/zoe/test/unitTests/contracts/test-multipoolAutoswap.js @@ -15,7 +15,7 @@ import { setup } from '../setupBasicMints'; const multipoolAutoswapRoot = `${__dirname}/../../../src/contracts/multipoolAutoswap`; test('multipoolAutoSwap with valid offers', async t => { - t.plan(37); + t.plan(35); try { const { moolaR, simoleanR, moola, simoleans } = setup(); const zoe = makeZoe(); @@ -43,10 +43,9 @@ test('multipoolAutoSwap with valid offers', async t => { const bundle = await bundleSource(multipoolAutoswapRoot); const installationHandle = await zoe.install(bundle); - const aliceInvite = await zoe.makeInstance( + const { invite: aliceInvite } = await zoe.makeInstance( installationHandle, harden({ CentralToken: centralR.issuer }), - harden({ CentralToken: centralR.issuer }), ); const makeAmountMathFromIssuer = issuer => @@ -54,7 +53,6 @@ test('multipoolAutoSwap with valid offers', async t => { E(issuer).getBrand(), E(issuer).getMathHelpersName(), ]).then(([brand, mathName]) => makeAmountMath(brand, mathName)); - const inviteAmountMath = await makeAmountMathFromIssuer(inviteIssuer); const aliceInviteAmount = await inviteIssuer.getAmountOf(aliceInvite); @@ -65,6 +63,7 @@ test('multipoolAutoSwap with valid offers', async t => { { inviteDesc: 'multipool autoswap add liquidity', instanceHandle: aliceInviteAmount.extent[0].instanceHandle, + installationHandle, handle: aliceInviteAmount.extent[0].handle, }, ]), @@ -72,7 +71,7 @@ test('multipoolAutoSwap with valid offers', async t => { `invite extent is as expected`, ); - const { publicAPI, handle: instanceHandle } = zoe.getInstance( + const { publicAPI, handle: instanceHandle } = zoe.getInstanceRecord( aliceInviteAmount.extent[0].instanceHandle, ); @@ -110,7 +109,7 @@ test('multipoolAutoSwap with valid offers', async t => { ); const simoleanLiquidity = simoleanLiquidityAmountMath.make; - const { issuerKeywordRecord } = zoe.getInstance(instanceHandle); + const { issuerKeywordRecord } = zoe.getInstanceRecord(instanceHandle); t.deepEquals( issuerKeywordRecord, harden({ @@ -145,11 +144,11 @@ test('multipoolAutoSwap with valid offers', async t => { // 10 moola = 5 central tokens at the time of the liquidity adding // aka 2 moola = 1 central token const aliceProposal = harden({ - want: { MoolaLiquidity: moolaLiquidity(50) }, - give: { Moola: moola(100), CentralToken: centralTokens(50) }, + want: { Liquidity: moolaLiquidity(50) }, + give: { SecondaryToken: moola(100), CentralToken: centralTokens(50) }, }); const alicePayments = { - Moola: aliceMoolaPayment, + SecondaryToken: aliceMoolaPayment, CentralToken: aliceCentralTokenPayment, }; @@ -165,7 +164,7 @@ test('multipoolAutoSwap with valid offers', async t => { ); const liquidityPayments = await aliceAddLiquidityPayoutP; - const liquidityPayout = await liquidityPayments.MoolaLiquidity; + const liquidityPayout = await liquidityPayments.Liquidity; t.deepEquals( await moolaLiquidityIssuer.getAmountOf(liquidityPayout), @@ -190,34 +189,13 @@ test('multipoolAutoSwap with valid offers', async t => { const { publicAPI: bobPublicAPI, installationHandle: bobInstallationId, - } = zoe.getInstance(bobInviteExtent.instanceHandle); + } = zoe.getInstanceRecord(bobInviteExtent.instanceHandle); t.equals( bobInstallationId, installationHandle, `installationHandle is as expected`, ); - // Bob can learn the keywords for brands by calling the following - // two methods on the publicAPI: getBrandKeywordRecord and getKeywordForBrand - - t.deepEquals( - await E(bobPublicAPI).getBrandKeywordRecord(), - harden({ - Moola: moolaR.brand, - Simoleans: simoleanR.brand, - CentralToken: centralR.brand, - MoolaLiquidity: await E(moolaLiquidityIssuer).getBrand(), - SimoleansLiquidity: await E(simoleanLiquidityIssuer).getBrand(), - }), - `keywords have expected brands`, - ); - - t.equals( - await E(bobPublicAPI).getKeywordForBrand(moolaR.brand), - 'Moola', - `moola keyword is Moola`, - ); - // Bob looks up the price of 17 moola in central tokens const priceInCentralTokens = bobPublicAPI.getCurrentPrice( moola(17), @@ -230,10 +208,10 @@ test('multipoolAutoSwap with valid offers', async t => { ); const bobMoolaForCentralProposal = harden({ - want: { CentralToken: centralTokens(7) }, - give: { Moola: moola(17) }, + want: { Out: centralTokens(7) }, + give: { In: moola(17) }, }); - const bobMoolaForCentralPayments = harden({ Moola: bobMoolaPayment }); + const bobMoolaForCentralPayments = harden({ In: bobMoolaPayment }); // Bob swaps const { outcome: offerOkP, payout: bobPayoutP } = await zoe.offer( @@ -246,8 +224,8 @@ test('multipoolAutoSwap with valid offers', async t => { const bobPayout = await bobPayoutP; - const bobMoolaPayout1 = await bobPayout.Moola; - const bobCentralTokenPayout1 = await bobPayout.CentralToken; + const bobMoolaPayout1 = await bobPayout.In; + const bobCentralTokenPayout1 = await bobPayout.Out; t.deepEqual( await moolaR.issuer.getAmountOf(bobMoolaPayout1), @@ -286,11 +264,11 @@ test('multipoolAutoSwap with valid offers', async t => { // Bob makes another offer and swaps const bobSwapInvite2 = bobPublicAPI.makeSwapInvite(); const bobCentralForMoolaProposal = harden({ - want: { Moola: moola(16) }, - give: { CentralToken: centralTokens(7) }, + want: { Out: moola(16) }, + give: { In: centralTokens(7) }, }); const centralForMoolaPayments = harden({ - CentralToken: await E(bobCentralTokenPurse).withdraw(centralTokens(7)), + In: await E(bobCentralTokenPurse).withdraw(centralTokens(7)), }); const { @@ -309,8 +287,8 @@ test('multipoolAutoSwap with valid offers', async t => { ); const bobCentralForMoolaPayout = await bobCentralForMoolaPayoutP; - const bobMoolaPayout2 = await bobCentralForMoolaPayout.Moola; - const bobCentralPayout2 = await bobCentralForMoolaPayout.CentralToken; + const bobMoolaPayout2 = await bobCentralForMoolaPayout.Out; + const bobCentralPayout2 = await bobCentralForMoolaPayout.In; t.deepEqual( await moolaR.issuer.getAmountOf(bobMoolaPayout2), @@ -338,14 +316,14 @@ test('multipoolAutoSwap with valid offers', async t => { // const aliceSimCentralLiquidityInvite = publicAPI.makeAddLiquidityInvite(); const aliceSimCentralProposal = harden({ - want: { SimoleansLiquidity: simoleanLiquidity(43) }, - give: { Simoleans: simoleans(398), CentralToken: centralTokens(43) }, + want: { Liquidity: simoleanLiquidity(43) }, + give: { SecondaryToken: simoleans(398), CentralToken: centralTokens(43) }, }); const aliceCentralTokenPayment2 = await centralR.mint.mintPayment( centralTokens(43), ); const aliceSimCentralPayments = { - Simoleans: aliceSimoleanPayment, + SecondaryToken: aliceSimoleanPayment, CentralToken: aliceCentralTokenPayment2, }; @@ -365,7 +343,7 @@ test('multipoolAutoSwap with valid offers', async t => { ); const simCentralPayments = await aliceSimCentralPayoutP; - const simoleanLiquidityPayout = await simCentralPayments.SimoleansLiquidity; + const simoleanLiquidityPayout = await simCentralPayments.Liquidity; t.deepEquals( await simoleanLiquidityIssuer.getAmountOf(simoleanLiquidityPayout), @@ -422,11 +400,11 @@ test('multipoolAutoSwap with valid offers', async t => { const bobThirdInvite = await E(bobPublicAPI).makeSwapInvite(); const bobSimsForMoolaProposal = harden({ - want: { Moola: moola(10) }, - give: { Simoleans: simoleans(74) }, + want: { Out: moola(10) }, + give: { In: simoleans(74) }, }); const simsForMoolaPayments = harden({ - Simoleans: bobSimoleanPayment, + In: bobSimoleanPayment, }); const { payout: bobSimsForMoolaPayoutP } = await zoe.offer( @@ -436,8 +414,8 @@ test('multipoolAutoSwap with valid offers', async t => { ); const bobSimsForMoolaPayout = await bobSimsForMoolaPayoutP; - const bobSimsPayout3 = await bobSimsForMoolaPayout.Simoleans; - const bobMoolaPayout3 = await bobSimsForMoolaPayout.Moola; + const bobSimsPayout3 = await bobSimsForMoolaPayout.In; + const bobMoolaPayout3 = await bobSimsForMoolaPayout.Out; t.deepEqual( await moolaR.issuer.getAmountOf(bobMoolaPayout3), @@ -477,8 +455,8 @@ test('multipoolAutoSwap with valid offers', async t => { // She's not picky... const aliceRemoveLiquidityInvite = publicAPI.makeRemoveLiquidityInvite(); const aliceRemoveLiquidityProposal = harden({ - give: { MoolaLiquidity: moolaLiquidity(50) }, - want: { Moola: moola(91), CentralToken: centralTokens(56) }, + give: { Liquidity: moolaLiquidity(50) }, + want: { SecondaryToken: moola(91), CentralToken: centralTokens(56) }, }); const { @@ -487,15 +465,15 @@ test('multipoolAutoSwap with valid offers', async t => { } = await zoe.offer( aliceRemoveLiquidityInvite, aliceRemoveLiquidityProposal, - harden({ MoolaLiquidity: liquidityPayout }), + harden({ Liquidity: liquidityPayout }), ); t.equals(await removeLiquidityResultP, 'Liquidity successfully removed.'); const aliceRemoveLiquidityPayout = await aliceRemoveLiquidityPayoutP; - const aliceMoolaPayout = await aliceRemoveLiquidityPayout.Moola; + const aliceMoolaPayout = await aliceRemoveLiquidityPayout.SecondaryToken; const aliceCentralTokenPayout = await aliceRemoveLiquidityPayout.CentralToken; - const aliceMoolaLiquidityPayout = await aliceRemoveLiquidityPayout.MoolaLiquidity; + const aliceMoolaLiquidityPayout = await aliceRemoveLiquidityPayout.Liquidity; t.deepEquals( await moolaR.issuer.getAmountOf(aliceMoolaPayout), diff --git a/packages/zoe/test/unitTests/contracts/test-operaConcertTicket.js b/packages/zoe/test/unitTests/contracts/test-operaConcertTicket.js deleted file mode 100644 index 153f073e651..00000000000 --- a/packages/zoe/test/unitTests/contracts/test-operaConcertTicket.js +++ /dev/null @@ -1,457 +0,0 @@ -import '@agoric/install-ses'; -// eslint-disable-next-line import/no-extraneous-dependencies -import { test } from 'tape-promise/tape'; -// eslint-disable-next-line import/no-extraneous-dependencies -import bundleSource from '@agoric/bundle-source'; -import harden from '@agoric/harden'; -import produceIssuer from '@agoric/ertp'; -import { E } from '@agoric/eventual-send'; - -import { makeZoe } from '../../../src/zoe'; - -const operaConcertTicketRoot = `${__dirname}/../../../src/contracts/operaConcertTicket`; - -// __Test Scenario__ - -// The Opera de Bordeaux plays the contract creator and the auditorium -// It creates the contract for a show ("Steven Universe, the Opera", Web, March -// 25th 2020 at 8pm, 3 tickets) -// The Opera wants 22 moolas per ticket - -// Alice buys ticket #1 - -// Then, the Joker tries malicious things: -// - they try to buy again ticket #1 (and will fail) -// - they try to buy to buy ticket #2 for 1 moola (and will fail) - -// Then, Bob tries to buy ticket 1 and fails. He buys ticket #2 and #3 - -// The Opera is told about the show being sold out. It gets all the moolas from -// the sale - -test(`Zoe opera ticket contract`, async t => { - // Setup initial conditions - const { - mint: moolaMint, - issuer: moolaIssuer, - amountMath: { make: moola }, - } = produceIssuer('moola'); - - const zoe = makeZoe(); - const inviteIssuer = zoe.getInviteIssuer(); - - // === Initial Opera de Bordeaux part === - const contractReadyP = bundleSource(operaConcertTicketRoot) - .then(bundle => zoe.install(bundle)) - .then(installationHandle => { - const expectedAmountPerTicket = moola(22); - - return zoe - .makeInstance(installationHandle, harden({ Money: moolaIssuer }), { - show: 'Steven Universe, the Opera', - start: 'Web, March 25th 2020 at 8pm', - count: 3, - expectedAmountPerTicket, - }) - .then(auditoriumInvite => { - return inviteIssuer - .getAmountOf(auditoriumInvite) - .then(({ extent: [{ instanceHandle: auditoriumHandle }] }) => { - const { publicAPI } = zoe.getInstanceRecord(auditoriumHandle); - - t.equal( - typeof publicAPI.makeBuyerInvite, - 'function', - 'publicAPI.makeBuyerInvite should be a function', - ); - t.equal( - typeof publicAPI.getTicketIssuer, - 'function', - 'publicAPI.getTicketIssuer should be a function', - ); - t.equal( - typeof publicAPI.getAvailableTickets, - 'function', - 'publicAPI.getAvailableTickets should be a function', - ); - - // The auditorium makes an offer. - return ( - // Note that the proposal here is empty - // This is due to a current limitation in proposal - // expressiveness: - // https://github.com/Agoric/agoric-sdk/issues/855 - // It's impossible to know in advance how many tickets will be - // sold, so it's not possible - // to say `want: moola(3*22)` - // in a future version of Zoe, it will be possible to express: - // "i want n times moolas where n is the number of sold tickets" - zoe - .offer(auditoriumInvite, harden({})) - // completeObj exists because of a current limitation in @agoric/marshal : https://github.com/Agoric/agoric-sdk/issues/818 - .then( - async ({ - outcome: auditoriumOutcomeP, - payout, - completeObj: { complete }, - offerHandle, - }) => { - t.equal( - await auditoriumOutcomeP, - `The offer has been accepted. Once the contract has been completed, please check your payout`, - `default acceptance message`, - ); - t.equal( - typeof complete, - 'function', - 'complete should be a function', - ); - - const currentAllocation = await E( - zoe, - ).getCurrentAllocation(await offerHandle); - - t.equal( - currentAllocation.Ticket.extent.length, - 3, - `the auditorium offerHandle should be associated with the 3 tickets`, - ); - - return { - publicAPI, - operaPayout: payout, - complete, - }; - }, - ) - ); - }); - }); - }); - - const alicePartFinished = contractReadyP.then(({ publicAPI }) => { - const ticketIssuer = publicAPI.getTicketIssuer(); - const ticketAmountMath = ticketIssuer.getAmountMath(); - - // === Alice part === - // Alice starts with 100 moolas - const alicePurse = moolaIssuer.makeEmptyPurse(); - alicePurse.deposit(moolaMint.mintPayment(moola(100))); - - // Alice makes an invite - const aliceInvite = inviteIssuer.claim(publicAPI.makeBuyerInvite()); - return inviteIssuer - .getAmountOf(aliceInvite) - .then(({ extent: [{ instanceHandle: aliceHandle }] }) => { - const { terms: termsOfAlice } = zoe.getInstanceRecord(aliceHandle); - // Alice checks terms - t.equal(termsOfAlice.show, 'Steven Universe, the Opera'); - t.equal(termsOfAlice.start, 'Web, March 25th 2020 at 8pm'); - t.equal(termsOfAlice.expectedAmountPerTicket.brand, moola(22).brand); - t.equal(termsOfAlice.expectedAmountPerTicket.extent, moola(22).extent); - - const availableTickets = publicAPI.getAvailableTickets(); - // and sees the currently available tickets - t.equal( - availableTickets.length, - 3, - 'Alice should see 3 available tickets', - ); - t.ok( - availableTickets.find(ticket => ticket.number === 1), - `availableTickets contains ticket number 1`, - ); - t.ok( - availableTickets.find(ticket => ticket.number === 2), - `availableTickets contains ticket number 2`, - ); - t.ok( - availableTickets.find(ticket => ticket.number === 3), - `availableTickets contains ticket number 3`, - ); - - // find the extent corresponding to ticket #1 - const ticket1Extent = availableTickets.find( - ticket => ticket.number === 1, - ); - // make the corresponding amount - const ticket1Amount = ticketAmountMath.make(harden([ticket1Extent])); - - const aliceProposal = harden({ - give: { Money: termsOfAlice.expectedAmountPerTicket }, - want: { Ticket: ticket1Amount }, - }); - - const alicePaymentForTicket = alicePurse.withdraw( - termsOfAlice.expectedAmountPerTicket, - ); - - return zoe - .offer(aliceInvite, aliceProposal, { - Money: alicePaymentForTicket, - }) - .then(({ payout: payoutP }) => { - return payoutP.then(alicePayout => { - return ticketIssuer - .claim(alicePayout.Ticket) - .then(aliceTicketPayment => { - return ticketIssuer - .getAmountOf(aliceTicketPayment) - .then(aliceBoughtTicketAmount => { - t.equal( - aliceBoughtTicketAmount.extent[0].show, - 'Steven Universe, the Opera', - 'Alice should have receieved the ticket for the correct show', - ); - t.equal( - aliceBoughtTicketAmount.extent[0].number, - 1, - 'Alice should have received the ticket for the correct number', - ); - }); - }); - }); - }); - }); - }); - - const jokerPartFinished = Promise.all([ - contractReadyP, - alicePartFinished, - ]).then(([{ publicAPI }]) => { - // === Joker part === - const ticketIssuer = publicAPI.getTicketIssuer(); - const ticketAmountMath = ticketIssuer.getAmountMath(); - - // Joker starts with 100 moolas - const jokerPurse = moolaIssuer.makeEmptyPurse(); - jokerPurse.deposit(moolaMint.mintPayment(moola(100))); - - // Joker attempts to buy ticket 1 (and should fail) - const buyTicket1Attempt = Promise.resolve().then(() => { - const jokerInvite = inviteIssuer.claim(publicAPI.makeBuyerInvite()); - - return inviteIssuer - .getAmountOf(jokerInvite) - .then(({ extent: [{ instanceHandle: instanceHandleOfJoker }] }) => { - const { terms } = zoe.getInstanceRecord(instanceHandleOfJoker); - - const { - expectedAmountPerTicket: expectedAmountPerTicketOfJoker, - } = terms; - - // Joker does NOT check available tickets and tries to buy the ticket - // number 1(already bought by Alice, but he doesn't know) - const ticket1Amount = ticketAmountMath.make( - harden([ - { - show: terms.show, - start: terms.start, - number: 1, - }, - ]), - ); - - const jokerProposal = harden({ - give: { Money: expectedAmountPerTicketOfJoker }, - want: { Ticket: ticket1Amount }, - }); - - const jokerPaymentForTicket = jokerPurse.withdraw( - expectedAmountPerTicketOfJoker, - ); - - return zoe - .offer(jokerInvite, jokerProposal, { - Money: jokerPaymentForTicket, - }) - .then(({ outcome, payout: payoutP }) => { - t.rejects( - outcome, - 'performExchange from Joker should throw when trying to buy ticket 1', - ); - - return payoutP.then(({ Ticket, Money }) => { - return Promise.all([ - ticketIssuer.getAmountOf(Ticket), - moolaIssuer.getAmountOf(Money), - ]).then(([jokerRefundTicketAmount, jokerRefundMoneyAmount]) => { - t.ok( - ticketAmountMath.isEmpty(jokerRefundTicketAmount), - 'Joker should not receive ticket #1', - ); - t.equal( - jokerRefundMoneyAmount.extent, - 22, - 'Joker should get a refund after trying to get ticket #1', - ); - }); - }); - }); - }); - }); - - // Joker attempts to buy ticket 2 for 1 moola (and should fail) - return buyTicket1Attempt.then(() => { - const jokerInvite = inviteIssuer.claim(publicAPI.makeBuyerInvite()); - - return inviteIssuer - .getAmountOf(jokerInvite) - .then(({ extent: [{ instanceHandle: instanceHandleOfJoker }] }) => { - const { terms } = zoe.getInstanceRecord(instanceHandleOfJoker); - - const ticket2Amount = ticketAmountMath.make( - harden([ - { - show: terms.show, - start: terms.start, - number: 2, - }, - ]), - ); - - const jokerInsuffisantAmount = moola(1); - - const jokerProposal = harden({ - give: { Money: jokerInsuffisantAmount }, - want: { Ticket: ticket2Amount }, - }); - - const jokerInsufficientPaymentForTicket = jokerPurse.withdraw( - jokerInsuffisantAmount, - ); - - return zoe - .offer(jokerInvite, jokerProposal, { - Money: jokerInsufficientPaymentForTicket, - }) - .then(({ outcome, payout }) => { - t.rejects( - outcome, - 'outcome from Joker should throw when trying to buy a ticket for 1 moola', - ); - - return payout.then(({ Ticket, Money }) => { - return Promise.all([ - ticketIssuer.getAmountOf(Ticket), - moolaIssuer.getAmountOf(Money), - ]).then(([jokerRefundTicketAmount, jokerRefundMoneyAmount]) => { - t.ok( - ticketAmountMath.isEmpty(jokerRefundTicketAmount), - 'Joker should not receive ticket #2', - ); - t.equal( - jokerRefundMoneyAmount.extent, - 1, - 'Joker should get a refund after trying to get ticket #2 for 1 moola', - ); - }); - }); - }); - }); - }); - }); - - const bobPartFinished = Promise.all([contractReadyP, jokerPartFinished]).then( - ([{ publicAPI }]) => { - // === Bob part === - const ticketIssuer = publicAPI.getTicketIssuer(); - const ticketAmountMath = ticketIssuer.getAmountMath(); - - // Bob starts with 100 moolas - const bobPurse = moolaIssuer.makeEmptyPurse(); - bobPurse.deposit(moolaMint.mintPayment(moola(100))); - - const availableTickets = publicAPI.getAvailableTickets(); - - // and sees the currently available tickets - t.equal(availableTickets.length, 2, 'Bob should see 2 available tickets'); - t.ok( - !availableTickets.find(ticket => ticket.number === 1), - `availableTickets should NOT contain ticket number 1`, - ); - t.ok( - availableTickets.find(ticket => ticket.number === 2), - `availableTickets should still contain ticket number 2`, - ); - t.ok( - availableTickets.find(ticket => ticket.number === 3), - `availableTickets should still contain ticket number 3`, - ); - - // Bob buys tickets 2 and 3 - const bobInvite = inviteIssuer.claim(publicAPI.makeBuyerInvite()); - - const ticket2and3Amount = ticketAmountMath.make( - harden([ - availableTickets.find(ticket => ticket.number === 2), - availableTickets.find(ticket => ticket.number === 3), - ]), - ); - - const bobProposal = harden({ - give: { Money: moola(2 * 22) }, - want: { Ticket: ticket2and3Amount }, - }); - const bobPaymentForTicket = bobPurse.withdraw(moola(2 * 22)); - - return zoe - .offer(bobInvite, bobProposal, { - Money: bobPaymentForTicket, - }) - .then(({ payout: payoutP }) => { - return payoutP.then(bobPayout => { - return ticketIssuer - .getAmountOf(bobPayout.Ticket) - .then(bobTicketAmount => { - t.equal( - bobTicketAmount.extent.length, - 2, - 'Bob should have received 2 tickets', - ); - t.ok( - bobTicketAmount.extent.find(ticket => ticket.number === 2), - 'Bob should have received tickets #2', - ); - t.ok( - bobTicketAmount.extent.find(ticket => ticket.number === 3), - 'Bob should have received tickets #3', - ); - }); - }); - }); - }, - ); - - return Promise.all([contractReadyP, bobPartFinished]) - .then(([{ publicAPI, operaPayout, complete }]) => { - // === Final Opera part === - // getting the money back - const availableTickets = publicAPI.getAvailableTickets(); - - t.equal(availableTickets.length, 0, 'All the tickets have been sold'); - - const operaPurse = moolaIssuer.makeEmptyPurse(); - - const done = operaPayout.then(payout => { - return payout.Money.then(moneyPayment => { - return operaPurse.deposit(moneyPayment); - }).then(() => { - t.equal( - operaPurse.getCurrentAmount().extent, - 3 * 22, - `The Opera should get ${3 * 22} moolas from ticket sales`, - ); - }); - }); - - complete(); - - return done; - }) - .catch(err => { - console.error('Error in last Opera part', err); - t.fail('error'); - }) - .then(() => t.end()); -}); diff --git a/packages/zoe/test/unitTests/contracts/test-publicAuction.js b/packages/zoe/test/unitTests/contracts/test-publicAuction.js index f65b032a32d..9163502bc2d 100644 --- a/packages/zoe/test/unitTests/contracts/test-publicAuction.js +++ b/packages/zoe/test/unitTests/contracts/test-publicAuction.js @@ -9,7 +9,6 @@ import harden from '@agoric/harden'; import { makeZoe } from '../../../src/zoe'; import { setup } from '../setupBasicMints'; import { setupMixed } from '../setupMixedMints'; -import { makeGetInstanceHandle } from '../../../src/clientSupport'; const publicAuctionRoot = `${__dirname}/../../../src/contracts/publicAuction`; @@ -19,7 +18,6 @@ test('zoe - secondPriceAuction w/ 3 bids', async t => { const { moolaR, simoleanR, moola, simoleans } = setup(); const zoe = makeZoe(); const inviteIssuer = zoe.getInviteIssuer(); - const getInstanceHandle = makeGetInstanceHandle(inviteIssuer); // Setup Alice const aliceMoolaPayment = moolaR.mint.mintPayment(moola(1)); @@ -50,22 +48,18 @@ test('zoe - secondPriceAuction w/ 3 bids', async t => { const numBidsAllowed = 3; const issuerKeywordRecord = harden({ Asset: moolaR.issuer, - Bid: simoleanR.issuer, + Ask: simoleanR.issuer, }); const terms = harden({ numBidsAllowed }); - const aliceInvite = await zoe.makeInstance( - installationHandle, - issuerKeywordRecord, - terms, - ); - - const instanceHandle = await getInstanceHandle(aliceInvite); - const { publicAPI } = zoe.getInstanceRecord(instanceHandle); + const { + invite: aliceInvite, + instanceRecord: { publicAPI }, + } = await zoe.makeInstance(installationHandle, issuerKeywordRecord, terms); // Alice escrows with zoe const aliceProposal = harden({ give: { Asset: moola(1) }, - want: { Bid: simoleans(3) }, + want: { Ask: simoleans(3) }, }); const alicePayments = { Asset: aliceMoolaPayment }; // Alice initializes the auction @@ -99,7 +93,7 @@ test('zoe - secondPriceAuction w/ 3 bids', async t => { t.equals(bobInstallationId, installationHandle, 'bobInstallationId'); t.deepEquals( bobIssuers, - { Asset: moolaR.issuer, Bid: simoleanR.issuer }, + { Asset: moolaR.issuer, Ask: simoleanR.issuer }, 'bobIssuers', ); t.equals(bobTerms.numBidsAllowed, 3, 'bobTerms'); @@ -142,7 +136,7 @@ test('zoe - secondPriceAuction w/ 3 bids', async t => { t.equals(carolInstallationId, installationHandle, 'carolInstallationId'); t.deepEquals( carolIssuers, - { Asset: moolaR.issuer, Bid: simoleanR.issuer }, + { Asset: moolaR.issuer, Ask: simoleanR.issuer }, 'carolIssuers', ); t.equals(carolTerms.numBidsAllowed, 3, 'carolTerms'); @@ -188,7 +182,7 @@ test('zoe - secondPriceAuction w/ 3 bids', async t => { t.equals(daveInstallationId, installationHandle, 'daveInstallationHandle'); t.deepEquals( daveIssuers, - { Asset: moolaR.issuer, Bid: simoleanR.issuer }, + { Asset: moolaR.issuer, Ask: simoleanR.issuer }, 'daveIssuers', ); t.equals(daveTerms.numBidsAllowed, 3, 'bobTerms'); @@ -221,7 +215,7 @@ test('zoe - secondPriceAuction w/ 3 bids', async t => { const daveResult = await davePayoutP; const aliceMoolaPayout = await aliceResult.Asset; - const aliceSimoleanPayout = await aliceResult.Bid; + const aliceSimoleanPayout = await aliceResult.Ask; const bobMoolaPayout = await bobResult.Asset; const bobSimoleanPayout = await bobResult.Bid; @@ -324,7 +318,6 @@ test('zoe - secondPriceAuction w/ 3 bids - alice exits onDemand', async t => { try { const { moolaR, simoleanR, moola, simoleans } = setup(); const zoe = makeZoe(); - const inviteIssuer = zoe.getInviteIssuer(); // Setup Alice const aliceMoolaPayment = moolaR.mint.mintPayment(moola(1)); @@ -345,23 +338,18 @@ test('zoe - secondPriceAuction w/ 3 bids - alice exits onDemand', async t => { const numBidsAllowed = 3; const issuerKeywordRecord = harden({ Asset: moolaR.issuer, - Bid: simoleanR.issuer, + Ask: simoleanR.issuer, }); const terms = harden({ numBidsAllowed }); - const aliceInvite = await zoe.makeInstance( - installationHandle, - issuerKeywordRecord, - terms, - ); const { - extent: [{ instanceHandle }], - } = await inviteIssuer.getAmountOf(aliceInvite); - const { publicAPI } = zoe.getInstanceRecord(instanceHandle); + invite: aliceInvite, + instanceRecord: { publicAPI }, + } = await zoe.makeInstance(installationHandle, issuerKeywordRecord, terms); // Alice escrows with zoe const aliceProposal = harden({ give: { Asset: moola(1) }, - want: { Bid: simoleans(3) }, + want: { Ask: simoleans(3) }, }); const alicePayments = harden({ Asset: aliceMoolaPayment }); // Alice initializes the auction @@ -378,7 +366,7 @@ test('zoe - secondPriceAuction w/ 3 bids - alice exits onDemand', async t => { 'The offer has been accepted. Once the contract has been completed, please check your payout', ); - // Alice cancels her offer, making the auction stop accepting + // Alice completes her offer, making the auction stop accepting // offers completeObj.complete(); @@ -410,7 +398,7 @@ test('zoe - secondPriceAuction w/ 3 bids - alice exits onDemand', async t => { const bobResult = await bobPayoutP; const aliceMoolaPayout = await aliceResult.Asset; - const aliceSimoleanPayout = await aliceResult.Bid; + const aliceSimoleanPayout = await aliceResult.Ask; const bobMoolaPayout = await bobResult.Asset; const bobSimoleanPayout = await bobResult.Bid; @@ -469,7 +457,6 @@ test('zoe - secondPriceAuction non-fungible asset', async t => { } = setupMixed(); const zoe = makeZoe(); const inviteIssuer = zoe.getInviteIssuer(); - const getInstanceHandle = makeGetInstanceHandle(inviteIssuer); // Setup Alice const aliceCcPayment = ccMint.mintPayment(cryptoCats(harden(['Felix']))); @@ -500,22 +487,18 @@ test('zoe - secondPriceAuction non-fungible asset', async t => { const numBidsAllowed = 3; const issuerKeywordRecord = harden({ Asset: ccIssuer, - Bid: moolaIssuer, + Ask: moolaIssuer, }); const terms = harden({ numBidsAllowed }); - const aliceInvite = await zoe.makeInstance( - installationHandle, - issuerKeywordRecord, - terms, - ); - - const instanceHandle = await getInstanceHandle(aliceInvite); - const { publicAPI } = zoe.getInstanceRecord(instanceHandle); + const { + invite: aliceInvite, + instanceRecord: { publicAPI }, + } = await zoe.makeInstance(installationHandle, issuerKeywordRecord, terms); // Alice escrows with zoe const aliceProposal = harden({ give: { Asset: cryptoCats(harden(['Felix'])) }, - want: { Bid: moola(3) }, + want: { Ask: moola(3) }, }); const alicePayments = { Asset: aliceCcPayment }; // Alice initializes the auction @@ -547,7 +530,7 @@ test('zoe - secondPriceAuction non-fungible asset', async t => { } = zoe.getInstanceRecord(bobInviteExtent.instanceHandle); t.equals(bobInstallationId, installationHandle, 'bobInstallationId'); - t.deepEquals(bobIssuers, { Asset: ccIssuer, Bid: moolaIssuer }, 'bobIssuers'); + t.deepEquals(bobIssuers, { Asset: ccIssuer, Ask: moolaIssuer }, 'bobIssuers'); t.equals(bobTerms.numBidsAllowed, 3, 'bobTerms'); t.deepEquals(bobInviteExtent.minimumBid, moola(3), 'minimumBid'); t.deepEquals( @@ -592,7 +575,7 @@ test('zoe - secondPriceAuction non-fungible asset', async t => { t.equals(carolInstallationId, installationHandle, 'carolInstallationId'); t.deepEquals( carolIssuers, - { Asset: ccIssuer, Bid: moolaIssuer }, + { Asset: ccIssuer, Ask: moolaIssuer }, 'carolIssuers', ); t.equals(carolTerms.numBidsAllowed, 3, 'carolTerms'); @@ -638,7 +621,7 @@ test('zoe - secondPriceAuction non-fungible asset', async t => { t.equals(daveInstallationId, installationHandle, 'daveInstallationHandle'); t.deepEquals( daveIssuers, - { Asset: ccIssuer, Bid: moolaIssuer }, + { Asset: ccIssuer, Ask: moolaIssuer }, 'daveIssuers', ); t.equals(daveTerms.numBidsAllowed, 3, 'bobTerms'); @@ -675,7 +658,7 @@ test('zoe - secondPriceAuction non-fungible asset', async t => { const daveResult = await davePayoutP; const aliceCcPayout = await aliceResult.Asset; - const aliceMoolaPayout = await aliceResult.Bid; + const aliceMoolaPayout = await aliceResult.Ask; const bobCcPayout = await bobResult.Asset; const bobMoolaPayout = await bobResult.Bid; diff --git a/packages/zoe/test/unitTests/contracts/test-sellTickets.js b/packages/zoe/test/unitTests/contracts/test-sellTickets.js new file mode 100644 index 00000000000..e05af1f29c9 --- /dev/null +++ b/packages/zoe/test/unitTests/contracts/test-sellTickets.js @@ -0,0 +1,563 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import '@agoric/install-ses'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { test } from 'tape-promise/tape'; +// eslint-disable-next-line import/no-extraneous-dependencies +import bundleSource from '@agoric/bundle-source'; +import harden from '@agoric/harden'; +import produceIssuer from '@agoric/ertp'; +import { E } from '@agoric/eventual-send'; + +import { makeZoe } from '../../../src/zoe'; +import { defaultAcceptanceMsg } from '../../../src/contractSupport'; + +const mintAndSellNFTRoot = `${__dirname}/../../../src/contracts/mintAndSellNFT`; +const sellItemsRoot = `${__dirname}/../../../src/contracts/sellItems`; + +test(`mint and sell tickets for multiple shows`, async t => { + // Setup initial conditions + const zoe = makeZoe(); + + const mintAndSellNFTBundle = await bundleSource(mintAndSellNFTRoot); + const mintAndSellNFTInstallationHandle = await E(zoe).install( + mintAndSellNFTBundle, + ); + + const sellItemsBundle = await bundleSource(sellItemsRoot); + const sellItemsInstallationHandle = await E(zoe).install(sellItemsBundle); + + const { issuer: moolaIssuer, amountMath: moolaAmountMath } = produceIssuer( + 'moola', + ); + + const { + instanceRecord: { publicAPI }, + invite, + } = await E(zoe).makeInstance(mintAndSellNFTInstallationHandle); + const { outcome } = await E(zoe).offer(invite); + const ticketMaker = await outcome; + const { outcome: escrowTicketsOutcome, sellItemsInstanceHandle } = await E( + ticketMaker, + ).sellTokens({ + customExtentProperties: { + show: 'Steven Universe, the Opera', + start: 'Wed, March 25th 2020 at 8pm', + }, + count: 3, + moneyIssuer: moolaIssuer, + sellItemsInstallationHandle, + pricePerItem: moolaAmountMath.make(20), + }); + t.equals( + await escrowTicketsOutcome, + defaultAcceptanceMsg, + `escrowTicketsOutcome is default acceptance message`, + ); + + const ticketIssuerP = E(publicAPI).getTokenIssuer(); + const ticketBrand = await E(ticketIssuerP).getBrand(); + const { publicAPI: ticketSalesPublicAPI } = await E(zoe).getInstanceRecord( + sellItemsInstanceHandle, + ); + const ticketsForSale = await E(ticketSalesPublicAPI).getAvailableItems(); + t.deepEquals( + ticketsForSale, + { + brand: ticketBrand, + extent: [ + { + show: 'Steven Universe, the Opera', + start: 'Wed, March 25th 2020 at 8pm', + number: 1, + }, + { + show: 'Steven Universe, the Opera', + start: 'Wed, March 25th 2020 at 8pm', + number: 2, + }, + { + show: 'Steven Universe, the Opera', + start: 'Wed, March 25th 2020 at 8pm', + number: 3, + }, + ], + }, + `the tickets are up for sale`, + ); + + const { sellItemsInstanceHandle: sellItemsInstanceHandle2 } = await E( + ticketMaker, + ).sellTokens({ + customExtentProperties: { + show: 'Reserved for private party', + start: 'Tues May 12, 2020 at 8pm', + }, + count: 2, + moneyIssuer: moolaIssuer, + sellItemsInstallationHandle, + pricePerItem: moolaAmountMath.make(20), + }); + const { publicAPI: salesPublicAPI2 } = await E(zoe).getInstanceRecord( + sellItemsInstanceHandle2, + ); + const ticketsForSale2 = await E(salesPublicAPI2).getAvailableItems(); + t.deepEquals( + ticketsForSale2, + { + brand: ticketBrand, + extent: [ + { + show: 'Reserved for private party', + start: 'Tues May 12, 2020 at 8pm', + number: 1, + }, + { + show: 'Reserved for private party', + start: 'Tues May 12, 2020 at 8pm', + number: 2, + }, + ], + }, + `we can reuse the mint to make more tickets and sell them in a different instance`, + ); + t.end(); +}); + +// __Test Scenario__ + +// The Opera de Bordeaux plays the contract creator and the auditorium +// It creates the contract for a show ("Steven Universe, the Opera", Wed, March +// 25th 2020 at 8pm, 3 tickets) +// The Opera wants 22 moolas per ticket + +// Alice buys ticket #1 + +// Then, the Joker tries malicious things: +// - they try to buy ticket #1 (and will fail because Alice already +// bought it) +// - they try to buy to buy ticket #2 for 1 moola (and will fail) + +// Then, Bob buys ticket #2 and #3 + +// The Opera is told about the show being sold out. It gets all the moolas from +// the sale + +test(`mint and sell opera tickets`, async t => { + // Setup initial conditions + const { + mint: moolaMint, + issuer: moolaIssuer, + amountMath: { make: moola }, + } = produceIssuer('moola'); + + const zoe = makeZoe(); + + const mintAndSellNFTBundle = await bundleSource(mintAndSellNFTRoot); + const mintAndSellNFTInstallationHandle = await E(zoe).install( + mintAndSellNFTBundle, + ); + + const sellItemsBundle = await bundleSource(sellItemsRoot); + const sellItemsInstallationHandle = await E(zoe).install(sellItemsBundle); + + // === Initial Opera de Bordeaux part === + + // create an instance of the venue contract + const mintTickets = async () => { + const { + instanceRecord: { publicAPI }, + invite, + } = await E(zoe).makeInstance(mintAndSellNFTInstallationHandle); + + const ticketIssuer = await E(publicAPI).getTokenIssuer(); + const { outcome } = await E(zoe).offer(invite); + const ticketSeller = await outcome; + + // completeObj exists because of a current limitation in @agoric/marshal : https://github.com/Agoric/agoric-sdk/issues/818 + const { + sellItemsInstanceHandle: ticketSalesInstanceHandle, + payout, + completeObj, + } = await E(ticketSeller).sellTokens({ + customExtentProperties: { + show: 'Steven Universe, the Opera', + start: 'Wed, March 25th 2020 at 8pm', + }, + count: 3, + moneyIssuer: moolaIssuer, + sellItemsInstallationHandle, + pricePerItem: moola(22), + }); + + const { publicAPI: ticketSalesPublicAPI } = await E(zoe).getInstanceRecord( + ticketSalesInstanceHandle, + ); + + const ticketsForSale = await E(ticketSalesPublicAPI).getAvailableItems(); + + t.equal(ticketsForSale.extent.length, 3, `3 tickets for sale`); + + return harden({ + ticketIssuer, + ticketSalesInstanceHandle, + ticketSalesPublicAPI, + payoutP: payout, + completeObj, + }); + }; + + // === Alice part === + // Alice is given the instanceHandle of the ticket sales instance + // and she has 100 moola + const aliceBuysTicket1 = async ( + ticketSalesInstanceHandle, + moola100Payment, + ) => { + const { publicAPI: ticketSalesPublicAPI, terms } = await E( + zoe, + ).getInstanceRecord(ticketSalesInstanceHandle); + const ticketIssuer = await E(ticketSalesPublicAPI).getItemsIssuer(); + const ticketAmountMath = await E(ticketIssuer).getAmountMath(); + + const alicePurse = await E(moolaIssuer).makeEmptyPurse(); + await E(alicePurse).deposit(moola100Payment); + + // Alice makes an invite for herself + const aliceInvite = await E(ticketSalesPublicAPI).makeBuyerInvite(); + + t.deepEquals(terms.pricePerItem, moola(22), `pricePerItem is 22 moola`); + + const availableTickets = await E(ticketSalesPublicAPI).getAvailableItems(); + + t.equal( + availableTickets.extent.length, + 3, + 'Alice should see 3 available tickets', + ); + t.ok( + availableTickets.extent.find(ticket => ticket.number === 1), + `availableTickets contains ticket number 1`, + ); + t.ok( + availableTickets.extent.find(ticket => ticket.number === 2), + `availableTickets contains ticket number 2`, + ); + t.ok( + availableTickets.extent.find(ticket => ticket.number === 3), + `availableTickets contains ticket number 3`, + ); + + // find the extent corresponding to ticket #1 + const ticket1Extent = availableTickets.extent.find( + ticket => ticket.number === 1, + ); + // make the corresponding amount + const ticket1Amount = ticketAmountMath.make(harden([ticket1Extent])); + + const aliceProposal = harden({ + give: { Money: terms.pricePerItem }, + want: { Items: ticket1Amount }, + }); + + const alicePaymentForTicket = alicePurse.withdraw(terms.pricePerItem); + + const alicePaymentKeywordRecord = harden({ Money: alicePaymentForTicket }); + + const { payout: payoutP } = await E(zoe).offer( + aliceInvite, + aliceProposal, + alicePaymentKeywordRecord, + ); + const alicePayout = await payoutP; + const aliceBoughtTicketAmount = await E(ticketIssuer).getAmountOf( + alicePayout.Items, + ); + + t.equal( + aliceBoughtTicketAmount.extent[0].show, + 'Steven Universe, the Opera', + 'Alice should have receieved the ticket for the correct show', + ); + t.equal( + aliceBoughtTicketAmount.extent[0].number, + 1, + 'Alice should have received the ticket for the correct number', + ); + }; + + // === Joker part === + // Joker starts with 100 moolas + // Joker attempts to buy ticket 1 (and should fail) + const jokerBuysTicket1 = async ( + ticketSalesInstanceHandle, + moola100Payment, + ) => { + const { publicAPI: ticketSalesPublicAPI } = await E(zoe).getInstanceRecord( + ticketSalesInstanceHandle, + ); + const ticketIssuer = await E(ticketSalesPublicAPI).getItemsIssuer(); + const ticketAmountMath = await E(ticketIssuer).getAmountMath(); + + const jokerPurse = await E(moolaIssuer).makeEmptyPurse(); + await E(jokerPurse).deposit(moola100Payment); + + const jokerInvite = await E(ticketSalesPublicAPI).makeBuyerInvite(); + + const { + terms: { pricePerItem }, + } = await E(zoe).getInstanceRecord(ticketSalesInstanceHandle); + + // Joker does NOT check available tickets and tries to buy the ticket + // number 1(already bought by Alice, but he doesn't know) + const ticket1Amount = ticketAmountMath.make( + harden([ + { + show: 'Steven Universe, the Opera', + start: 'Wed, March 25th 2020 at 8pm', + number: 1, + }, + ]), + ); + + const jokerProposal = harden({ + give: { Money: pricePerItem }, + want: { Items: ticket1Amount }, + }); + + const jokerPaymentForTicket = jokerPurse.withdraw(pricePerItem); + + const { outcome, payout: payoutP } = await zoe.offer( + jokerInvite, + jokerProposal, + harden({ + Money: jokerPaymentForTicket, + }), + ); + + t.rejects( + outcome, + /Some of the wanted items were not available for sale/, + 'ticket 1 is no longer available', + ); + + const payout = await payoutP; + const jokerTicketPayoutAmount = await ticketIssuer.getAmountOf( + payout.Items, + ); + const jokerMoneyPayoutAmount = await moolaIssuer.getAmountOf(payout.Money); + + t.ok( + ticketAmountMath.isEmpty(jokerTicketPayoutAmount), + 'Joker should not receive ticket #1', + ); + t.deepEquals( + jokerMoneyPayoutAmount, + moola(22), + 'Joker should get a refund after trying to get ticket #1', + ); + }; + + // Joker attempts to buy ticket 2 for 1 moola (and should fail) + const jokerTriesToBuyTicket2 = async ( + ticketSalesInstanceHandle, + moola100Payment, + ) => { + const { publicAPI: ticketSalesPublicAPI } = await E(zoe).getInstanceRecord( + ticketSalesInstanceHandle, + ); + const ticketIssuer = await E(ticketSalesPublicAPI).getItemsIssuer(); + const ticketAmountMath = await E(ticketIssuer).getAmountMath(); + + const jokerPurse = await E(moolaIssuer).makeEmptyPurse(); + await E(jokerPurse).deposit(moola100Payment); + + const jokerInvite = await E(ticketSalesPublicAPI).makeBuyerInvite(); + + const ticket2Amount = ticketAmountMath.make( + harden([ + { + show: 'Steven Universe, the Opera', + start: 'Wed, March 25th 2020 at 8pm', + number: 2, + }, + ]), + ); + + const insufficientAmount = moola(1); + const jokerProposal = harden({ + give: { Money: insufficientAmount }, + want: { Items: ticket2Amount }, + }); + + const jokerInsufficientPaymentForTicket = jokerPurse.withdraw( + insufficientAmount, + ); + + const { outcome, payout: payoutP } = await zoe.offer( + jokerInvite, + jokerProposal, + harden({ + Money: jokerInsufficientPaymentForTicket, + }), + ); + + t.rejects( + outcome, + /More money.*is required to buy these items/, + 'outcome from Joker should throw when trying to buy a ticket for 1 moola', + ); + const payout = await payoutP; + const jokerTicketPayoutAmount = await ticketIssuer.getAmountOf( + payout.Items, + ); + const jokerMoneyPayoutAmount = await moolaIssuer.getAmountOf(payout.Money); + + t.ok( + ticketAmountMath.isEmpty(jokerTicketPayoutAmount), + 'Joker should not receive ticket #2', + ); + t.deepEquals( + jokerMoneyPayoutAmount, + insufficientAmount, + 'Joker should get a refund after trying to get ticket #2 for 1 moola', + ); + }; + + const bobBuysTicket2And3 = async ( + ticketSalesInstanceHandle, + moola100Payment, + ) => { + const { publicAPI: ticketSalesPublicAPI, terms } = await E( + zoe, + ).getInstanceRecord(ticketSalesInstanceHandle); + const ticketIssuer = await E(ticketSalesPublicAPI).getItemsIssuer(); + const ticketAmountMath = await E(ticketIssuer).getAmountMath(); + + const bobPurse = await E(moolaIssuer).makeEmptyPurse(); + await E(bobPurse).deposit(moola100Payment); + + const bobInvite = await E(ticketSalesPublicAPI).makeBuyerInvite(); + + const availableTickets = await E(ticketSalesPublicAPI).getAvailableItems(); + + // Bob sees the currently available tickets + t.equal( + availableTickets.extent.length, + 2, + 'Bob should see 2 available tickets', + ); + t.ok( + !availableTickets.extent.find(ticket => ticket.number === 1), + `availableTickets should NOT contain ticket number 1`, + ); + t.ok( + availableTickets.extent.find(ticket => ticket.number === 2), + `availableTickets should still contain ticket number 2`, + ); + t.ok( + availableTickets.extent.find(ticket => ticket.number === 3), + `availableTickets should still contain ticket number 3`, + ); + + // Bob buys tickets 2 and 3 + const ticket2and3Amount = ticketAmountMath.make( + harden([ + availableTickets.extent.find(ticket => ticket.number === 2), + availableTickets.extent.find(ticket => ticket.number === 3), + ]), + ); + + const totalCost = moola(2 * terms.pricePerItem.extent); + + const bobProposal = harden({ + give: { Money: totalCost }, + want: { Items: ticket2and3Amount }, + }); + const bobPaymentForTicket = await E(bobPurse).withdraw(totalCost); + const paymentKeywordRecord = harden({ + Money: bobPaymentForTicket, + }); + + const { payout: payoutP } = await E(zoe).offer( + bobInvite, + bobProposal, + paymentKeywordRecord, + ); + const payout = await payoutP; + const bobTicketAmount = await E(ticketIssuer).getAmountOf(payout.Items); + t.equal( + bobTicketAmount.extent.length, + 2, + 'Bob should have received 2 tickets', + ); + t.ok( + bobTicketAmount.extent.find(ticket => ticket.number === 2), + 'Bob should have received tickets #2', + ); + t.ok( + bobTicketAmount.extent.find(ticket => ticket.number === 3), + 'Bob should have received tickets #3', + ); + }; + + // === Final Opera part === + const ticketSellerClosesContract = async ({ + ticketIssuer, + ticketSalesPublicAPI, + payoutP, + completeObj, + }) => { + const availableTickets = await E(ticketSalesPublicAPI).getAvailableItems(); + const ticketAmountMath = await E(ticketIssuer).getAmountMath(); + t.ok( + ticketAmountMath.isEmpty(availableTickets), + 'All the tickets have been sold', + ); + + const operaPurse = moolaIssuer.makeEmptyPurse(); + + await E(completeObj).complete(); + + const payout = await payoutP; + const moneyPayment = await payout.Money; + await E(operaPurse).deposit(moneyPayment); + const currentPurseBalance = await E(operaPurse).getCurrentAmount(); + + t.equal( + currentPurseBalance.extent, + 3 * 22, + `The Opera should get ${3 * 22} moolas from ticket sales`, + ); + }; + + const { + ticketIssuer, + ticketSalesInstanceHandle, + ticketSalesPublicAPI, + payoutP, + completeObj, + } = await mintTickets(); + await aliceBuysTicket1( + ticketSalesInstanceHandle, + moolaMint.mintPayment(moola(100)), + ); + await jokerBuysTicket1( + ticketSalesInstanceHandle, + moolaMint.mintPayment(moola(100)), + ); + await jokerTriesToBuyTicket2( + ticketSalesInstanceHandle, + moolaMint.mintPayment(moola(100)), + ); + await bobBuysTicket2And3( + ticketSalesInstanceHandle, + moolaMint.mintPayment(moola(100)), + ); + await ticketSellerClosesContract({ + ticketIssuer, + ticketSalesPublicAPI, + payoutP, + completeObj, + }); + t.end(); +}); diff --git a/packages/zoe/test/unitTests/contracts/test-simpleExchange.js b/packages/zoe/test/unitTests/contracts/test-simpleExchange.js index 30904762bd3..6d5a1bee7da 100644 --- a/packages/zoe/test/unitTests/contracts/test-simpleExchange.js +++ b/packages/zoe/test/unitTests/contracts/test-simpleExchange.js @@ -27,7 +27,6 @@ test('simpleExchange with valid offers', async t => { } = setup(); const zoe = makeZoe(); const inviteIssuer = zoe.getInviteIssuer(); - const getInstanceHandle = makeGetInstanceHandle(inviteIssuer); // Pack the contract. const bundle = await bundleSource(simpleExchange); @@ -44,14 +43,14 @@ test('simpleExchange with valid offers', async t => { const bobMoolaPurse = moolaIssuer.makeEmptyPurse(); const bobSimoleanPurse = simoleanIssuer.makeEmptyPurse(); - // 1: Simon creates a simpleExchange instance and spreads the invite far and - // wide with instructions on how to use it. - const simonInvite = await zoe.makeInstance(installationHandle, { + // 1: Simon creates a simpleExchange instance and spreads the publicAPI far + // and wide with instructions on how to call makeInvite(). + const { + instanceRecord: { publicAPI }, + } = await zoe.makeInstance(installationHandle, { Asset: moolaIssuer, Price: simoleanIssuer, }); - const instanceHandle = await getInstanceHandle(simonInvite); - const { publicAPI } = zoe.getInstanceRecord(instanceHandle); const aliceInvite = publicAPI.makeInvite(); @@ -88,11 +87,13 @@ test('simpleExchange with valid offers', async t => { // 5: Bob decides to join. const bobExclusiveInvite = await inviteIssuer.claim(bobInvite); + const getInstanceHandle = makeGetInstanceHandle(inviteIssuer); + const bobInstanceHandle = await getInstanceHandle(bobExclusiveInvite); const { installationHandle: bobInstallationId, issuerKeywordRecord: bobIssuers, - } = zoe.getInstanceRecord(instanceHandle); + } = zoe.getInstanceRecord(bobInstanceHandle); t.equals(bobInstallationId, installationHandle); @@ -163,9 +164,9 @@ test('simpleExchange with valid offers', async t => { // Alice had 3 moola and 0 simoleans. // Bob had 0 moola and 7 simoleans. t.equals(aliceMoolaPurse.getCurrentAmount().extent, 0); - t.equals(aliceSimoleanPurse.getCurrentAmount().extent, 7); + t.equals(aliceSimoleanPurse.getCurrentAmount().extent, 4); t.equals(bobMoolaPurse.getCurrentAmount().extent, 3); - t.equals(bobSimoleanPurse.getCurrentAmount().extent, 0); + t.equals(bobSimoleanPurse.getCurrentAmount().extent, 3); }); test('simpleExchange with multiple sell offers', async t => { @@ -181,7 +182,6 @@ test('simpleExchange with multiple sell offers', async t => { } = setup(); const zoe = makeZoe(); const inviteIssuer = zoe.getInviteIssuer(); - const getInstanceHandle = makeGetInstanceHandle(inviteIssuer); // Pack the contract. const bundle = await bundleSource(simpleExchange); @@ -198,12 +198,12 @@ test('simpleExchange with multiple sell offers', async t => { // 1: Simon creates a simpleExchange instance and spreads the invite far and // wide with instructions on how to use it. - const simonInvite = await zoe.makeInstance(installationHandle, { + const { + instanceRecord: { publicAPI }, + } = await zoe.makeInstance(installationHandle, { Asset: moolaIssuer, Price: simoleanIssuer, }); - const instanceHandle = await getInstanceHandle(simonInvite); - const { publicAPI } = zoe.getInstanceRecord(instanceHandle); const aliceInvite1 = publicAPI.makeInvite(); @@ -273,8 +273,6 @@ test('simpleExchange showPayoutRules', async t => { t.plan(1); const { moolaIssuer, simoleanIssuer, moolaMint, moola, simoleans } = setup(); const zoe = makeZoe(); - const inviteIssuer = zoe.getInviteIssuer(); - const getInstanceHandle = makeGetInstanceHandle(inviteIssuer); // Pack the contract. const bundle = await bundleSource(simpleExchange); @@ -285,12 +283,12 @@ test('simpleExchange showPayoutRules', async t => { const aliceMoolaPayment = moolaMint.mintPayment(moola(3)); // 1: Simon creates a simpleExchange instance and spreads the invite far and // wide with instructions on how to use it. - const simonInvite = await zoe.makeInstance(installationHandle, { + const { + instanceRecord: { publicAPI }, + } = await zoe.makeInstance(installationHandle, { Asset: moolaIssuer, Price: simoleanIssuer, }); - const instanceHandle = await getInstanceHandle(simonInvite); - const { publicAPI } = zoe.getInstanceRecord(instanceHandle); const aliceInvite = publicAPI.makeInvite(); @@ -332,7 +330,6 @@ test('simpleExchange with non-fungible assets', async t => { const zoe = makeZoe(); const inviteIssuer = zoe.getInviteIssuer(); - const getInstanceHandle = makeGetInstanceHandle(inviteIssuer); // Pack the contract. const bundle = await bundleSource(simpleExchange); @@ -352,12 +349,12 @@ test('simpleExchange with non-fungible assets', async t => { // 1: Simon creates a simpleExchange instance and spreads the invite far and // wide with instructions on how to use it. - const simonInvite = await zoe.makeInstance(installationHandle, { + const { + instanceRecord: { publicAPI }, + } = await zoe.makeInstance(installationHandle, { Asset: rpgIssuer, Price: ccIssuer, }); - const instanceHandle = await getInstanceHandle(simonInvite); - const { publicAPI } = zoe.getInstanceRecord(instanceHandle); const aliceInvite = publicAPI.makeInvite(); @@ -365,7 +362,7 @@ test('simpleExchange with non-fungible assets', async t => { // sell a Spell of Binding and wants to receive CryptoCats in return. const aliceSellOrderProposal = harden({ give: { Asset: rpgItems(spell) }, - want: { Price: cryptoCats(harden([])) }, + want: { Price: cryptoCats(harden(['Cheshire Cat'])) }, exit: { onDemand: null }, }); const alicePayments = { Asset: aliceRpgPayment }; @@ -380,11 +377,13 @@ test('simpleExchange with non-fungible assets', async t => { // 5: Bob decides to join. const bobExclusiveInvite = await inviteIssuer.claim(bobInvite); + const getInstanceHandle = makeGetInstanceHandle(inviteIssuer); + const bobInstanceHandle = await getInstanceHandle(bobExclusiveInvite); const { installationHandle: bobInstallationId, issuerKeywordRecord: bobIssuers, - } = zoe.getInstanceRecord(instanceHandle); + } = zoe.getInstanceRecord(bobInstanceHandle); t.equals(bobInstallationId, installationHandle); diff --git a/packages/zoe/test/unitTests/test-cleanProposal.js b/packages/zoe/test/unitTests/test-cleanProposal.js index 461cf5b78ea..3d0470834e9 100644 --- a/packages/zoe/test/unitTests/test-cleanProposal.js +++ b/packages/zoe/test/unitTests/test-cleanProposal.js @@ -1,6 +1,8 @@ // eslint-disable-next-line import/no-extraneous-dependencies import { test } from 'tape-promise/tape'; +import makeStore from '@agoric/weak-store'; + import harden from '@agoric/harden'; import { cleanProposal } from '../../src/cleanProposal'; @@ -10,44 +12,27 @@ import buildManualTimer from '../../tools/manualTimer'; test('cleanProposal test', t => { t.plan(1); try { - const { - simoleanIssuer, - moolaIssuer, - bucksIssuer, - moola, - simoleans, - amountMaths, - } = setup(); - - const issuerKeywordRecord = harden({ - Asset: simoleanIssuer, - Price: moolaIssuer, - AlternativePrice: bucksIssuer, - }); + const { simoleanR, moolaR, bucksR, moola, simoleans } = setup(); + + const brandToAmountMath = makeStore('brand'); + brandToAmountMath.init(moolaR.brand, moolaR.amountMath); + brandToAmountMath.init(simoleanR.brand, simoleanR.amountMath); + brandToAmountMath.init(bucksR.brand, bucksR.amountMath); + + const getAmountMathForBrand = brandToAmountMath.get; const proposal = harden({ give: { Asset: simoleans(1) }, want: { Price: moola(3) }, }); - const amountMathKeywordRecord = harden({ - Asset: amountMaths.get('simoleans'), - Price: amountMaths.get('moola'), - AlternativePrice: amountMaths.get('bucks'), - }); - - // CleanProposal no longer fills in missing keywords const expected = harden({ give: { Asset: simoleans(1) }, want: { Price: moola(3) }, exit: { onDemand: null }, }); - const actual = cleanProposal( - issuerKeywordRecord, - amountMathKeywordRecord, - proposal, - ); + const actual = cleanProposal(getAmountMathForBrand, proposal); t.deepEquals(actual, expected); } catch (e) { @@ -58,18 +43,14 @@ test('cleanProposal test', t => { test('cleanProposal - all empty', t => { t.plan(1); try { - const { simoleanIssuer, moolaIssuer, bucksIssuer, amountMaths } = setup(); - - const issuerKeywordRecord = { - Asset: simoleanIssuer, - Price: moolaIssuer, - AlternativePrice: bucksIssuer, - }; - const amountMathKeywordRecord = harden({ - Asset: amountMaths.get('simoleans'), - Price: amountMaths.get('moola'), - AlternativePrice: amountMaths.get('bucks'), - }); + const { simoleanR, moolaR, bucksR } = setup(); + + const brandToAmountMath = makeStore('brand'); + brandToAmountMath.init(moolaR.brand, moolaR.amountMath); + brandToAmountMath.init(simoleanR.brand, simoleanR.amountMath); + brandToAmountMath.init(bucksR.brand, bucksR.amountMath); + + const getAmountMathForBrand = brandToAmountMath.get; const proposal = harden({ give: {}, @@ -84,10 +65,7 @@ test('cleanProposal - all empty', t => { }); // cleanProposal no longer fills in empty keywords - t.deepEquals( - cleanProposal(issuerKeywordRecord, amountMathKeywordRecord, proposal), - expected, - ); + t.deepEquals(cleanProposal(getAmountMathForBrand, proposal), expected); } catch (e) { t.assert(false, e); } @@ -96,32 +74,15 @@ test('cleanProposal - all empty', t => { test('cleanProposal - repeated brands', t => { t.plan(3); try { - const { - moolaIssuer, - simoleanIssuer, - bucksIssuer, - moola, - simoleans, - amountMaths, - } = setup(); - const timer = buildManualTimer(console.log); + const { simoleanR, moolaR, bucksR, moola, simoleans } = setup(); - const issuerKeywordRecord = { - Asset1: simoleanIssuer, - Price1: moolaIssuer, - AlternativePrice1: bucksIssuer, - Asset2: simoleanIssuer, - Price2: moolaIssuer, - AlternativePrice2: bucksIssuer, - }; - const amountMathsObj = harden({ - Asset1: amountMaths.get('simoleans'), - Price1: amountMaths.get('moola'), - AlternativePrice1: amountMaths.get('bucks'), - Asset2: amountMaths.get('simoleans'), - Price2: amountMaths.get('moola'), - AlternativePrice2: amountMaths.get('bucks'), - }); + const brandToAmountMath = makeStore('brand'); + brandToAmountMath.init(moolaR.brand, moolaR.amountMath); + brandToAmountMath.init(simoleanR.brand, simoleanR.amountMath); + brandToAmountMath.init(bucksR.brand, bucksR.amountMath); + + const getAmountMathForBrand = brandToAmountMath.get; + const timer = buildManualTimer(console.log); const proposal = harden({ want: { Asset2: simoleans(1) }, @@ -137,7 +98,7 @@ test('cleanProposal - repeated brands', t => { exit: { afterDeadline: { timer, deadline: 100 } }, }); // cleanProposal no longer fills in empty keywords - const actual = cleanProposal(issuerKeywordRecord, amountMathsObj, proposal); + const actual = cleanProposal(getAmountMathForBrand, proposal); t.deepEquals(actual.want, expected.want); t.deepEquals(actual.give, expected.give); t.deepEquals(actual.exit, expected.exit); diff --git a/packages/zoe/test/unitTests/test-offerSafety.js b/packages/zoe/test/unitTests/test-offerSafety.js index 6f628471858..0c8ee75fd4b 100644 --- a/packages/zoe/test/unitTests/test-offerSafety.js +++ b/packages/zoe/test/unitTests/test-offerSafety.js @@ -2,9 +2,14 @@ import { test } from 'tape-promise/tape'; import harden from '@agoric/harden'; -import { isOfferSafeForOffer } from '../../src/offerSafety'; +import { isOfferSafe } from '../../src/offerSafety'; import { setup } from './setupBasicMints'; +function makeGetAmountMath(mapping) { + const brandToAmountMath = new Map(mapping); + return brand => brandToAmountMath.get(brand); +} + // Potential outcomes: // 1. Users can get what they wanted, get back what they gave, both, or // neither @@ -23,216 +28,216 @@ import { setup } from './setupBasicMints'; // equal to want, equal to give -> true // more than want, more than give -> isOfferSafe() = true -test('isOfferSafeForOffer - more than want, more than give', t => { +test('isOfferSafe - more than want, more than give', t => { t.plan(1); try { const { moolaR, simoleanR, bucksR, moola, simoleans, bucks } = setup(); - const amountMathKeywordRecord = harden({ - A: moolaR.amountMath, - B: simoleanR.amountMath, - C: bucksR.amountMath, - }); + const getAmountMath = makeGetAmountMath([ + [moolaR.brand, moolaR.amountMath], + [simoleanR.brand, simoleanR.amountMath], + [bucksR.brand, bucksR.amountMath], + ]); const proposal = harden({ give: { A: moola(8) }, want: { B: simoleans(6), C: bucks(7) }, }); const amounts = harden({ A: moola(10), B: simoleans(7), C: bucks(8) }); - t.ok(isOfferSafeForOffer(amountMathKeywordRecord, proposal, amounts)); + t.ok(isOfferSafe(getAmountMath, proposal, amounts)); } catch (e) { t.assert(false, e); } }); // more than want, less than give -> true -test('isOfferSafeForOffer - more than want, less than give', t => { +test('isOfferSafe - more than want, less than give', t => { t.plan(1); try { - const { amountMaths, moola, simoleans, bucks } = setup(); - const amountMathKeywordRecord = harden({ - A: amountMaths.get('moola'), - B: amountMaths.get('simoleans'), - C: amountMaths.get('bucks'), - }); + const { moolaR, simoleanR, bucksR, moola, simoleans, bucks } = setup(); + const getAmountMath = makeGetAmountMath([ + [moolaR.brand, moolaR.amountMath], + [simoleanR.brand, simoleanR.amountMath], + [bucksR.brand, bucksR.amountMath], + ]); const proposal = harden({ give: { A: moola(8) }, want: { B: simoleans(6), C: bucks(7) }, }); const amounts = harden({ A: moola(1), B: simoleans(7), C: bucks(8) }); - t.ok(isOfferSafeForOffer(amountMathKeywordRecord, proposal, amounts)); + t.ok(isOfferSafe(getAmountMath, proposal, amounts)); } catch (e) { t.assert(false, e); } }); // more than want, equal to give -> true -test('isOfferSafeForOffer - more than want, equal to give', t => { +test('isOfferSafe - more than want, equal to give', t => { t.plan(1); try { - const { amountMaths, moola, simoleans, bucks } = setup(); - const amountMathKeywordRecord = harden({ - A: amountMaths.get('moola'), - B: amountMaths.get('simoleans'), - C: amountMaths.get('bucks'), - }); + const { moolaR, simoleanR, bucksR, moola, simoleans, bucks } = setup(); + const getAmountMath = makeGetAmountMath([ + [moolaR.brand, moolaR.amountMath], + [simoleanR.brand, simoleanR.amountMath], + [bucksR.brand, bucksR.amountMath], + ]); const proposal = harden({ want: { A: moola(8) }, give: { B: simoleans(6), C: bucks(7) }, }); const amounts = harden({ A: moola(9), B: simoleans(6), C: bucks(7) }); - t.ok(isOfferSafeForOffer(amountMathKeywordRecord, proposal, amounts)); + t.ok(isOfferSafe(getAmountMath, proposal, amounts)); } catch (e) { t.assert(false, e); } }); // less than want, more than give -> true -test('isOfferSafeForOffer - less than want, more than give', t => { +test('isOfferSafe - less than want, more than give', t => { t.plan(1); try { const { moolaR, simoleanR, bucksR, moola, simoleans, bucks } = setup(); - const amountMathKeywordRecord = harden({ - A: moolaR.amountMath, - B: simoleanR.amountMath, - C: bucksR.amountMath, - }); + const getAmountMath = makeGetAmountMath([ + [moolaR.brand, moolaR.amountMath], + [simoleanR.brand, simoleanR.amountMath], + [bucksR.brand, bucksR.amountMath], + ]); const proposal = harden({ want: { A: moola(8) }, give: { B: simoleans(6), C: bucks(7) }, }); const amounts = harden({ A: moola(7), B: simoleans(9), C: bucks(19) }); - t.ok(isOfferSafeForOffer(amountMathKeywordRecord, proposal, amounts)); + t.ok(isOfferSafe(getAmountMath, proposal, amounts)); } catch (e) { t.assert(false, e); } }); // less than want, less than give -> false -test('isOfferSafeForOffer - less than want, less than give', t => { +test('isOfferSafe - less than want, less than give', t => { t.plan(1); try { const { moolaR, simoleanR, bucksR, moola, simoleans, bucks } = setup(); - const amountMathKeywordRecord = harden({ - A: moolaR.amountMath, - B: simoleanR.amountMath, - C: bucksR.amountMath, - }); + const getAmountMath = makeGetAmountMath([ + [moolaR.brand, moolaR.amountMath], + [simoleanR.brand, simoleanR.amountMath], + [bucksR.brand, bucksR.amountMath], + ]); const proposal = harden({ want: { A: moola(8) }, give: { B: simoleans(6), C: bucks(7) }, }); const amounts = harden({ A: moola(7), B: simoleans(5), C: bucks(6) }); - t.notOk(isOfferSafeForOffer(amountMathKeywordRecord, proposal, amounts)); + t.notOk(isOfferSafe(getAmountMath, proposal, amounts)); } catch (e) { t.assert(false, e); } }); // less than want, equal to give -> true -test('isOfferSafeForOffer - less than want, equal to give', t => { +test('isOfferSafe - less than want, equal to give', t => { t.plan(1); try { const { moolaR, simoleanR, bucksR, moola, simoleans, bucks } = setup(); - const amountMathKeywordRecord = harden({ - A: moolaR.amountMath, - B: simoleanR.amountMath, - C: bucksR.amountMath, - }); + const getAmountMath = makeGetAmountMath([ + [moolaR.brand, moolaR.amountMath], + [simoleanR.brand, simoleanR.amountMath], + [bucksR.brand, bucksR.amountMath], + ]); const proposal = harden({ want: { B: simoleans(6) }, give: { A: moola(1), C: bucks(7) }, }); const amounts = harden({ A: moola(1), B: simoleans(5), C: bucks(7) }); - t.ok(isOfferSafeForOffer(amountMathKeywordRecord, proposal, amounts)); + t.ok(isOfferSafe(getAmountMath, proposal, amounts)); } catch (e) { t.assert(false, e); } }); // equal to want, more than give -> true -test('isOfferSafeForOffer - equal to want, more than give', t => { +test('isOfferSafe - equal to want, more than give', t => { t.plan(1); try { const { moolaR, simoleanR, bucksR, moola, simoleans, bucks } = setup(); - const amountMathKeywordRecord = harden({ - A: moolaR.amountMath, - B: simoleanR.amountMath, - C: bucksR.amountMath, - }); + const getAmountMath = makeGetAmountMath([ + [moolaR.brand, moolaR.amountMath], + [simoleanR.brand, simoleanR.amountMath], + [bucksR.brand, bucksR.amountMath], + ]); const proposal = harden({ want: { B: simoleans(6) }, give: { A: moola(1), C: bucks(7) }, }); const amounts = harden({ A: moola(2), B: simoleans(6), C: bucks(8) }); - t.ok(isOfferSafeForOffer(amountMathKeywordRecord, proposal, amounts)); + t.ok(isOfferSafe(getAmountMath, proposal, amounts)); } catch (e) { t.assert(false, e); } }); // equal to want, less than give -> true -test('isOfferSafeForOffer - equal to want, less than give', t => { +test('isOfferSafe - equal to want, less than give', t => { t.plan(1); try { const { moolaR, simoleanR, bucksR, moola, simoleans, bucks } = setup(); - const amountMathKeywordRecord = harden({ - A: moolaR.amountMath, - B: simoleanR.amountMath, - C: bucksR.amountMath, - }); + const getAmountMath = makeGetAmountMath([ + [moolaR.brand, moolaR.amountMath], + [simoleanR.brand, simoleanR.amountMath], + [bucksR.brand, bucksR.amountMath], + ]); const proposal = harden({ want: { B: simoleans(6) }, give: { A: moola(1), C: bucks(7) }, }); const amounts = harden({ A: moola(0), B: simoleans(6), C: bucks(0) }); - t.ok(isOfferSafeForOffer(amountMathKeywordRecord, proposal, amounts)); + t.ok(isOfferSafe(getAmountMath, proposal, amounts)); } catch (e) { t.assert(false, e); } }); // equal to want, equal to give -> true -test('isOfferSafeForOffer - equal to want, equal to give', t => { +test('isOfferSafe - equal to want, equal to give', t => { t.plan(1); try { const { moolaR, simoleanR, bucksR, moola, simoleans, bucks } = setup(); - const amountMathKeywordRecord = harden({ - A: moolaR.amountMath, - B: simoleanR.amountMath, - C: bucksR.amountMath, - }); + const getAmountMath = makeGetAmountMath([ + [moolaR.brand, moolaR.amountMath], + [simoleanR.brand, simoleanR.amountMath], + [bucksR.brand, bucksR.amountMath], + ]); const proposal = harden({ want: { B: simoleans(6) }, give: { A: moola(1), C: bucks(7) }, }); const amounts = harden({ A: moola(1), B: simoleans(6), C: bucks(7) }); - t.ok(isOfferSafeForOffer(amountMathKeywordRecord, proposal, amounts)); + t.ok(isOfferSafe(getAmountMath, proposal, amounts)); } catch (e) { t.assert(false, e); } }); -test('isOfferSafeForOffer - empty proposal', t => { +test('isOfferSafe - empty proposal', t => { t.plan(1); try { const { moolaR, simoleanR, bucksR, moola, simoleans, bucks } = setup(); - const amountMathKeywordRecord = harden({ - A: moolaR.amountMath, - B: simoleanR.amountMath, - C: bucksR.amountMath, - }); + const getAmountMath = makeGetAmountMath([ + [moolaR.brand, moolaR.amountMath], + [simoleanR.brand, simoleanR.amountMath], + [bucksR.brand, bucksR.amountMath], + ]); const proposal = harden({ give: {}, want: {} }); const amounts = harden({ A: moola(1), B: simoleans(6), C: bucks(7) }); - t.ok(isOfferSafeForOffer(amountMathKeywordRecord, proposal, amounts)); + t.ok(isOfferSafe(getAmountMath, proposal, amounts)); } catch (e) { t.assert(false, e); } diff --git a/packages/zoe/test/unitTests/test-rightsConservation.js b/packages/zoe/test/unitTests/test-rightsConservation.js index ef4e746a051..631f6bfb52f 100644 --- a/packages/zoe/test/unitTests/test-rightsConservation.js +++ b/packages/zoe/test/unitTests/test-rightsConservation.js @@ -1,8 +1,9 @@ // eslint-disable-next-line import/no-extraneous-dependencies import { test } from 'tape-promise/tape'; +import makeStore from '@agoric/weak-store'; import produceIssuer from '@agoric/ertp'; -import { areRightsConserved, transpose } from '../../src/rightsConservation'; +import { areRightsConserved } from '../../src/rightsConservation'; const setupAmountMaths = () => { const moolaIssuerResults = produceIssuer('moola'); @@ -10,7 +11,16 @@ const setupAmountMaths = () => { const bucksIssuerResults = produceIssuer('bucks'); const all = [moolaIssuerResults, simoleanIssuerResults, bucksIssuerResults]; - return all.map(objs => objs.amountMath); + const amountMathArray = all.map(objs => objs.amountMath); + const brandToAmountMath = makeStore('brand'); + all.forEach(bundle => + brandToAmountMath.init(bundle.brand, bundle.amountMath), + ); + const getAmountMathForBrand = brandToAmountMath.get; + return { + amountMathArray, + getAmountMathForBrand, + }; }; const makeAmountMatrix = (amountMathArray, extentMatrix) => @@ -18,31 +28,12 @@ const makeAmountMatrix = (amountMathArray, extentMatrix) => row.map((extent, i) => amountMathArray[i].make(extent)), ); -test('transpose', t => { - t.plan(1); - try { - t.deepEquals( - transpose([ - [1, 2, 3], - [4, 5, 6], - ]), - [ - [1, 4], - [2, 5], - [3, 6], - ], - ); - } catch (e) { - t.assert(false, e); - } -}); - // rights are conserved for amount with Nat extents test(`areRightsConserved - true for amount with nat extents`, t => { t.plan(1); try { - const amountMaths = setupAmountMaths(); - const oldExtents = [ + const { amountMathArray, getAmountMathForBrand } = setupAmountMaths(); + const previousExtents = [ [0, 1, 0], [4, 1, 0], [6, 3, 0], @@ -53,10 +44,15 @@ test(`areRightsConserved - true for amount with nat extents`, t => { [6, 2, 0], ]; - const oldAmounts = makeAmountMatrix(amountMaths, oldExtents); - const newAmounts = makeAmountMatrix(amountMaths, newExtents); + const previousAmounts = makeAmountMatrix( + amountMathArray, + previousExtents, + ).flat(); + const newAmounts = makeAmountMatrix(amountMathArray, newExtents).flat(); - t.ok(areRightsConserved(amountMaths, oldAmounts, newAmounts)); + t.ok( + areRightsConserved(getAmountMathForBrand, previousAmounts, newAmounts), + ); } catch (e) { t.assert(false, e); } @@ -66,7 +62,7 @@ test(`areRightsConserved - true for amount with nat extents`, t => { test(`areRightsConserved - false for amount with Nat extents`, t => { t.plan(1); try { - const amountMaths = setupAmountMaths(); + const { amountMathArray, getAmountMathForBrand } = setupAmountMaths(); const oldExtents = [ [0, 1, 4], [4, 1, 0], @@ -78,10 +74,10 @@ test(`areRightsConserved - false for amount with Nat extents`, t => { [6, 2, 0], ]; - const oldAmounts = makeAmountMatrix(amountMaths, oldExtents); - const newAmounts = makeAmountMatrix(amountMaths, newExtents); + const oldAmounts = makeAmountMatrix(amountMathArray, oldExtents).flat(); + const newAmounts = makeAmountMatrix(amountMathArray, newExtents).flat(); - t.notOk(areRightsConserved(amountMaths, oldAmounts, newAmounts)); + t.notOk(areRightsConserved(getAmountMathForBrand, oldAmounts, newAmounts)); } catch (e) { t.assert(false, e); } @@ -90,11 +86,11 @@ test(`areRightsConserved - false for amount with Nat extents`, t => { test(`areRightsConserved - empty arrays`, t => { t.plan(1); try { - const amountMaths = setupAmountMaths(); - const oldAmounts = [[], [], []]; - const newAmounts = [[], [], []]; + const { getAmountMathForBrand } = setupAmountMaths(); + const oldAmounts = []; + const newAmounts = []; - t.ok(areRightsConserved(amountMaths, oldAmounts, newAmounts)); + t.ok(areRightsConserved(getAmountMathForBrand, oldAmounts, newAmounts)); } catch (e) { t.assert(false, e); }