diff --git a/core/contractHost.js b/core/contractHost.js index 78febdc4526..4c29fe7bd45 100644 --- a/core/contractHost.js +++ b/core/contractHost.js @@ -9,6 +9,7 @@ import { insist } from '../util/insist'; import { mustBeSameStructure, allComparable } from '../util/sameStructure'; import { makeUniAssayMaker } from './assays'; import { makeMint } from './issuers'; +import { makeBasicMintController } from './mintController'; import makePromise from '../util/makePromise'; function makeContractHost(E, evaluate) { @@ -25,12 +26,16 @@ function makeContractHost(E, evaluate) { return seatDesc; } const makeUniAssay = makeUniAssayMaker(descriptionCoercer); - const inviteMint = makeMint('contract host', makeUniAssay); + const inviteMint = makeMint( + 'contract host', + makeBasicMintController, + makeUniAssay, + ); const inviteIssuer = inviteMint.getIssuer(); const inviteAssay = inviteIssuer.getAssay(); function redeem(allegedInvitePayment) { - const allegedInviteAmount = allegedInvitePayment.getXferBalance(); + const allegedInviteAmount = allegedInvitePayment.getBalance(); const inviteAmount = inviteAssay.vouch(allegedInviteAmount); insist(!inviteAssay.isEmpty(inviteAmount))`\ No invites left`; diff --git a/core/issuers.js b/core/issuers.js index 348d0f1dc94..ba12e3ef23d 100644 --- a/core/issuers.js +++ b/core/issuers.js @@ -1,48 +1,27 @@ +/* eslint no-use-before-define: 0 */ // --> OFF // Copyright (C) 2019 Agoric, under Apache License 2.0 import harden from '@agoric/harden'; -import { makePrivateName } from '../util/PrivateName'; import { insist } from '../util/insist'; import { makeNatAssay } from './assays'; +import { makeBasicMintController } from './mintController'; -function makeMint(description, makeAssay = makeNatAssay) { +function makeMint( + description, + makeMintController = makeBasicMintController, + makeAssay = makeNatAssay, +) { insist(description)`\ Description must be truthy: ${description}`; - // Map from purse or payment to the transfer rights it currently - // holds. Transfer rights can move via payments, or they can cause a - // transfer of both the transfer and use rights by depositing it - // into a purse. - const xferRights = makePrivateName(); - - // Map from purse to useRights, where useRights do not include the - // right to transfer. Creating a payment moves some xferRights into the - // payment, but no useRights. Depositing a payment into another - // purse transfers both the xferRights and the useRights. - const useRights = makePrivateName(); - - // Map from payment to the home purse the payment came from. When the - // payment is deposited elsewhere, useRights are transfered from the - // home purse to the destination purse. - const homePurses = makePrivateName(); - // src is a purse or payment. Return a fresh payment. One internal // function used for both cases, since they are so similar. function takePayment(amount, isPurse, src, _name) { - // eslint-disable-next-line no-use-before-define amount = assay.coerce(amount); _name = `${_name}`; - if (isPurse) { - insist(useRights.has(src))`\ -Purse expected: ${src}`; - } else { - insist(homePurses.has(src))`\ -Payment expected: ${src}`; - } - const srcOldXferAmount = xferRights.get(src); - // eslint-disable-next-line no-use-before-define - const srcNewXferAmount = assay.without(srcOldXferAmount, amount); + const srcOldRightsAmount = mintController.getAmount(src); + const srcNewRightsAmount = assay.without(srcOldRightsAmount, amount); // ///////////////// commit point ////////////////// // All queries above passed with no side effects. @@ -51,33 +30,26 @@ Payment expected: ${src}`; const payment = harden({ getIssuer() { - // eslint-disable-next-line no-use-before-define return issuer; }, - getXferBalance() { - return xferRights.get(payment); + getBalance() { + return mintController.getAmount(payment); }, }); - xferRights.set(src, srcNewXferAmount); - xferRights.init(payment, amount); - const homePurse = isPurse ? src : homePurses.get(src); - homePurses.init(payment, homePurse); + mintController.recordPayment(src, payment, amount, srcNewRightsAmount); return payment; } const issuer = harden({ getLabel() { - // eslint-disable-next-line no-use-before-define return assay.getLabel(); }, getAssay() { - // eslint-disable-next-line no-use-before-define return assay; }, makeEmptyPurse(name = 'a purse') { - // eslint-disable-next-line no-use-before-define return mint.mint(assay.empty(), name); // mint and issuer call each other }, @@ -89,7 +61,12 @@ Payment expected: ${src}`; getExclusiveAll(srcPaymentP, name = 'a payment') { return Promise.resolve(srcPaymentP).then(srcPayment => - takePayment(xferRights.get(srcPayment), false, srcPayment, name), + takePayment( + mintController.getAmount(srcPayment), + false, + srcPayment, + name, + ), ); }, @@ -109,31 +86,27 @@ Payment expected: ${src}`; const label = harden({ issuer, description }); const assay = makeAssay(label); + const mintController = makeMintController(assay); function depositInto(purse, amount, srcPayment) { amount = assay.coerce(amount); - const purseOldXferAmount = xferRights.get(purse); - const srcOldXferAmount = xferRights.get(srcPayment); - // Also checks that the union is representable - const purseNewXferAmount = assay.with(purseOldXferAmount, amount); - const srcNewXferAmount = assay.without(srcOldXferAmount, amount); - - const homePurse = homePurses.get(srcPayment); - const purseOldUseAmount = useRights.get(purse); - const homeOldUseAmount = useRights.get(homePurse); + const purseOldRightsAmount = mintController.getAmount(purse); + const srcOldRightsAmount = mintController.getAmount(srcPayment); // Also checks that the union is representable - const purseNewUseAmount = assay.with(purseOldUseAmount, amount); - const homeNewUseAmount = assay.without(homeOldUseAmount, amount); + const purseNewRightsAmount = assay.with(purseOldRightsAmount, amount); + const srcNewRightsAmount = assay.without(srcOldRightsAmount, amount); // ///////////////// commit point ////////////////// // All queries above passed with no side effects. // During side effects below, any early exits should be made into // fatal turn aborts. - xferRights.set(srcPayment, srcNewXferAmount); - xferRights.set(purse, purseNewXferAmount); - useRights.set(homePurse, homeNewUseAmount); - useRights.set(purse, purseNewUseAmount); + mintController.recordDeposit( + srcPayment, + assay.coerce(srcNewRightsAmount), + purse, + assay.coerce(purseNewRightsAmount), + ); return amount; } @@ -142,6 +115,19 @@ Payment expected: ${src}`; getIssuer() { return issuer; }, + destroyAll() { + mintController.destroyAll(); + }, + destroy(amount) { + amount = assay.coerce(amount); + // for non-fungible tokens that are unique, destroy them by removing them from + // the purses/payments that they live in + mintController.destroy(amount); + }, + revoke(amount) { + this.destroy(amount); + return mint(amount); + }, mint(initialBalance, _name = 'a purse') { initialBalance = assay.coerce(initialBalance); _name = `${_name}`; @@ -150,11 +136,8 @@ Payment expected: ${src}`; getIssuer() { return issuer; }, - getXferBalance() { - return xferRights.get(purse); - }, - getUseBalance() { - return useRights.get(purse); + getBalance() { + return mintController.getAmount(purse); }, deposit(amount, srcPaymentP) { return Promise.resolve(srcPaymentP).then(srcPayment => { @@ -163,21 +146,31 @@ Payment expected: ${src}`; }, depositAll(srcPaymentP) { return Promise.resolve(srcPaymentP).then(srcPayment => { - return depositInto(purse, xferRights.get(srcPayment), srcPayment); + return depositInto( + purse, + mintController.getAmount(srcPayment), + srcPayment, + ); }); }, withdraw(amount, name = 'a withdrawal payment') { return takePayment(amount, true, purse, name); }, withdrawAll(name = 'a withdrawal payment') { - return takePayment(xferRights.get(purse), true, purse, name); + return takePayment( + mintController.getAmount(purse), + true, + purse, + name, + ); }, }); - xferRights.init(purse, initialBalance); - useRights.init(purse, initialBalance); + mintController.recordMint(purse, initialBalance); return purse; }, }); + + // TODO: pass along destroyMint capability too return mint; } harden(makeMint); @@ -186,7 +179,12 @@ harden(makeMint); // currency. Returns a promise for a peg object that asynchonously // converts between the two. The local currency is synchronously // transferable locally. -function makePeg(E, remoteIssuerP, makeAssay = makeNatAssay) { +function makePeg( + E, + remoteIssuerP, + makeMintController, + makeAssay = makeNatAssay, +) { const remoteLabelP = E(remoteIssuerP).getLabel(); // The remoteLabel is a local copy of the remote pass-by-copy @@ -198,7 +196,7 @@ function makePeg(E, remoteIssuerP, makeAssay = makeNatAssay) { const backingPurseP = E(remoteIssuerP).makeEmptyPurse('backing'); const { description } = remoteLabel; - const localMint = makeMint(description, makeAssay); + const localMint = makeMint(description, makeMintController, makeAssay); const localIssuer = localMint.getIssuer(); const localLabel = localIssuer.getLabel(); diff --git a/core/mintController.js b/core/mintController.js new file mode 100644 index 00000000000..d523e3a58ea --- /dev/null +++ b/core/mintController.js @@ -0,0 +1,50 @@ +import { makePrivateName } from '../util/PrivateName'; + +export function makeBasicMintController() { + // Map from purse or payment to the rights it currently + // holds. Rights can move via payments + + // purse/payment to amount + let rights = makePrivateName(); + + function recordPayment(src, payment, amount, srcNewRightsAmount) { + rights.set(src, srcNewRightsAmount); + rights.init(payment, amount); + } + + function destroy(_amount) { + throw new Error('destroy is not implemented'); + } + + function destroyAll() { + rights = makePrivateName(); // reset rights + } + + function recordDeposit( + srcPayment, + srcNewRightsAmount, + purse, + purseNewRightsAmount, + ) { + rights.set(srcPayment, srcNewRightsAmount); + rights.set(purse, purseNewRightsAmount); + } + + function recordMint(purse, initialAmount) { + rights.init(purse, initialAmount); + } + + function getAmount(pursePayment) { + return rights.get(pursePayment); + } + + const mintController = { + destroy, + destroyAll, + recordPayment, + recordDeposit, + recordMint, + getAmount, + }; + return mintController; +} diff --git a/demo/contractHost/bootstrap.js b/demo/contractHost/bootstrap.js index 54a90d7a518..5b902297e09 100644 --- a/demo/contractHost/bootstrap.js +++ b/demo/contractHost/bootstrap.js @@ -11,8 +11,8 @@ function build(E, log) { // it. function showPaymentBalance(name, paymentP) { return E(paymentP) - .getXferBalance() - .then(amount => log(name, ' xfer balance ', amount)); + .getBalance() + .then(amount => log(name, ' balance ', amount)); } // TODO BUG: All callers should wait until settled before doing // anything that would change the balance before show*Balance* reads @@ -20,11 +20,8 @@ function build(E, log) { function showPurseBalances(name, purseP) { return Promise.all([ E(purseP) - .getXferBalance() - .then(amount => log(name, ' xfer balance ', amount)), - E(purseP) - .getUseBalance() - .then(amount => log(name, ' use balance ', amount)), + .getBalance() + .then(amount => log(name, ' balance ', amount)), ]); } @@ -75,6 +72,7 @@ function build(E, log) { function mintTestNumber(mint) { log('starting mintTestNumber'); const mMintP = E(mint).makeMint('quatloos'); + mMintP.then(newMint => console.log(newMint)); const alicePurseP = E(mMintP).mint(1000, 'alice'); const paymentP = E(alicePurseP).withdraw(50); diff --git a/demo/contractHost/vat-alice.js b/demo/contractHost/vat-alice.js index 3b649727614..128c56a6de4 100644 --- a/demo/contractHost/vat-alice.js +++ b/demo/contractHost/vat-alice.js @@ -14,8 +14,8 @@ function makeAliceMaker(E, host, log) { // it. function showPaymentBalance(name, paymentP) { return E(paymentP) - .getXferBalance() - .then(amount => log(name, ' xfer balance ', amount)); + .getBalance() + .then(amount => log(name, ' balance ', amount)); } return harden({ @@ -48,9 +48,7 @@ function makeAliceMaker(E, host, log) { log('++ alice.acceptInvite starting'); showPaymentBalance('alice invite', allegedInvitePaymentP); - const allegedInviteAmountP = E( - allegedInvitePaymentP, - ).getXferBalance(); + const allegedInviteAmountP = E(allegedInvitePaymentP).getBalance(); const verifiedInviteP = E.resolve(allegedInviteAmountP).then( allegedInviteAmount => { @@ -112,9 +110,7 @@ function makeAliceMaker(E, host, log) { log('++ alice.acceptOptionDirectly starting'); showPaymentBalance('alice invite', allegedInvitePaymentP); - const allegedInviteAmountP = E( - allegedInvitePaymentP, - ).getXferBalance(); + const allegedInviteAmountP = E(allegedInvitePaymentP).getBalance(); const verifiedInvitePaymentP = E.resolve(allegedInviteAmountP).then( allegedInviteAmount => { @@ -174,7 +170,7 @@ function makeAliceMaker(E, host, log) { acceptOptionForFred(allegedInvitePaymentP) { log('++ alice.acceptOptionForFred starting'); const finNeededP = E(E(optFinIssuerP).getAssay()).make(55); - const inviteNeededP = E(allegedInvitePaymentP).getXferBalance(); + const inviteNeededP = E(allegedInvitePaymentP).getBalance(); const terms = harden([finNeededP, inviteNeededP]); const invitesP = E(escrowExchangeInstallationP).spawn(terms); diff --git a/demo/contractHost/vat-fred.js b/demo/contractHost/vat-fred.js index 818b3c8cf94..45243884440 100644 --- a/demo/contractHost/vat-fred.js +++ b/demo/contractHost/vat-fred.js @@ -53,9 +53,7 @@ function makeFredMaker(E, host, log) { quantity: 55, }); - const allegedSaleAmountP = E( - allegedSaleInvitePaymentP, - ).getXferBalance(); + const allegedSaleAmountP = E(allegedSaleInvitePaymentP).getBalance(); const verifiedSaleInvitePaymentP = E.resolve(allegedSaleAmountP).then( allegedSaleInviteAmount => { diff --git a/demo/gallery/README.md b/demo/gallery/README.md new file mode 100644 index 00000000000..dff1ff6cf6a --- /dev/null +++ b/demo/gallery/README.md @@ -0,0 +1,51 @@ +# Pixel Gallery Demo + +This demo is roughly based on [Reddit's +r/Place](https://en.wikipedia.org/wiki/Place_(Reddit)), but has a +number of additional features that showcase the unique affordances of +the Agoric platform, including: higher-order contracts, easy creation +of new assets, and safe code reusability. + +## Pixels +The base asset a pixelList (an array of +pixels). The holder of a pixel is able to change the color of the +pixel by splitting the pixelList up into the useRight (the right to +color) and a transfer right (the right to transfer the pixel). The +holder of the use right can send a payment of the right to the Gallery +(the creator the pixel canvas) to color. + +When the transfer right is sent as a payment to another user, that +user can do turn the transfer right in to the Gallery to get a full +pixel back, thus revoking any coloring rights that may be in the hands +of other users. + +## Dust +We also have a currency called Dust (Pixel Dust, heh heh). Users do not start out with any Dust - they only start out with access to the faucet. As described in more detail below, they can sell the pixels that they get for free from the faucet back to the Gallery in order to earn Dust. + +## Gallery + +At the start, all pixels and all color rights are held by the Gallery. +The Gallery provides a faucet that allows users to get a pixel at a +time for free. The Gallery has a queue of all the pixels ordered by +least recently used (at the start, this is all the pixels in an +unusual order), and takes from the front of this LRU queue to provide a pixel to the faucet. + +The user eventually gets a handful of pixels from the faucet (in the +future, this would be rate-limited per user), but at this point, it is unlikely that the user is able to draw +anything interesting. Hopefully, this will incentivize them to keep +playing (“gotta collect them all”) rather than deter them. + +## Further Gameplay + +In order to amass the pixels that they want in order to draw their masterpiece, the user will need to sell some pixels to get our currency, Dust. (The user does not start out with any money.) Our Gallery will always buy pixels back, but it values pixels near the center much more than pixels on the periphery. This will incentivize people to keep hitting the faucet because they might get a “valuable” pixel in the next go. + +In order to sell pixels, the user must create an ask in our order book. The Gallery will always have a bid (request to buy) for all pixels, but the price should be relatively low, lower than selling to another user (this will need to be done after experimentation, not sure how to guarantee it now). + +Now that the user has some Dust, how can they buy their pixels? The pixel canvas will have an on-hover attribute that shows the x, y coordinates of the hovered-over pixel. The user should be able to look at the canvas to see what they want to buy, and record the coordinates. Then, they can create a bid in the order book for that pixel or sell it to the Gallery for a relatively low price. + +The user should be able to put their color rights into our ERTP covered call and other contract components and create things like options. + +For examples of how the ERTP assets work, see: + test/demo/test-gallery-demo.js +and: + test/more/pixels/test-gallery.js diff --git a/demo/gallery/bootstrap.js b/demo/gallery/bootstrap.js new file mode 100644 index 00000000000..72b90709327 --- /dev/null +++ b/demo/gallery/bootstrap.js @@ -0,0 +1,94 @@ +// Copyright (C) 2019 Agoric, under Apache License 2.0 + +import harden from '@agoric/harden'; + +import { makeGallery } from '../../more/pixels/gallery'; + +function build(E, log) { + function testTapFaucet(aliceMaker, gallery) { + log('starting testTapFaucet'); + const aliceP = E(aliceMaker).make(gallery.userFacet); + return E(aliceP).doTapFaucet(); + } + async function testAliceChangesColor(aliceMaker, gallery) { + log('starting testAliceChangesColor'); + const aliceP = E(aliceMaker).make(gallery.userFacet); + const alicePixelAmount = await E(aliceP).doChangeColor(); + const rawPixel = alicePixelAmount.quantity[0]; + log(`current color ${gallery.userFacet.getColor(rawPixel.x, rawPixel.y)}`); + } + async function testAliceSendsOnlyUseRight(aliceMaker, bobMaker, gallery) { + log('starting testAliceSendsOnlyUseRight'); + const aliceP = E(aliceMaker).make(gallery.userFacet); + const bobP = E(bobMaker).make(gallery.userFacet); + await E(aliceP).doSendOnlyUseRight(bobP); + } + async function testGalleryRevokes(aliceMaker, bobMaker, gallery) { + log('starting testGalleryRevokes'); + const aliceP = E(aliceMaker).make(gallery.userFacet); + const rawPixel = await E(aliceP).doTapFaucetAndStore(); + gallery.adminFacet.revokePixel(rawPixel); + E(aliceP).checkAfterRevoked(); + } + + const obj0 = { + async bootstrap(argv, vats) { + const canvasSize = 10; + function stateChangeHandler(_newState) { + // does nothing in this test + } + + switch (argv[0]) { + case 'tapFaucet': { + log('starting tapFaucet'); + const aliceMaker = await E(vats.alice).makeAliceMaker(); + const gallery = makeGallery(stateChangeHandler, canvasSize); + log('alice is made'); + return testTapFaucet(aliceMaker, gallery); + } + case 'aliceChangesColor': { + log('starting aliceChangesColor'); + const aliceMaker = await E(vats.alice).makeAliceMaker(); + const gallery = makeGallery(stateChangeHandler, canvasSize); + log('alice is made'); + return testAliceChangesColor(aliceMaker, gallery); + } + case 'aliceSendsOnlyUseRight': { + log('starting aliceSendsOnlyUseRight'); + const aliceMaker = await E(vats.alice).makeAliceMaker(); + const bobMaker = await E(vats.bob).makeBobMaker(); + const gallery = makeGallery(stateChangeHandler, canvasSize); + log('alice is made'); + return testAliceSendsOnlyUseRight(aliceMaker, bobMaker, gallery); + } + case 'galleryRevokes': { + log('starting galleryRevokes'); + const aliceMaker = await E(vats.alice).makeAliceMaker(); + const bobMaker = await E(vats.bob).makeBobMaker(); + const gallery = makeGallery(stateChangeHandler, canvasSize); + return testGalleryRevokes(aliceMaker, bobMaker, gallery); + } + default: { + throw new Error(`unrecognized argument value ${argv[0]}`); + } + } + }, + }; + return harden(obj0); +} +harden(build); + +function setup(syscall, state, helpers) { + function log(...args) { + helpers.log(...args); + console.log(...args); + } + log(`=> setup called`); + return helpers.makeLiveSlots( + syscall, + state, + E => build(E, log), + helpers.vatID, + ); +} +export default harden(setup); diff --git a/demo/gallery/vat-alice.js b/demo/gallery/vat-alice.js new file mode 100644 index 00000000000..b9c2bd3cd05 --- /dev/null +++ b/demo/gallery/vat-alice.js @@ -0,0 +1,225 @@ +// Copyright (C) 2013 Google Inc, under Apache License 2.0 +// Copyright (C) 2018 Agoric, under Apache License 2.0 + +import harden from '@agoric/harden'; + +import { insist } from '../../util/insist'; + +let storedUseRight; +let storedTransferRight; + +function makeAliceMaker(E, log) { + // TODO BUG: All callers should wait until settled before doing + // anything that would change the balance before show*Balance* reads + // it. + function showPaymentBalance(name, paymentP) { + return E(paymentP) + .getBalance() + .then(amount => log(name, ' balance ', amount)); + } + + return harden({ + make(gallery) { + const alice = harden({ + doTapFaucet() { + log('++ alice.doTapFaucet starting'); + const pixelPaymentP = E(gallery).tapFaucet(); + showPaymentBalance('pixel from faucet', pixelPaymentP); + }, + async doChangeColor() { + log('++ alice.doChangeColor starting'); + const pixelPaymentP = E(gallery).tapFaucet(); + log('tapped Faucet'); + + const pixelIssuer = E(pixelPaymentP).getIssuer(); + const exclusivePixelPaymentP = E(pixelIssuer).getExclusiveAll( + pixelPaymentP, + ); + + const useRightTransferRightBundleP = await E( + gallery, + ).transformToTransferAndUse(exclusivePixelPaymentP); + + const { + useRightPayment: useRightPaymentP, + } = useRightTransferRightBundleP; + + const useRightIssuer = E(useRightPaymentP).getIssuer(); + const exclusiveUseRightPaymentP = E(useRightIssuer).getExclusiveAll( + useRightPaymentP, + ); + + const changedAmount = await E(gallery).changeColor( + exclusiveUseRightPaymentP, + '#000000', + ); + return changedAmount; + }, + async doSendOnlyUseRight(bob) { + log('++ alice.doOnlySendUseRight starting'); + const pixelPaymentP = E(gallery).tapFaucet(); + log('tapped Faucet'); + + const pixelIssuer = E(pixelPaymentP).getIssuer(); + const exclusivePixelPaymentP = await E(pixelIssuer).getExclusiveAll( + pixelPaymentP, + ); + + const amount = await E(exclusivePixelPaymentP).getBalance(); + + const rawPixel = amount.quantity[0]; + + const origColor = await E(gallery).getColor(rawPixel.x, rawPixel.y); + + log( + `pixel x:${rawPixel.x}, y:${ + rawPixel.y + } has original color ${origColor}`, + ); + + const { + useRightPayment: useRightPaymentP, + transferRightPayment: transferRightPaymentP, + } = await E(gallery).transformToTransferAndUse( + exclusivePixelPaymentP, + ); + + const useRightIssuer = E(useRightPaymentP).getIssuer(); + const exclusiveUseRightPaymentP = E(useRightIssuer).getExclusiveAll( + useRightPaymentP, + ); + + const transferRightIssuer = E(transferRightPaymentP).getIssuer(); + const exclusiveTransferRightPaymentP = E( + transferRightIssuer, + ).getExclusiveAll(transferRightPaymentP); + + // we have gotten exclusive access to both the useRight and + // the transferRight payments. + + // send useRightPayment to Bob + // Alice keeps transferRightPayment + const result = await E(bob).receiveUseRight( + exclusiveUseRightPaymentP, + ); + const bobsRawPixel = result.quantity[0]; + insist( + bobsRawPixel.x === rawPixel.x && bobsRawPixel.y === rawPixel.y, + ); + const bobsColor = await E(gallery).getColor(rawPixel.x, rawPixel.y); + log( + `pixel x:${rawPixel.x}, y:${ + rawPixel.y + } changed to bob's color ${bobsColor}`, + ); + + // alice takes the right back + const pixelPayment2P = await E(gallery).transformToPixel( + exclusiveTransferRightPaymentP, + ); + const exclusivePixelPayment2P = await E(pixelIssuer).getExclusiveAll( + pixelPayment2P, + ); + const { useRightPayment: useRightPayment2P } = await E( + gallery, + ).transformToTransferAndUse(exclusivePixelPayment2P); + + const exclusiveUseRightPayment2P = await E( + useRightIssuer, + ).getExclusiveAll(useRightPayment2P); + + await E(gallery).changeColor( + exclusiveUseRightPayment2P, + '#9FBF95', // a light green + ); + + const alicesColor = await E(gallery).getColor(rawPixel.x, rawPixel.y); + log( + `pixel x:${rawPixel.x}, y:${ + rawPixel.y + } changed to alice's color ${alicesColor}`, + ); + + // tell bob to try to color, he can't + return E(bob) + .tryToColor() + .then( + _res => log('uh oh, bob was able to color'), + rej => log(`bob was unable to color: ${rej}`), + ); + }, + async doTapFaucetAndStore() { + log('++ alice.doTapFaucetAndStore starting'); + const pixelPaymentP = E(gallery).tapFaucet(); + const pixelIssuer = E(pixelPaymentP).getIssuer(); + const exclusivePixelPaymentP = await E(pixelIssuer).getExclusiveAll( + pixelPaymentP, + ); + + const { + useRightPayment: useRightPaymentP, + transferRightPayment: transferRightPaymentP, + } = await E(gallery).transformToTransferAndUse( + exclusivePixelPaymentP, + ); + + const useRightIssuer = E(useRightPaymentP).getIssuer(); + const exclusiveUseRightPaymentP = E(useRightIssuer).getExclusiveAll( + useRightPaymentP, + ); + + const transferRightIssuer = E(transferRightPaymentP).getIssuer(); + const exclusiveTransferRightPaymentP = E( + transferRightIssuer, + ).getExclusiveAll(transferRightPaymentP); + + storedUseRight = exclusiveUseRightPaymentP; + storedTransferRight = exclusiveTransferRightPaymentP; + + const amount = await E(storedUseRight).getBalance(); + + const rawPixel = amount.quantity[0]; + + return rawPixel; + }, + async checkAfterRevoked() { + log('++ alice.checkAfterRevoked starting'); + // changeColor throws an Error with an empty payment + // check transferRight is empty + E(gallery) + .changeColor( + storedUseRight, + '#9FBF95', // a light green + ) + .then( + _res => log(`successfully changed color, but shouldn't`), + rej => log(`successfully threw ${rej}`), + ); + + const amount = await E(storedTransferRight).getBalance(); + log( + `amount quantity should be an array of length 0: ${ + amount.quantity.length + }`, + ); + }, + }); + return alice; + }, + }); +} + +function setup(syscall, state, helpers) { + function log(...args) { + helpers.log(...args); + console.log(...args); + } + return helpers.makeLiveSlots(syscall, state, E => + harden({ + makeAliceMaker() { + return harden(makeAliceMaker(E, log)); + }, + }), + ); +} +export default harden(setup); diff --git a/demo/gallery/vat-bob.js b/demo/gallery/vat-bob.js new file mode 100644 index 00000000000..d9142cfb7ec --- /dev/null +++ b/demo/gallery/vat-bob.js @@ -0,0 +1,74 @@ +// Copyright (C) 2013 Google Inc, under Apache License 2.0 +// Copyright (C) 2018 Agoric, under Apache License 2.0 + +import harden from '@agoric/harden'; + +let storedExclusivePayment; + +function makeBobMaker(E, log) { + return harden({ + make(gallery) { + const bob = harden({ + /** + * This is not an imperative to Bob to buy something but rather + * the opposite. It is a request by a client to buy something from + * Bob, and therefore a request that Bob sell something. OO naming + * is a bit confusing here. + */ + async receiveUseRight(useRightPaymentP) { + log('++ bob.receiveUseRight starting'); + + const { useRightIssuer } = await E(gallery).getIssuers(); + const useRightPurse = E(useRightIssuer).makeEmptyPurse(); + // does bob know the amount that he is getting? + // use getExclusive() instead + const exclusiveUseRightPaymentP = E(useRightIssuer).getExclusiveAll( + useRightPaymentP, + ); + + // putting it in a purse isn't useful but it allows us to + // test the functionality + await E(useRightPurse).depositAll(exclusiveUseRightPaymentP); + const payment = await E(useRightPurse).withdrawAll(); + + const exclusivePayment = await E(useRightIssuer).getExclusiveAll( + payment, + ); + + // bob actually changes the color to light purple + const amountP = await E(gallery).changeColor( + exclusivePayment, + '#B695C0', + ); + + storedExclusivePayment = exclusivePayment; + return amountP; + }, + async tryToColor() { + // bob tries to change the color to light purple + const amountP = await E(gallery).changeColor( + storedExclusivePayment, + '#B695C0', + ); + return amountP; + }, + }); + return bob; + }, + }); +} + +function setup(syscall, state, helpers) { + function log(...args) { + helpers.log(...args); + console.log(...args); + } + return helpers.makeLiveSlots(syscall, state, E => + harden({ + makeBobMaker() { + return harden(makeBobMaker(E, log)); + }, + }), + ); +} +export default harden(setup); diff --git a/demo/gallery/vat-mint.js b/demo/gallery/vat-mint.js new file mode 100644 index 00000000000..f7f6393a94d --- /dev/null +++ b/demo/gallery/vat-mint.js @@ -0,0 +1,24 @@ +// Copyright (C) 2019 Agoric, under Apache License 2.0 + +import harden from '@agoric/harden'; + +import { makeMint } from '../../core/issuers'; + +function build(_E, _log) { + return harden({ makeMint }); +} +harden(build); + +function setup(syscall, state, helpers) { + function log(...args) { + helpers.log(...args); + console.log(...args); + } + return helpers.makeLiveSlots( + syscall, + state, + E => build(E, log), + helpers.vatID, + ); +} +export default harden(setup); diff --git a/demo/pixel-demo/bootstrap.js b/demo/pixel-demo/bootstrap.js index ce0e36ae397..3462465d911 100644 --- a/demo/pixel-demo/bootstrap.js +++ b/demo/pixel-demo/bootstrap.js @@ -12,8 +12,8 @@ function build(E, log) { // it. function showPaymentBalance(name, paymentP) { return E(paymentP) - .getXferBalance() - .then(amount => log(name, ' xfer balance ', amount)); + .getBalance() + .then(amount => log(name, ' balance ', amount)); } // TODO BUG: All callers should wait until settled before doing // anything that would change the balance before show*Balance* reads @@ -21,11 +21,8 @@ function build(E, log) { function showPurseBalances(name, purseP) { return Promise.all([ E(purseP) - .getXferBalance() - .then(amount => log(name, ' xfer balance ', amount)), - E(purseP) - .getUseBalance() - .then(amount => log(name, ' use balance ', amount)), + .getBalance() + .then(amount => log(name, ' balance ', amount)), ]); } diff --git a/demo/pixel-demo/vat-alice.js b/demo/pixel-demo/vat-alice.js index 7274cad4f07..381dbc4e15f 100644 --- a/demo/pixel-demo/vat-alice.js +++ b/demo/pixel-demo/vat-alice.js @@ -14,8 +14,8 @@ function makeAliceMaker(E, host, log) { // it. function showPaymentBalance(name, paymentP) { return E(paymentP) - .getXferBalance() - .then(amount => log(name, ' xfer balance ', amount)); + .getBalance() + .then(amount => log(name, ' balance ', amount)); } return harden({ @@ -48,9 +48,7 @@ function makeAliceMaker(E, host, log) { log('++ alice.acceptInvite starting'); showPaymentBalance('alice invite', allegedInvitePaymentP); - const allegedInviteAmountP = E( - allegedInvitePaymentP, - ).getXferBalance(); + const allegedInviteAmountP = E(allegedInvitePaymentP).getBalance(); const verifiedInviteP = E.resolve(allegedInviteAmountP).then( allegedInviteAmount => { @@ -117,9 +115,7 @@ function makeAliceMaker(E, host, log) { log('++ alice.acceptOptionDirectly starting'); showPaymentBalance('alice invite', allegedInvitePaymentP); - const allegedInviteAmountP = E( - allegedInvitePaymentP, - ).getXferBalance(); + const allegedInviteAmountP = E(allegedInvitePaymentP).getBalance(); const verifiedInvitePaymentP = E.resolve(allegedInviteAmountP).then( allegedInviteAmount => { @@ -184,7 +180,7 @@ function makeAliceMaker(E, host, log) { acceptOptionForFred(allegedInvitePaymentP) { log('++ alice.acceptOptionForFred starting'); const finNeededP = E(E(optFinIssuerP).getAssay()).make(55); - const inviteNeededP = E(allegedInvitePaymentP).getXferBalance(); + const inviteNeededP = E(allegedInvitePaymentP).getBalance(); const terms = harden([finNeededP, inviteNeededP]); const invitesP = E(escrowExchangeInstallationP).spawn(terms); diff --git a/demo/pixel-demo/vat-mint.js b/demo/pixel-demo/vat-mint.js index 8ef4dda31d9..07fccf64ec3 100644 --- a/demo/pixel-demo/vat-mint.js +++ b/demo/pixel-demo/vat-mint.js @@ -3,12 +3,13 @@ import harden from '@agoric/harden'; import { makeMint } from '../../core/issuers'; -import { makePixelListAssayMaker } from '../../more/pixels/pixelListAssay'; +import { makeCompoundPixelAssayMaker } from '../../more/pixels/pixelAssays'; +import { makeMintController } from '../../more/pixels/pixelMintController'; function build(_E, _log) { function makePixelListMint(canvasSize) { - const makePixelListAssay = makePixelListAssayMaker(canvasSize); - return makeMint('pixelList', makePixelListAssay); + const makePixelListAssay = makeCompoundPixelAssayMaker(canvasSize); + return makeMint('pixelList', makeMintController, makePixelListAssay); } return harden({ makePixelListMint, makeMint }); } diff --git a/more/pixels/gallery.js b/more/pixels/gallery.js new file mode 100644 index 00000000000..47efce26e73 --- /dev/null +++ b/more/pixels/gallery.js @@ -0,0 +1,288 @@ +import Nat from '@agoric/nat'; +import harden from '@agoric/harden'; + +import { + makeCompoundPixelAssayMaker, + makeTransferRightPixelAssayMaker, + makeUseRightPixelAssayMaker, +} from './pixelAssays'; +import { makeMint } from '../../core/issuers'; +import { makeWholePixelList, insistPixelList } from './types/pixelList'; +import { makeMintController } from './pixelMintController'; +import { makeLruQueue } from './lruQueue'; + +function mockStateChangeHandler(_newState) { + // does nothing +} + +export function makeGallery( + stateChangeHandler = mockStateChangeHandler, + canvasSize = 10, +) { + function getRandomColor() { + // TODO: actually getRandomColor in a deterministic way + // return `#${Math.floor(Math.random() * 16777215).toString(16)}`; + return '#D3D3D3'; + } + + function makeRandomData() { + const pixels = []; + for (let x = 0; x < canvasSize; x += 1) { + const pixelRow = []; + for (let y = 0; y < canvasSize; y += 1) { + pixelRow.push(getRandomColor()); + } + pixels.push(pixelRow); + } + return pixels; + } + const state = makeRandomData(); + + // provide state the canvas html page + function getState() { + return JSON.stringify(state); + } + + function setPixelState(pixel, newColor) { + state[pixel.x][pixel.y] = newColor; + // for now we pass the whole state + stateChangeHandler(getState()); + } + + // create all pixels (list of raw objs) + const allPixels = makeWholePixelList(canvasSize); + + // create LRU for "seemingly unpredictable" output from faucet + const { lruQueue, lruQueueBuilder } = makeLruQueue(); + + for (const pixel of allPixels) { + lruQueueBuilder.push(pixel); + } + lruQueueBuilder.resortArbitrarily(allPixels.length, 7); + + // START ERTP + + const makePixelListAssay = makeCompoundPixelAssayMaker(canvasSize); + const makeTransferAssay = makeTransferRightPixelAssayMaker(canvasSize); + const makeUseAssay = makeUseRightPixelAssayMaker(canvasSize); + + // a pixel represents the right to color and transfer the right to color + const pixelMint = makeMint('pixels', makeMintController, makePixelListAssay); + const pixelIssuer = pixelMint.getIssuer(); + const pixelAssay = pixelIssuer.getAssay(); + const pixelLabel = harden({ issuer: pixelIssuer, description: 'pixels' }); + + const transferRightMint = makeMint( + 'pixelTransferRights', + makeMintController, + makeTransferAssay, + ); + const useRightMint = makeMint( + 'pixelUseRights', + makeMintController, + makeUseAssay, + ); + const useRightIssuer = useRightMint.getIssuer(); + const useRightAssay = useRightIssuer.getAssay(); + const transferRightIssuer = transferRightMint.getIssuer(); + const transferRightAssay = transferRightIssuer.getAssay(); + + // Dust is the currency that the Gallery accepts for pixels + const dustMint = makeMint('dust'); + const dustIssuer = dustMint.getIssuer(); + const dustAssay = dustIssuer.getAssay(); + + // get the pixelList from the LRU + function makePixelPayment(rawPixelList) { + insistPixelList(rawPixelList, canvasSize); + const pixelAmount = { + label: pixelLabel, + quantity: rawPixelList, + }; + // we need to create this, since it was just destroyed + const newGalleryPurse = pixelMint.mint(pixelAmount, 'gallery'); + const payment = newGalleryPurse.withdraw(pixelAmount); + return payment; + } + + const gallerySplitPixelPurse = pixelIssuer.makeEmptyPurse(); + + // split pixelList into UseRights and TransferRights + async function transformToTransferAndUse(pixelListPaymentP) { + return Promise.resolve(pixelListPaymentP).then(async pixelListPayment => { + const pixelListAmount = pixelListPayment.getBalance(); + + const exclusivePayment = await pixelIssuer.getExclusiveAll( + pixelListPayment, + ); + await gallerySplitPixelPurse.depositAll(exclusivePayment); // conserve pixels + + const { transferAmount, useAmount } = pixelAssay.toTransferAndUseRights( + pixelListAmount, + useRightAssay, + transferRightAssay, + ); + + const transferRightPurse = transferRightMint.mint(transferAmount); + const useRightPurse = useRightMint.mint(useAmount); + + const transferRightPayment = await transferRightPurse.withdrawAll( + 'transferRights', + ); + const useRightPayment = await useRightPurse.withdrawAll('useRights'); + + return { + transferRightPayment, + useRightPayment, + }; + }); + } + + // merge UseRights and TransferRights into a pixel + async function transformToPixel(transferRightPaymentP) { + return Promise.resolve(transferRightPaymentP).then( + async transferRightPayment => { + // someone else may have the useRightPayment so we must destroy the + // useRight + + // we have an exclusive on the transfer right + const transferAmount = transferRightPayment.getBalance(); + await transferRightIssuer.getExclusiveAll(transferRightPayment); + + const pixelListAmount = transferRightAssay.toPixel( + transferAmount, + pixelAssay, + ); + + const { useAmount } = pixelAssay.toTransferAndUseRights( + pixelListAmount, + useRightAssay, + transferRightAssay, + ); + + // commit point + await useRightMint.destroy(useAmount); + await transferRightMint.destroy(transferAmount); + + const pixelPayment = await gallerySplitPixelPurse.withdraw( + pixelListAmount, + 'pixels', + ); // conserve pixels + return pixelPayment; + }, + ); + } + + function insistColor(_myColor) { + // TODO: check whether allowed + } + + async function changeColor(useRightPaymentP, newColor) { + return Promise.resolve(useRightPaymentP).then(async useRightPayment => { + const emptyAmount = useRightAssay.make(harden([])); + + // withdraw empty amount from payment + // if this doesn't error, it was a useRightPayment + useRightIssuer.getExclusive(emptyAmount, useRightPaymentP); + + const pixelAmount = useRightPayment.getBalance(); + + if (useRightAssay.isEmpty(pixelAmount)) { + throw new Error('no use rights present'); + } + insistColor(newColor); + + const pixelList = useRightAssay.quantity(pixelAmount); + + for (let i = 0; i < pixelList.length; i += 1) { + const pixel = pixelList[i]; + setPixelState(pixel, newColor); + } + return pixelAmount; + }); + } + + function revokePixel(rawPixel) { + const pixelList = harden([rawPixel]); + const pixelAmount = pixelAssay.make(pixelList); + const useRightAmount = useRightAssay.make(pixelList); + const transferRightAmount = transferRightAssay.make(pixelList); + + pixelMint.destroy(pixelAmount); + useRightMint.destroy(useRightAmount); + transferRightMint.destroy(transferRightAmount); + } + + function tapFaucet() { + const rawPixel = lruQueue.popToTail(); + revokePixel(rawPixel); + return makePixelPayment(harden([rawPixel])); + } + + function getDistance(a, b) { + const { x: xA, y: yA } = a; + const { x: xB, y: yB } = b; + return Math.floor(Math.sqrt((xA - xB) ** 2 + (yA - yB) ** 2)); + } + + function getDistanceFromCenter(rawPixel) { + const centerCoord = Math.floor(canvasSize / 2); + const center = { x: centerCoord, y: centerCoord }; + return getDistance(rawPixel, center); + } + + function pricePixel(rawPixel) { + const distance = getDistanceFromCenter(rawPixel); + // prices are simplistic for now + // they range from canvasSize / 2 to canvasSize + const price = canvasSize - distance; + return dustAssay.make(price); + } + + // anyone can getColor, no restrictions, no tokens + function getColor(x, y) { + const rawPixel = { x: Nat(x), y: Nat(y) }; + return state[rawPixel.x][rawPixel.y]; + } + + function getIssuers() { + return { + pixelIssuer, + useRightIssuer, + transferRightIssuer, + dustIssuer, + }; + } + + const userFacet = { + changeColor, + getColor, + tapFaucet, + transformToTransferAndUse, + transformToPixel, + getIssuers, + getCanvasSize() { + return canvasSize; + }, + }; + + const adminFacet = { + revokePixel, + getDistance, + getDistanceFromCenter, + pricePixel, + }; + + const readFacet = { + getState, + getColor, + }; + + const gallery = { + userFacet, + adminFacet, + readFacet, + }; + + return gallery; +} diff --git a/more/pixels/lruQueue.js b/more/pixels/lruQueue.js index 1dc195c6836..b5174e6eb23 100644 --- a/more/pixels/lruQueue.js +++ b/more/pixels/lruQueue.js @@ -7,7 +7,7 @@ import harden from '@agoric/harden'; // requeuing arbitrary entries to the tail. Initialize by pushing an arbitrary // number of items, then call resortArbitrarily() with the number of entries and // a step size (something prime and not a multiple of the row size). -function makeLruQueue() { +export function makeLruQueue() { function makeNode(obj, prev = null, next = null) { return { contents: obj, prev, next }; } @@ -106,8 +106,5 @@ function makeLruQueue() { const lruQueue = harden({ popToTail, requeue }); const lruQueueBuilder = harden({ push, resortArbitrarily, isEmpty }); - return { lruQueue, lruQueueBuilder }; + return harden({ lruQueue, lruQueueBuilder }); } -harden(makeLruQueue()); - -export { makeLruQueue }; diff --git a/more/pixels/pixelAssays.js b/more/pixels/pixelAssays.js new file mode 100644 index 00000000000..ad85d119268 --- /dev/null +++ b/more/pixels/pixelAssays.js @@ -0,0 +1,189 @@ +// Copyright (C) 2019 Agoric, under Apache License 2.0 + +import harden from '@agoric/harden'; + +import { insist } from '../../util/insist'; +import { + mustBeSameStructure, + mustBeComparable, +} from '../../util/sameStructure'; + +import { + insistPixelList, + includesPixelList, + withPixelList, + withoutPixelList, +} from './types/pixelList'; + +// A pixelList is a naive collection of pixels in the form: +// [ { x: 0, y: 0 }, { x: 1, y: 1} ...] +// This is less than ideal for efficiency and expressiveness but will +// do for now + +// a label is an object w/ the properties 'issuer' and 'description' +// issuer is an obj with methods like getExclusive & getEmptyPurse +// description is a string + +// our PixelLists should have the same issuer and the same description +// the description is "pixelList" + +function makeAbstractPixelListAssayMaker(canvasSize) { + function makeAbstractPixelListAssay(pixelLabel) { + mustBeComparable(pixelLabel); + + const brand = new WeakSet(); + + // our empty pixelList is an empty array + const emptyAmount = harden({ label: pixelLabel, quantity: [] }); + brand.add(emptyAmount); + + const assay = { + getLabel() { + return pixelLabel; + }, + + make(pixelList) { + mustBeComparable(pixelList); + insistPixelList(pixelList, canvasSize); + + if (pixelList.length === 0) { + return emptyAmount; + } + + const amount = harden({ label: pixelLabel, quantity: pixelList }); + brand.add(amount); + return amount; + }, + + vouch(amount) { + insist(brand.has(amount))`\ + Unrecognized amount: ${amount}`; + return amount; + }, + + coerce(allegedPixelListAmount) { + if (brand.has(allegedPixelListAmount)) { + return allegedPixelListAmount; + } + const { + label: allegedLabel, + quantity: pixelList, + } = allegedPixelListAmount; + mustBeSameStructure(pixelLabel, allegedLabel, 'Unrecognized label'); + return assay.make(pixelList); + }, + + quantity(amount) { + return assay.vouch(amount).quantity; + }, + + empty() { + return emptyAmount; + }, + + isEmpty(amount) { + return assay.quantity(amount).length === 0; + }, + + // does left include right? + includes(leftAmount, rightAmount) { + const leftPixelList = assay.quantity(leftAmount); + const rightPixelList = assay.quantity(rightAmount); + + return includesPixelList(leftPixelList, rightPixelList); + }, + + // set union + with(leftAmount, rightAmount) { + const leftPixelList = assay.quantity(leftAmount); + const rightPixelList = assay.quantity(rightAmount); + + const resultPixelList = withPixelList(leftPixelList, rightPixelList); + + return assay.make(harden(resultPixelList)); + }, + + // Covering set subtraction of erights. + // If leftAmount does not include rightAmount, error. + // Describe the erights described by `leftAmount` and not described + // by `rightAmount`. + without(leftAmount, rightAmount) { + const leftPixelList = assay.quantity(leftAmount); + const rightPixelList = assay.quantity(rightAmount); + + const resultPixelList = withoutPixelList(leftPixelList, rightPixelList); + + return assay.make(harden(resultPixelList)); + }, + }; + return harden(assay); + } + return harden(makeAbstractPixelListAssay); +} +harden(makeAbstractPixelListAssayMaker); + +function makeCompoundPixelAssayMaker(canvasSize) { + const superPixelAssayMaker = makeAbstractPixelListAssayMaker(canvasSize); + function makeCompoundPixelAssay(pixelLabel) { + const superPixelAssay = superPixelAssayMaker(pixelLabel); + const compoundPixelAssay = harden({ + ...superPixelAssay, + // my pixelList of length one : [ {x: 0, y:0 }] should turn + // into two amounts: useRights [{ x: 0, y: 0}] and + // transferRights [{ x: 0, y: 0 }] + + // pixelListAmount -> useRightsAmount and transferRightsAmount + toTransferAndUseRights(srcAmount, useRightAssay, transferRightAssay) { + const srcPixelList = compoundPixelAssay.quantity(srcAmount); + + const useAmount = useRightAssay.make(harden(srcPixelList)); + const transferAmount = transferRightAssay.make(harden(srcPixelList)); + + return harden({ + transferAmount, + useAmount, + }); + }, + }); + return compoundPixelAssay; + } + return harden(makeCompoundPixelAssay); +} +harden(makeCompoundPixelAssayMaker); + +function makeTransferRightPixelAssayMaker(canvasSize) { + const superPixelAssayMaker = makeAbstractPixelListAssayMaker(canvasSize); + function makeTransferAssay(pixelLabel) { + const superPixelAssay = superPixelAssayMaker(pixelLabel); + const transferAssay = harden({ + ...superPixelAssay, + toPixel(transferAmount, pixelAssay) { + const quantity = transferAssay.quantity(transferAmount); + const pixelAmount = pixelAssay.make(harden(quantity)); + return pixelAmount; + }, + }); + return transferAssay; + } + return harden(makeTransferAssay); +} +harden(makeTransferRightPixelAssayMaker); + +function makeUseRightPixelAssayMaker(canvasSize) { + const superPixelAssayMaker = makeAbstractPixelListAssayMaker(canvasSize); + function makeUseAssay(pixelLabel) { + const superPixelAssay = superPixelAssayMaker(pixelLabel); + const useAssay = harden({ + ...superPixelAssay, + }); + return useAssay; + } + return harden(makeUseAssay); +} +harden(makeUseRightPixelAssayMaker); + +export { + makeCompoundPixelAssayMaker, + makeTransferRightPixelAssayMaker, + makeUseRightPixelAssayMaker, +}; diff --git a/more/pixels/pixelListAssay.js b/more/pixels/pixelListAssay.js deleted file mode 100644 index 1cd62251beb..00000000000 --- a/more/pixels/pixelListAssay.js +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (C) 2019 Agoric, under Apache License 2.0 - -import harden from '@agoric/harden'; - -import { insist } from '../../util/insist'; -import { - mustBeSameStructure, - mustBeComparable, -} from '../../util/sameStructure'; - -import { - insistPixelList, - includesPixelList, - withPixelList, - withoutPixelList, -} from './types/pixelList'; - -// A pixelList is a naive collection of pixels in the form: -// [ { x: 0, y: 0 }, { x: 1, y: 1} ...] -// This is less than ideal for efficiency and expressiveness but will -// do for now - -// a label is an object w/ the properties 'issuer' and 'description' -// issuer is an obj with methods like getExclusive & getEmptyPurse -// description is a string - -// our PixelLists should have the same issuer and the same description -// the description is "pixelList" - -function makePixelListAssayMaker(canvasSize) { - function makePixelListAssay(label) { - mustBeComparable(label); - - const brand = new WeakSet(); - - // our empty pixelList is an empty array - const emptyAmount = harden({ label, pixelList: [] }); - brand.add(emptyAmount); - - const assay = harden({ - getLabel() { - return label; - }, - - make(pixelList) { - mustBeComparable(pixelList); - insistPixelList(pixelList, canvasSize); - - if (pixelList.length === 0) { - return emptyAmount; - } - - const amount = harden({ label, quantity: pixelList }); - brand.add(amount); - return amount; - }, - - vouch(amount) { - insist(brand.has(amount))`\ - Unrecognized amount: ${amount}`; - return amount; - }, - - coerce(allegedPixelListAmount) { - if (brand.has(allegedPixelListAmount)) { - return allegedPixelListAmount; - } - const { - label: allegedLabel, - quantity: pixelList, - } = allegedPixelListAmount; - mustBeSameStructure(label, allegedLabel, 'Unrecognized label'); - return assay.make(pixelList); - }, - - quantity(amount) { - return assay.vouch(amount).quantity; - }, - - empty() { - return emptyAmount; - }, - - isEmpty(amount) { - return assay.quantity(amount) === []; - }, - - // does left include right? - includes(leftAmount, rightAmount) { - const leftPixelList = assay.quantity(leftAmount); - const rightPixelList = assay.quantity(rightAmount); - - return includesPixelList(leftPixelList, rightPixelList); - }, - - // set union - with(leftAmount, rightAmount) { - const leftPixelList = assay.quantity(leftAmount); - const rightPixelList = assay.quantity(rightAmount); - - return harden({ - label, - quantity: withPixelList(leftPixelList, rightPixelList), - }); - }, - - // Covering set subtraction of erights. - // If leftAmount does not include rightAmount, error. - // Describe the erights described by `leftAmount` and not described - // by `rightAmount`. - without(leftAmount, rightAmount) { - const leftPixelList = assay.quantity(leftAmount); - const rightPixelList = assay.quantity(rightAmount); - - const pixelList = withoutPixelList(leftPixelList, rightPixelList); - - return harden({ - label, - quantity: pixelList, - }); - }, - }); - return assay; - } - return harden(makePixelListAssay); -} -harden(makePixelListAssayMaker); - -export { makePixelListAssayMaker }; diff --git a/more/pixels/pixelMintController.js b/more/pixels/pixelMintController.js new file mode 100644 index 00000000000..b66f159ea79 --- /dev/null +++ b/more/pixels/pixelMintController.js @@ -0,0 +1,95 @@ +import { makePrivateName } from '../../util/PrivateName'; + +import { getString } from './types/pixel'; + +export function makeMintController(assay) { + // Map from purse or payment to the rights it currently + // holds. Rights can move via payments + + // purse/payment to amount + let rights = makePrivateName(); + + // pixel to purse/payment + const pixelToPursePayment = new Map(); + + function setLocation(amount, pursePayment) { + // purse/payment is the key of rights + amount = assay.coerce(amount); + const pixelList = assay.quantity(amount); + for (const pixel of pixelList) { + pixelToPursePayment.set(getString(pixel), pursePayment); + } + } + + function destroy(amount) { + // amount must only contain one pixel + const pixelList = assay.quantity(amount); + // assume length === 1 for now + + const pixel = pixelList[0]; + const strPixel = getString(pixel); + if (!pixelToPursePayment.has(strPixel)) { + return; + } + const location = pixelToPursePayment.get(strPixel); + // amount is guaranteed to be there + // eslint-disable-next-line no-use-before-define + amount = assay.coerce(amount); + const srcOldRightsAmount = rights.get(location); + // eslint-disable-next-line no-use-before-define + const srcNewRightsAmount = assay.without(srcOldRightsAmount, amount); + + // ///////////////// commit point ////////////////// + // All queries above passed with no side effects. + // During side effects below, any early exits should be made into + // fatal turn aborts. + + rights.set(location, srcNewRightsAmount); + setLocation(srcNewRightsAmount, location); + + // delete pixel from pixelToPursePayment + pixelToPursePayment.delete(pixel); + } + + function destroyAll() { + rights = makePrivateName(); // reset rights + } + + function recordPayment(src, payment, amount, srcNewRightsAmount) { + rights.set(src, srcNewRightsAmount); + setLocation(srcNewRightsAmount, src); + rights.init(payment, amount); + setLocation(amount, payment); + } + + function recordDeposit( + srcPayment, + srcNewRightsAmount, + purse, + purseNewRightsAmount, + ) { + rights.set(srcPayment, srcNewRightsAmount); + setLocation(srcNewRightsAmount, srcPayment); + rights.set(purse, purseNewRightsAmount); + setLocation(purseNewRightsAmount, purse); + } + + function recordMint(purse, initialAmount) { + rights.init(purse, initialAmount); + setLocation(initialAmount, purse); + } + + function getAmount(pursePayment) { + return rights.get(pursePayment); + } + + const mintController = { + destroy, + destroyAll, + recordPayment, + recordDeposit, + recordMint, + getAmount, + }; + return mintController; +} diff --git a/more/pixels/types/pixel.js b/more/pixels/types/pixel.js index aa77d67bb8b..ea5f6a10966 100644 --- a/more/pixels/types/pixel.js +++ b/more/pixels/types/pixel.js @@ -33,4 +33,14 @@ function isLessThanOrEqual(leftPixel, rightPixel) { return leftPixel.x <= rightPixel.x && leftPixel.y <= rightPixel.y; } -export { insistWithinBounds, insistPixel, isEqual, isLessThanOrEqual }; +function getString(pixel) { + return `x${pixel.x}y${pixel.y}`; +} + +export { + insistWithinBounds, + insistPixel, + isEqual, + isLessThanOrEqual, + getString, +}; diff --git a/more/pixels/types/pixelList.js b/more/pixels/types/pixelList.js index be1c6623063..1de7be1938e 100644 --- a/more/pixels/types/pixelList.js +++ b/more/pixels/types/pixelList.js @@ -88,6 +88,13 @@ function makeWholePixelList(canvasSize) { return pixelList; } +function insistPixelListEqual(leftPixelList, rightPixelList) { + // includes both ways, super inefficient + // if pixelLists were ordered, this would be must more efficient + insistIncludesPixelList(leftPixelList, rightPixelList); + insistIncludesPixelList(rightPixelList, leftPixelList); +} + export { insistPixelList, includesPixel, @@ -96,4 +103,5 @@ export { withPixelList, withoutPixelList, makeWholePixelList, + insistPixelListEqual, }; diff --git a/test/demo/test-demos.js b/test/demo/test-demos.js index 96ca7712752..2d6bd3edc97 100644 --- a/test/demo/test-demos.js +++ b/test/demo/test-demos.js @@ -17,12 +17,10 @@ const contractMintGolden = [ '=> setup called', 'starting mintTestAssay', 'starting mintTestNumber', - 'alice xfer balance {"label":{"issuer":{},"description":"quatloos"},"quantity":950}', - 'alice use balance {"label":{"issuer":{},"description":"quatloos"},"quantity":1000}', - 'payment xfer balance {"label":{"issuer":{},"description":"quatloos"},"quantity":50}', - 'alice xfer balance {"label":{"issuer":{},"description":"bucks"},"quantity":950}', - 'alice use balance {"label":{"issuer":{},"description":"bucks"},"quantity":1000}', - 'payment xfer balance {"label":{"issuer":{},"description":"bucks"},"quantity":50}', + 'alice balance {"label":{"issuer":{},"description":"quatloos"},"quantity":950}', + 'payment balance {"label":{"issuer":{},"description":"quatloos"},"quantity":50}', + 'alice balance {"label":{"issuer":{},"description":"bucks"},"quantity":950}', + 'payment balance {"label":{"issuer":{},"description":"bucks"},"quantity":50}', ]; test('run contractHost Demo --mint with SES', async t => { @@ -41,10 +39,10 @@ const contractTrivialGolden = [ '=> setup called', 'starting trivialContractTest', "Does source function trivContract(terms, inviteMaker) {\n return inviteMaker.make('foo', 8);\n } match? true", - 'foo xfer balance {"label":{"issuer":{},"description":"contract host"},"quantity":{"installation":{},"terms":"foo terms","seatIdentity":{},"seatDesc":"foo"}}', + 'foo balance {"label":{"issuer":{},"description":"contract host"},"quantity":{"installation":{},"terms":"foo terms","seatIdentity":{},"seatDesc":"foo"}}', '++ eightP resolved to 8 (should be 8)', '++ DONE', - 'foo xfer balance {"label":{"issuer":{},"description":"contract host"},"quantity":null}', + 'foo balance {"label":{"issuer":{},"description":"contract host"},"quantity":null}', ]; test('run contractHost Demo --trivial with SES', async t => { @@ -82,21 +80,17 @@ const contractBobFirstGolden = [ '=> setup called', '++ bob.tradeWell starting', '++ alice.acceptInvite starting', - 'alice invite xfer balance {"label":{"issuer":{},"description":"contract host"},"quantity":{"installation":{},"terms":[{"label":{"issuer":{},"description":"clams"},"quantity":10},{"label":{"issuer":{},"description":"fudco"},"quantity":7}],"seatIdentity":{},"seatDesc":"left"}}', - 'verified invite xfer balance {"label":{"issuer":{},"description":"contract host"},"quantity":{"installation":{},"terms":[{"label":{"issuer":{},"description":"clams"},"quantity":10},{"label":{"issuer":{},"description":"fudco"},"quantity":7}],"seatIdentity":{},"seatDesc":"left"}}', + 'alice invite balance {"label":{"issuer":{},"description":"contract host"},"quantity":{"installation":{},"terms":[{"label":{"issuer":{},"description":"clams"},"quantity":10},{"label":{"issuer":{},"description":"fudco"},"quantity":7}],"seatIdentity":{},"seatDesc":"left"}}', + 'verified invite balance {"label":{"issuer":{},"description":"contract host"},"quantity":{"installation":{},"terms":[{"label":{"issuer":{},"description":"clams"},"quantity":10},{"label":{"issuer":{},"description":"fudco"},"quantity":7}],"seatIdentity":{},"seatDesc":"left"}}', 'bob escrow wins: {"label":{"issuer":{},"description":"clams"},"quantity":10} refs: null', 'alice escrow wins: {"label":{"issuer":{},"description":"fudco"},"quantity":7} refs: null', '++ bob.tradeWell done', '++ bobP.tradeWell done:[[{"label":{"issuer":{},"description":"fudco"},"quantity":7},null],[{"label":{"issuer":{},"description":"clams"},"quantity":10},null]]', '++ DONE', - 'alice money xfer balance {"label":{"issuer":{},"description":"clams"},"quantity":990}', - 'alice money use balance {"label":{"issuer":{},"description":"clams"},"quantity":990}', - 'alice stock xfer balance {"label":{"issuer":{},"description":"fudco"},"quantity":2009}', - 'alice stock use balance {"label":{"issuer":{},"description":"fudco"},"quantity":2009}', - 'bob money xfer balance {"label":{"issuer":{},"description":"clams"},"quantity":1011}', - 'bob money use balance {"label":{"issuer":{},"description":"clams"},"quantity":1011}', - 'bob stock xfer balance {"label":{"issuer":{},"description":"fudco"},"quantity":1996}', - 'bob stock use balance {"label":{"issuer":{},"description":"fudco"},"quantity":1996}', + 'alice money balance {"label":{"issuer":{},"description":"clams"},"quantity":990}', + 'alice stock balance {"label":{"issuer":{},"description":"fudco"},"quantity":2009}', + 'bob money balance {"label":{"issuer":{},"description":"clams"},"quantity":1011}', + 'bob stock balance {"label":{"issuer":{},"description":"fudco"},"quantity":1996}', ]; test('run contractHost Demo --bob-first with SES', async t => { @@ -116,21 +110,17 @@ const contractCoveredCallGolden = [ '++ bob.offerAliceOption starting', '++ alice.acceptOptionDirectly starting', 'Pretend singularity never happens', - 'alice invite xfer balance {"label":{"issuer":{},"description":"contract host"},"quantity":{"installation":{},"terms":[{},{"label":{"issuer":{},"description":"smackers"},"quantity":10},{"label":{"issuer":{},"description":"yoyodyne"},"quantity":7},{},"singularity"],"seatIdentity":{},"seatDesc":"holder"}}', - 'verified invite xfer balance {"label":{"issuer":{},"description":"contract host"},"quantity":{"installation":{},"terms":[{},{"label":{"issuer":{},"description":"smackers"},"quantity":10},{"label":{"issuer":{},"description":"yoyodyne"},"quantity":7},{},"singularity"],"seatIdentity":{},"seatDesc":"holder"}}', + 'alice invite balance {"label":{"issuer":{},"description":"contract host"},"quantity":{"installation":{},"terms":[{},{"label":{"issuer":{},"description":"smackers"},"quantity":10},{"label":{"issuer":{},"description":"yoyodyne"},"quantity":7},{},"singularity"],"seatIdentity":{},"seatDesc":"holder"}}', + 'verified invite balance {"label":{"issuer":{},"description":"contract host"},"quantity":{"installation":{},"terms":[{},{"label":{"issuer":{},"description":"smackers"},"quantity":10},{"label":{"issuer":{},"description":"yoyodyne"},"quantity":7},{},"singularity"],"seatIdentity":{},"seatDesc":"holder"}}', 'alice option wins: {"label":{"issuer":{},"description":"yoyodyne"},"quantity":7} refs: null', 'bob option wins: {"label":{"issuer":{},"description":"smackers"},"quantity":10} refs: null', '++ bob.offerAliceOption done', '++ bobP.offerAliceOption done:[[{"label":{"issuer":{},"description":"yoyodyne"},"quantity":7},null],[{"label":{"issuer":{},"description":"smackers"},"quantity":10},null]]', '++ DONE', - 'alice money xfer balance {"label":{"issuer":{},"description":"smackers"},"quantity":990}', - 'alice money use balance {"label":{"issuer":{},"description":"smackers"},"quantity":990}', - 'alice stock xfer balance {"label":{"issuer":{},"description":"yoyodyne"},"quantity":2009}', - 'alice stock use balance {"label":{"issuer":{},"description":"yoyodyne"},"quantity":2009}', - 'bob money xfer balance {"label":{"issuer":{},"description":"smackers"},"quantity":1011}', - 'bob money use balance {"label":{"issuer":{},"description":"smackers"},"quantity":1011}', - 'bob stock xfer balance {"label":{"issuer":{},"description":"yoyodyne"},"quantity":1996}', - 'bob stock use balance {"label":{"issuer":{},"description":"yoyodyne"},"quantity":1996}', + 'alice money balance {"label":{"issuer":{},"description":"smackers"},"quantity":990}', + 'alice stock balance {"label":{"issuer":{},"description":"yoyodyne"},"quantity":2009}', + 'bob money balance {"label":{"issuer":{},"description":"smackers"},"quantity":1011}', + 'bob stock balance {"label":{"issuer":{},"description":"yoyodyne"},"quantity":1996}', ]; test('run contractHost Demo --covered-call with SES', async t => { @@ -160,22 +150,14 @@ const contractCoveredCallSaleGolden = [ '++ bob.offerAliceOption done', '++ bobP.offerAliceOption done:[[[{"label":{"issuer":{},"description":"wonka"},"quantity":7},null],[{"label":{"issuer":{},"description":"fins"},"quantity":55},null]],[{"label":{"issuer":{},"description":"dough"},"quantity":10},null]]', '++ DONE', - 'alice dough xfer balance {"label":{"issuer":{},"description":"dough"},"quantity":1000}', - 'alice dough use balance {"label":{"issuer":{},"description":"dough"},"quantity":1000}', - 'alice stock xfer balance {"label":{"issuer":{},"description":"wonka"},"quantity":2002}', - 'alice stock use balance {"label":{"issuer":{},"description":"wonka"},"quantity":2002}', - 'alice fins xfer balance {"label":{"issuer":{},"description":"fins"},"quantity":3055}', - 'alice fins use balance {"label":{"issuer":{},"description":"fins"},"quantity":3055}', - 'bob dough xfer balance {"label":{"issuer":{},"description":"dough"},"quantity":1011}', - 'bob dough use balance {"label":{"issuer":{},"description":"dough"},"quantity":1011}', - 'bob stock xfer balance {"label":{"issuer":{},"description":"wonka"},"quantity":1996}', - 'bob stock use balance {"label":{"issuer":{},"description":"wonka"},"quantity":1996}', - 'fred dough xfer balance {"label":{"issuer":{},"description":"dough"},"quantity":992}', - 'fred dough use balance {"label":{"issuer":{},"description":"dough"},"quantity":992}', - 'fred stock xfer balance {"label":{"issuer":{},"description":"wonka"},"quantity":2011}', - 'fred stock use balance {"label":{"issuer":{},"description":"wonka"},"quantity":2011}', - 'fred fins xfer balance {"label":{"issuer":{},"description":"fins"},"quantity":2946}', - 'fred fins use balance {"label":{"issuer":{},"description":"fins"},"quantity":2946}', + 'alice dough balance {"label":{"issuer":{},"description":"dough"},"quantity":1000}', + 'alice stock balance {"label":{"issuer":{},"description":"wonka"},"quantity":2002}', + 'alice fins balance {"label":{"issuer":{},"description":"fins"},"quantity":3055}', + 'bob dough balance {"label":{"issuer":{},"description":"dough"},"quantity":1011}', + 'bob stock balance {"label":{"issuer":{},"description":"wonka"},"quantity":1996}', + 'fred dough balance {"label":{"issuer":{},"description":"dough"},"quantity":992}', + 'fred stock balance {"label":{"issuer":{},"description":"wonka"},"quantity":2011}', + 'fred fins balance {"label":{"issuer":{},"description":"fins"},"quantity":2946}', ]; test('run contractHost Demo --covered-call-sale with SES', async t => { @@ -264,9 +246,8 @@ test('run handoff Demo --Two Party handoff', async t => { const successfulWithdraw = [ '=> setup called', 'starting mintTestPixelListAssay', - 'alice xfer balance {"label":{"issuer":{},"description":"pixelList"},"quantity":[{"x":0,"y":1},{"x":1,"y":0},{"x":1,"y":1}]}', - 'alice use balance {"label":{"issuer":{},"description":"pixelList"},"quantity":[{"x":0,"y":0},{"x":0,"y":1},{"x":1,"y":0},{"x":1,"y":1}]}', - 'payment xfer balance {"label":{"issuer":{},"description":"pixelList"},"quantity":[{"x":0,"y":0}]}', + 'alice balance {"label":{"issuer":{},"description":"pixelList"},"quantity":[{"x":0,"y":1},{"x":1,"y":0},{"x":1,"y":1}]}', + 'payment balance {"label":{"issuer":{},"description":"pixelList"},"quantity":[{"x":0,"y":0}]}', ]; test('run Pixel Demo mint and withdraw with SES', async t => { @@ -307,21 +288,17 @@ const contractBobFirstGoldenPixel = [ '=> setup called', '++ bob.tradeWell starting', '++ alice.acceptInvite starting', - 'alice invite xfer balance {"label":{"issuer":{},"description":"contract host"},"quantity":{"installation":{},"terms":[{"label":{"issuer":{},"description":"clams"},"quantity":10},{"label":{"issuer":{},"description":"pixelList"},"quantity":[{"x":1,"y":1}]}],"seatIdentity":{},"seatDesc":"left"}}', - 'verified invite xfer balance {"label":{"issuer":{},"description":"contract host"},"quantity":{"installation":{},"terms":[{"label":{"issuer":{},"description":"clams"},"quantity":10},{"label":{"issuer":{},"description":"pixelList"},"quantity":[{"x":1,"y":1}]}],"seatIdentity":{},"seatDesc":"left"}}', + 'alice invite balance {"label":{"issuer":{},"description":"contract host"},"quantity":{"installation":{},"terms":[{"label":{"issuer":{},"description":"clams"},"quantity":10},{"label":{"issuer":{},"description":"pixelList"},"quantity":[{"x":1,"y":1}]}],"seatIdentity":{},"seatDesc":"left"}}', + 'verified invite balance {"label":{"issuer":{},"description":"contract host"},"quantity":{"installation":{},"terms":[{"label":{"issuer":{},"description":"clams"},"quantity":10},{"label":{"issuer":{},"description":"pixelList"},"quantity":[{"x":1,"y":1}]}],"seatIdentity":{},"seatDesc":"left"}}', 'bob escrow wins: {"label":{"issuer":{},"description":"clams"},"quantity":10} refs: null', 'alice escrow wins: {"label":{"issuer":{},"description":"pixelList"},"quantity":[{"x":1,"y":1}]} refs: null', '++ bob.tradeWell done', '++ bobP.tradeWell done:[[{"label":{"issuer":{},"description":"pixelList"},"quantity":[{"x":1,"y":1}]},null],[{"label":{"issuer":{},"description":"clams"},"quantity":10},null]]', '++ DONE', - 'alice money xfer balance {"label":{"issuer":{},"description":"clams"},"quantity":990}', - 'alice money use balance {"label":{"issuer":{},"description":"clams"},"quantity":990}', - 'alice pixels xfer balance {"label":{"issuer":{},"description":"pixelList"},"quantity":[{"x":0,"y":0},{"x":0,"y":1},{"x":1,"y":1}]}', - 'alice pixels use balance {"label":{"issuer":{},"description":"pixelList"},"quantity":[{"x":0,"y":0},{"x":0,"y":1},{"x":1,"y":1}]}', - 'bob money xfer balance {"label":{"issuer":{},"description":"clams"},"quantity":1011}', - 'bob money use balance {"label":{"issuer":{},"description":"clams"},"quantity":1011}', - 'bob pixels xfer balance {"label":{"issuer":{},"description":"pixelList"},"quantity":[{"x":1,"y":0}]}', - 'bob pixels use balance {"label":{"issuer":{},"description":"pixelList"},"quantity":[{"x":1,"y":0}]}', + 'alice money balance {"label":{"issuer":{},"description":"clams"},"quantity":990}', + 'alice pixels balance {"label":{"issuer":{},"description":"pixelList"},"quantity":[{"x":0,"y":0},{"x":0,"y":1},{"x":1,"y":1}]}', + 'bob money balance {"label":{"issuer":{},"description":"clams"},"quantity":1011}', + 'bob pixels balance {"label":{"issuer":{},"description":"pixelList"},"quantity":[{"x":1,"y":0}]}', ]; test('run Pixel Demo --bob-first with SES', async t => { @@ -341,21 +318,17 @@ const contractCoveredCallGoldenPixel = [ '++ bob.offerAliceOption starting', '++ alice.acceptOptionDirectly starting', 'Pretend singularity never happens', - 'alice invite xfer balance {"label":{"issuer":{},"description":"contract host"},"quantity":{"installation":{},"terms":[{},{"label":{"issuer":{},"description":"smackers"},"quantity":10},{"label":{"issuer":{},"description":"pixelList"},"quantity":[{"x":1,"y":1}]},{},"singularity"],"seatIdentity":{},"seatDesc":"holder"}}', - 'verified invite xfer balance {"label":{"issuer":{},"description":"contract host"},"quantity":{"installation":{},"terms":[{},{"label":{"issuer":{},"description":"smackers"},"quantity":10},{"label":{"issuer":{},"description":"pixelList"},"quantity":[{"x":1,"y":1}]},{},"singularity"],"seatIdentity":{},"seatDesc":"holder"}}', + 'alice invite balance {"label":{"issuer":{},"description":"contract host"},"quantity":{"installation":{},"terms":[{},{"label":{"issuer":{},"description":"smackers"},"quantity":10},{"label":{"issuer":{},"description":"pixelList"},"quantity":[{"x":1,"y":1}]},{},"singularity"],"seatIdentity":{},"seatDesc":"holder"}}', + 'verified invite balance {"label":{"issuer":{},"description":"contract host"},"quantity":{"installation":{},"terms":[{},{"label":{"issuer":{},"description":"smackers"},"quantity":10},{"label":{"issuer":{},"description":"pixelList"},"quantity":[{"x":1,"y":1}]},{},"singularity"],"seatIdentity":{},"seatDesc":"holder"}}', 'alice option wins: {"label":{"issuer":{},"description":"pixelList"},"quantity":[{"x":1,"y":1}]} refs: null', 'bob option wins: {"label":{"issuer":{},"description":"smackers"},"quantity":10} refs: null', '++ bob.offerAliceOption done', '++ bobP.offerAliceOption done:[[{"label":{"issuer":{},"description":"pixelList"},"quantity":[{"x":1,"y":1}]},null],[{"label":{"issuer":{},"description":"smackers"},"quantity":10},null]]', '++ DONE', - 'alice money xfer balance {"label":{"issuer":{},"description":"smackers"},"quantity":990}', - 'alice money use balance {"label":{"issuer":{},"description":"smackers"},"quantity":990}', - 'alice pixel xfer balance {"label":{"issuer":{},"description":"pixelList"},"quantity":[{"x":0,"y":0},{"x":0,"y":1},{"x":1,"y":1}]}', - 'alice pixel use balance {"label":{"issuer":{},"description":"pixelList"},"quantity":[{"x":0,"y":0},{"x":0,"y":1},{"x":1,"y":1}]}', - 'bob money xfer balance {"label":{"issuer":{},"description":"smackers"},"quantity":1011}', - 'bob money use balance {"label":{"issuer":{},"description":"smackers"},"quantity":1011}', - 'bob pixel xfer balance {"label":{"issuer":{},"description":"pixelList"},"quantity":[{"x":1,"y":0}]}', - 'bob pixel use balance {"label":{"issuer":{},"description":"pixelList"},"quantity":[{"x":1,"y":0}]}', + 'alice money balance {"label":{"issuer":{},"description":"smackers"},"quantity":990}', + 'alice pixel balance {"label":{"issuer":{},"description":"pixelList"},"quantity":[{"x":0,"y":0},{"x":0,"y":1},{"x":1,"y":1}]}', + 'bob money balance {"label":{"issuer":{},"description":"smackers"},"quantity":1011}', + 'bob pixel balance {"label":{"issuer":{},"description":"pixelList"},"quantity":[{"x":1,"y":0}]}', ]; test('run Pixel Demo --covered-call with SES', async t => { diff --git a/test/demo/test-gallery-demo.js b/test/demo/test-gallery-demo.js new file mode 100644 index 00000000000..0b9eb7405ef --- /dev/null +++ b/test/demo/test-gallery-demo.js @@ -0,0 +1,105 @@ +import { test } from 'tape-promise/tape'; +import { loadBasedir, buildVatController } from '@agoric/swingset-vat'; + +async function main(withSES, basedir, argv) { + const config = await loadBasedir(basedir); + const ldSrcPath = require.resolve( + '@agoric/swingset-vat/src/devices/loopbox-src', + ); + config.devices = [['loopbox', ldSrcPath, {}]]; + + const controller = await buildVatController(config, withSES, argv); + await controller.run(); + return controller.dump(); +} + +const expectedTapFaucetLog = [ + '=> setup called', + 'starting tapFaucet', + 'alice is made', + 'starting testTapFaucet', + '++ alice.doTapFaucet starting', + 'pixel from faucet balance {"label":{"issuer":{},"description":"pixels"},"quantity":[{"x":1,"y":4}]}', +]; + +test('run gallery demo tapFaucet with SES', async t => { + const dump = await main(true, 'demo/gallery', ['tapFaucet']); + t.deepEquals(dump.log, expectedTapFaucetLog); + t.end(); +}); + +test('run gallery demo tapFaucet without SES', async t => { + const dump = await main(false, 'demo/gallery', ['tapFaucet']); + t.deepEquals(dump.log, expectedTapFaucetLog); + t.end(); +}); + +const expectedAliceChangesColorLog = [ + '=> setup called', + 'starting aliceChangesColor', + 'alice is made', + 'starting testAliceChangesColor', + '++ alice.doChangeColor starting', + 'tapped Faucet', + 'current color #000000', +]; + +test('run gallery demo aliceChangesColor with SES', async t => { + const dump = await main(true, 'demo/gallery', ['aliceChangesColor']); + t.deepEquals(dump.log, expectedAliceChangesColorLog); + t.end(); +}); + +test('run gallery demo aliceChangesColor without SES', async t => { + const dump = await main(false, 'demo/gallery', ['aliceChangesColor']); + t.deepEquals(dump.log, expectedAliceChangesColorLog); + t.end(); +}); + +const expectedAliceSendsOnlyUseRightLog = [ + '=> setup called', + 'starting aliceSendsOnlyUseRight', + 'alice is made', + 'starting testAliceSendsOnlyUseRight', + '++ alice.doOnlySendUseRight starting', + 'tapped Faucet', + 'pixel x:1, y:4 has original color #D3D3D3', + '++ bob.receiveUseRight starting', + "pixel x:1, y:4 changed to bob's color #B695C0", + "pixel x:1, y:4 changed to alice's color #9FBF95", + 'bob was unable to color: Error: no use rights present', +]; + +test('run gallery demo aliceSendsOnlyUseRight with SES', async t => { + const dump = await main(true, 'demo/gallery', ['aliceSendsOnlyUseRight']); + t.deepEquals(dump.log, expectedAliceSendsOnlyUseRightLog); + t.end(); +}); + +test('run gallery demo aliceSendsOnlyUseRight without SES', async t => { + const dump = await main(false, 'demo/gallery', ['aliceSendsOnlyUseRight']); + t.deepEquals(dump.log, expectedAliceSendsOnlyUseRightLog); + t.end(); +}); + +const expectedGalleryRevokesLog = [ + '=> setup called', + 'starting galleryRevokes', + 'starting testGalleryRevokes', + '++ alice.doTapFaucetAndStore starting', + '++ alice.checkAfterRevoked starting', + 'amount quantity should be an array of length 0: 0', + 'successfully threw Error: no use rights present', +]; + +test('run gallery demo galleryRevokes with SES', async t => { + const dump = await main(true, 'demo/gallery', ['galleryRevokes']); + t.deepEquals(dump.log, expectedGalleryRevokesLog); + t.end(); +}); + +test('run gallery demo galleryRevokes without SES', async t => { + const dump = await main(false, 'demo/gallery', ['galleryRevokes']); + t.deepEquals(dump.log, expectedGalleryRevokesLog); + t.end(); +}); diff --git a/test/more/pixels/test-gallery.js b/test/more/pixels/test-gallery.js new file mode 100644 index 00000000000..d70ff1ca0a3 --- /dev/null +++ b/test/more/pixels/test-gallery.js @@ -0,0 +1,252 @@ +import { test } from 'tape-promise/tape'; + +import { makeGallery } from '../../../more/pixels/gallery'; + +import { insistPixelList } from '../../../more/pixels/types/pixelList'; + +test('tapFaucet', t => { + const { userFacet } = makeGallery(); + const { pixelIssuer } = userFacet.getIssuers(); + const pixelPayment = userFacet.tapFaucet(); + const amount = pixelPayment.getBalance(); + const pixelAssay = pixelIssuer.getAssay(); + const quantity = pixelAssay.quantity(amount); + t.doesNotThrow(() => insistPixelList(quantity, userFacet.getCanvasSize())); + t.end(); +}); + +test('get exclusive pixel payment from faucet', t => { + const { userFacet } = makeGallery(); + const payment = userFacet.tapFaucet(); + const { pixelIssuer } = userFacet.getIssuers(); + pixelIssuer.getExclusiveAll(payment).then(pixelPayment => { + const amount = pixelPayment.getBalance(); + const pixelAssay = pixelIssuer.getAssay(); + const quantity = pixelAssay.quantity(amount); + t.doesNotThrow(() => insistPixelList(quantity, userFacet.getCanvasSize())); + t.end(); + }); +}); + +test('the user changes the color of a pixel', async t => { + // setup + const { userFacet } = makeGallery(); + const { pixelIssuer, useRightIssuer } = userFacet.getIssuers(); + + // user actions + const pixelPayment = userFacet.tapFaucet(); + + const exclusivePixelPayment = await pixelIssuer.getExclusiveAll(pixelPayment); + const { useRightPayment } = await userFacet.transformToTransferAndUse( + exclusivePixelPayment, + ); + const exclusiveUseRightPayment = await useRightIssuer.getExclusiveAll( + useRightPayment, + ); + const useRightAssay = exclusiveUseRightPayment.getIssuer().getAssay(); + + const rawPixel = useRightAssay.quantity( + exclusiveUseRightPayment.getBalance(), + )[0]; + await userFacet.changeColor(exclusiveUseRightPayment, '#000000'); + t.equal(userFacet.getColor(rawPixel.x, rawPixel.y), '#000000'); + t.end(); +}); + +// The user gives away the right to change the color (but not transfer the right to transfer the color) and guarantees that the right to change the color is exclusive. Even the original user cannot change the color unless they transfer the pixel back to themselves. +test('The user allows someone else to change the color but not the right to transfer the right to change the color', async t => { + // setup + const { userFacet } = makeGallery(); + const { + pixelIssuer, + useRightIssuer, + transferRightIssuer, + } = userFacet.getIssuers(); + + // user actions + const pixelPayment = userFacet.tapFaucet(); + + const exclusivePixelPayment = await pixelIssuer.getExclusiveAll(pixelPayment); + const { + useRightPayment, + transferRightPayment, + } = await userFacet.transformToTransferAndUse(exclusivePixelPayment); + const exclusiveUseRightPayment = await useRightIssuer.getExclusiveAll( + useRightPayment, + ); + const exclusiveTransferRightPayment = await transferRightIssuer.getExclusiveAll( + transferRightPayment, + ); + const useRightAssay = exclusiveUseRightPayment.getIssuer().getAssay(); + + // first user stores the pixel location info + const rawPixel = useRightAssay.quantity( + exclusiveUseRightPayment.getBalance(), + )[0]; + + // TODO: send to other vat + // other user below + // other user puts the useRight in their purse, changes color + const otherUserPurse = useRightIssuer.makeEmptyPurse(); + const otherUserExclusiveUseRightPayment = await useRightIssuer.getExclusiveAll( + exclusiveUseRightPayment, + ); + await otherUserPurse.depositAll(otherUserExclusiveUseRightPayment); + + const payment = await otherUserPurse.withdrawAll(); + + const exclusivePayment = await useRightIssuer.getExclusiveAll(payment); + + await userFacet.changeColor(exclusivePayment, '#00000'); + + t.equal(userFacet.getColor(rawPixel.x, rawPixel.y), '#00000'); + + // original user transforms the transfer right into a pixel to get + // the color right back + const pixelPayment2 = await userFacet.transformToPixel( + exclusiveTransferRightPayment, + ); + const exclusivePixelPayment2 = await pixelIssuer.getExclusiveAll( + pixelPayment2, + ); + const { + useRightPayment: useRightPayment2, + } = await userFacet.transformToTransferAndUse(exclusivePixelPayment2); + const exclusiveUseRightPayment2 = await useRightIssuer.getExclusiveAll( + useRightPayment2, + ); + await userFacet.changeColor(exclusiveUseRightPayment2, '#FFFFFF'); + t.equal(userFacet.getColor(rawPixel.x, rawPixel.y), '#FFFFFF'); + + // other user cannot color + t.rejects(userFacet.changeColor(exclusivePayment, '#00000')); + t.end(); +}); + +test('The user gives away their right to the pixel (right to transfer color rights) permanently', async t => { + // setup + const { userFacet } = makeGallery(); + const { pixelIssuer, useRightIssuer } = userFacet.getIssuers(); + const useRightAssay = useRightIssuer.getAssay(); + + const pixelPurse = pixelIssuer.makeEmptyPurse(); + + // user actions + const pixelPayment = userFacet.tapFaucet(); + const pixelAssay = pixelIssuer.getAssay(); + const rawPixel = pixelAssay.quantity(pixelPayment.getBalance())[0]; + const exclusivePixelPayment = await pixelIssuer.getExclusiveAll(pixelPayment); + await pixelPurse.depositAll(exclusivePixelPayment); + + const newPayment = await pixelPurse.withdrawAll(); + + // TODO: send over vat to other user + + const exclPaymentNewUser = await pixelIssuer.getExclusiveAll(newPayment); + + const { useRightPayment } = await userFacet.transformToTransferAndUse( + exclPaymentNewUser, + ); + const exclusiveUseRightPayment = await useRightIssuer.getExclusiveAll( + useRightPayment, + ); + + await userFacet.changeColor(exclusiveUseRightPayment, '#00000'); + + t.equal(userFacet.getColor(rawPixel.x, rawPixel.y), '#00000'); + + const { + useRightPayment: useRightPayment2, + } = await userFacet.transformToTransferAndUse(newPayment); + const amount = useRightPayment2.getBalance(); + t.true(useRightAssay.isEmpty(amount)); + t.end(); +}); + +test('The Gallery revokes the right to transfer the pixel or color with it', async t => { + // setup + const { userFacet, adminFacet } = makeGallery(); + const { + pixelIssuer, + useRightIssuer, + transferRightIssuer, + } = userFacet.getIssuers(); + + // user actions + const pixelPayment = userFacet.tapFaucet(); + const pixelAssay = pixelIssuer.getAssay(); + const rawPixel = pixelAssay.quantity(pixelPayment.getBalance())[0]; + const originalColor = userFacet.getColor(rawPixel.x, rawPixel.y); + const exclusivePixelPayment = await pixelIssuer.getExclusiveAll(pixelPayment); + + const { + useRightPayment, + transferRightPayment, + } = await userFacet.transformToTransferAndUse(exclusivePixelPayment); + const exclusiveUseRightPayment = await useRightIssuer.getExclusiveAll( + useRightPayment, + ); + const exclusiveTransferRightPayment = await transferRightIssuer.getExclusiveAll( + transferRightPayment, + ); + + // Gallery revokes + adminFacet.revokePixel(rawPixel); + + // TODO: send over vat to other user + + t.rejects(userFacet.changeColor(exclusiveUseRightPayment, '#00000')); + // other user tries to get exclusive on the transfer right that was sent to + // them. + const otherUserTransferRightPayment = await transferRightIssuer.getExclusiveAll( + exclusiveTransferRightPayment, + ); + // this doesn't error but is empty + const balance = otherUserTransferRightPayment.getBalance(); + + t.deepEqual(balance.quantity, []); + t.strictEqual(userFacet.getColor(rawPixel.x, rawPixel.y), originalColor); + + t.end(); +}); + +test('getDistance', t => { + const { adminFacet } = makeGallery(); + const { getDistance } = adminFacet; + t.strictEqual(getDistance({ x: 0, y: 1 }, { x: 0, y: 1 }), 0); + t.strictEqual(getDistance({ x: 2, y: 1 }, { x: 0, y: 1 }), 2); + t.strictEqual(getDistance({ x: 2, y: 3 }, { x: 0, y: 1 }), 2); + t.strictEqual(getDistance({ x: 0, y: 1 }, { x: 4, y: 1 }), 4); + t.strictEqual(getDistance({ x: 2, y: 2 }, { x: 0, y: 7 }), 5); + t.end(); +}); + +test('getDistanceFromCenter', t => { + const { adminFacet } = makeGallery(); + // default canvasSize is 10 + const { getDistanceFromCenter } = adminFacet; + t.strictEqual(getDistanceFromCenter({ x: 0, y: 1 }), 6); + t.strictEqual(getDistanceFromCenter({ x: 2, y: 1 }), 5); + t.strictEqual(getDistanceFromCenter({ x: 2, y: 3 }), 3); + t.strictEqual(getDistanceFromCenter({ x: 4, y: 1 }), 4); + t.strictEqual(getDistanceFromCenter({ x: 0, y: 7 }), 5); + t.strictEqual(getDistanceFromCenter({ x: 5, y: 5 }), 0); + t.end(); +}); + +test('pricePixel', t => { + const { adminFacet, userFacet } = makeGallery(); + // default canvasSize is 10 + const { pricePixel } = adminFacet; + const { getIssuers } = userFacet; + const { dustIssuer } = getIssuers(); + const dustAssay = dustIssuer.getAssay(); + + t.deepEqual(pricePixel({ x: 0, y: 1 }), dustAssay.make(4)); + t.deepEqual(pricePixel({ x: 2, y: 1 }), dustAssay.make(5)); + t.deepEqual(pricePixel({ x: 2, y: 3 }), dustAssay.make(7)); + t.deepEqual(pricePixel({ x: 4, y: 1 }), dustAssay.make(6)); + t.deepEqual(pricePixel({ x: 0, y: 7 }), dustAssay.make(5)); + t.deepEqual(pricePixel({ x: 5, y: 5 }), dustAssay.make(10)); + t.end(); +});