diff --git a/agoric/contract/package.json b/agoric/contract/package.json index 6b06e17ab..a49301559 100644 --- a/agoric/contract/package.json +++ b/agoric/contract/package.json @@ -16,6 +16,8 @@ "devDependencies": { "@agoric/eslint-config": "agoric-upgrade-11", "@endo/eslint-plugin": "^0.4.4", + "@agoric/swingset-vat": "^0.32.2", + "@endo/promise-kit": "^0.2.59", "@jessie.js/eslint-plugin": "^0.4.0", "ava": "^4.3.1", "eslint": "^8.47.0", diff --git a/agoric/contract/src/kreadV1/README.md b/agoric/contract/src/kreadV1/README.md new file mode 100644 index 000000000..9a33799dc --- /dev/null +++ b/agoric/contract/src/kreadV1/README.md @@ -0,0 +1,164 @@ +# KREAd contract + +# index.js +Contains initialization of `kreadKit` with correct data such as: +* Paths used for storage nodes of data to be availabe through RPC +* Creation of asset mint capabilities (`characters` and `items`) +* creation of `storageNode` access +* Creation of ratios based on provided `terms` + +And eventually exposes the `creatorFacet` and `publicFacet` + +# kreadKit.js +The brunt of all KREAd functionality lives here, this utilises the capabilities granted by `index.js` to provide the end-user the ability interact with the contract and the storage-proposal to create and initialize it correctly. + +The kit consists of initialization and 6 facets: `character`, `item`, `market`, `helper`, `public` and the `creator` + +## init +setup all durable storage for the data required by the contract, this consists of the following: +* `characters`: all characters that have been minted by end-users +* `baseCharacters`: all currently available (not been minted) `baseCharacters` +* `items`: all items that have been minted by the artist through publishing an item collection AND the items assigned (and minted) on character mint +* `baseItems`: `baseItems` the items that can be assigned (and minted) on character mint +* `characterMarket`: all characters that have currently been put up for sale by end-users +* `itemMarket`: all items that are currenctly for sale, either first-sales by the artist or secondary sales by end-users + +It also holds metric data more ephemeral, reset on contract upgrade and to be revived through the proposal. + + +## helper +The helper facet exposes functions that are shared between all facets listed here. + +## character +This contains all functions related to characters and the management of them, it contains the following helper functions: +* `calculateLevel` +* `validateInventoryState`: ensures that the inventory state is no breaking any rules (does not contain multiple items with the same category, this automatically limits the size of the inventory to 10 as these categories are the only ones allowed) +* `isNameUnique` +* `getRandomBaseIndex`: gets a random number based on the indexes left in the `baseCharacter` storage +* `makeInventoryRecorderKit` + +and the following functions that are exposed to the end-user through the public facet: +* `mint`: mints a new character NFT with the name provided by the end-users through offer-args, it also requires a `30000000 uist` give in `Price` in the proposal as a mint fee. This character mint will do the following things on contract: + * pick a random base index to retrieve from the `baseCharacter` storage to assign to this NFT (it is removed after) + * create an empty seat to represent the inventory + * mint 2 character NFT keys, to be used as access to this characters specific inventory. 1 is sent to the user 1 is sent to the inventory seat + * create a storage node path (`inventoryRecorderKit`) based on the name of the NFT (the name is sanitized using a regex + string length of max `20`) + * mint the starting items to the character using the `item` facet `mint` function + * distribute the payed mint fee to the royalty and platform addresses and payout the NFT + * update durable storage with: `character` entry, update collection (`market`) metrics + * write the inventory update to the inventory Path + * write the character update to the character vstorage +* `equip`: takes a `characterKey` + an `item` in give and a `characterKey` in want. equip will do the following things on contract: + * ensure character exists and the correct keys are being swapped + * adds the item to the current allocation, tests whether this allocation is a valid state, if not errors out otherwise it continues + * does a reallocation of funds to the correct seats + * updates the inventory recorder with the updated inventory +* `unequip`: takes a `characterKey` in give and an `item` + a `characterKey` in want. unequip will do the following things on contract: + * ensure character exists and the correct keys are being swapped + * does a reallocation of funds to the correct seats + * updates the inventory recorder with the updated inventory +* `swap`: takes a `characterKey` + `item` in give and an `item` + a `characterKey` in want + * ensure character exists and the correct keys are being swapped + * changes the current allocation based on the items being swapped around, tests whether this allocation is a valid state, if not errors out otherwise it continues + * does a reallocation of funds to the correct seats + * updates the inventory recorder with the updated inventory +* `unequipAll`: takes a `characterKey` in give and all `items` + a `characterKey` in want. unequipAll will do the following things on contract: + * ensure character exists and the correct keys are being swapped + * does a reallocation of all items to the user seat + further reallocation to the correct seats + * updates the inventory recorder with the updated inventory + +## item +This contains all functions related to items and the management of them, it contains the following creatorFacet functions: +* `initializeBaseItems` +* `mintBatch`: mints a batch of items defined as an array of `item objects` + `supply` to the seat that made called invitation. Mintbatch will do the following on contract: + * create amounts for provided items + * creates SFTs for each item, supply being provided by array of objects + * all minted items are added as entries in the durable storage `items` + * all item updates are written to the `items` vstorage path + * market metrics are updated based on what was minted + +and the following functions that are used by other assets through the public facet: +* `mintDefaultBatch`: mints the 3 `baseItems` that are given to each character. This mintDefaultBatch will do the following things on contract: + * pick a random common item from the common bases + * pick a second random common item from the common bases, but filter on category (we do not want to mint multiple of the same categories to a character as that invalidates the inventory) + * pick a third item, this time from the legendary bases and filter out both categories picked already. + * mint these items to the inventory seat + * all minted items are added as entries in the durable storage `items` + * all item updates are written to the `items` vstorage path + * market metrics are updated based on what was minted + +## market +This contains all functions related to marketplace and the management of them. + +It contains the following helper functions: +* `handleExitCharacter` and `handleExitItem`: these are long living promises that listen to the exit status of marketplace listings, this allows the end-users to call exit offer from anywhere (wallet-ui etc..). This removes the entry from durable storage and updates the market storage node with the updated list of things for sale. +* `updateMetrics`: uses util functions in `market-metrics.js` to calculate and update metrics + +It contains the following creatorFacet functions: +* `publishItemCollection`: mints a batch of items defined as an array of `item objects` + `supply` and the `salePrice` to the seat that called invitation. Mintbatch will do the following on contract: + * mints the items + * defines the fees (royalty and platform) + * creates marketplace entries and lists them as `isFirstSale = true` + * all minted items are added as entries in the durable storage `market-items` + * all marketplace updates are written to the `market-items` vstorage path updating the currently for sale items + * market metrics are updated based on what was put on sale + +and the following functions that are exposed to the end-user through the public facet: +* `sellItem`: puts an item on the marketplace based on the `item` and price in give. This sellItem will do the following things on contract: + * ensure brand is the defined paymentbrand we set + * calculate the fees required when buying the item + * create a marketplace entry and add it to the market durable storage + * update the market recorder with the updated list of items for sale + * update the metrics based on the item put for sale + * start an exit subscriber for the item +* `sellCharacter`: puts a character on the marketplace based on the `character` and price in give. This sellCharacter does the same as `sellItem` to the contract, only changin what storage node and durable storage to update. +* `buyItem`: attempts to buy an item listed on the marketplace based on the entryId in the offer args + price in give and `item` in want. This buyItem will do the following things on contract: + * ensure the sell record exists + * define whether it is first or secondary sale and use the correct function + * `buyFirstSale` and `buySecondarySale` differences: Seat does not exit on first sale, fees are different on first sale + * ensure the give price is higher than the price listed + the fees + * ensure the want item is the correct item + * reallocate funds + * remove marketplace ntry from market durable storage + * update the market recorder with the updated list of items for sale + * update the metrics based on the item just bought +* `buyCharacter`: attempts to buy a character on the marketplace based on the `character` in want and the price in give. This buyCharacter does the same as `sellItem` to the contract however, it only consists of secondary sale objects and it changes what storage node and durable storage to update. + +## public +This contains wrappers for all functions that are needed for end-users to create their invitations from the UI. + +It contains the following helper functions for tests: + * `getCharacters`: + * `getCharacterInventory`: + * `getCharactersForSale`: + * `getItemsForSale`: + * `getMarketMetrics`: + * `getCharacterLevel`: + +It contains the following wrapped functions for end-users: + * `makeMintCharacterInvitation`: + * `makeMintItemInvitation`: + * `makeEquipInvitation`: + * `makeUnequipInvitation`: + * `makeUnequipAllInvitation`: + * `makeItemSwapInvitation`: + * `makeSellCharacterInvitation`: + * `makeBuyCharacterInvitation`: + * `makeSellItemInvitation`: + * `makeBuyItemInvitation`: + + +## creator +This contains wrappers for all functions that are needed to setup the contract correctly and ensure it can be upgraded/restarted correctly. + +It contains the following helper functions for tests: + * `makeMintItemInvitation`: + + +It contains the following wrapped functions for contract start and governance: + * `initializeMetrics`: initializes the metrics with a base value + * `reviveMarketExitSubscribers`: loops over all marketplace entries in durable storage and calls the function to start the exit subscriber + * `initializeBaseAssets`: provides the `baseCharacters` and `baseAssets`. This will do the following things on contract: + * add the list of base characters to durable storage with a numbered key + * add the base items to the corresponding rarity list and adds this to durable storage + * `makePublishItemCollectionInvitation`: publishes a new item collection diff --git a/agoric/contract/src/errors.js b/agoric/contract/src/kreadV1/errors.js similarity index 100% rename from agoric/contract/src/errors.js rename to agoric/contract/src/kreadV1/errors.js diff --git a/agoric/contract/src/index.js b/agoric/contract/src/kreadV1/index.js similarity index 98% rename from agoric/contract/src/index.js rename to agoric/contract/src/kreadV1/index.js index 0855e1fc7..b604ce5fd 100644 --- a/agoric/contract/src/index.js +++ b/agoric/contract/src/kreadV1/index.js @@ -72,7 +72,7 @@ harden(meta); * * @param {Baggage} baggage */ -export const start = async (zcf, privateArgs, baggage) => { +export const prepare = async (zcf, privateArgs, baggage) => { const terms = zcf.getTerms(); // TODO: move to proposal @@ -177,4 +177,4 @@ export const start = async (zcf, privateArgs, baggage) => { }); }; -harden(start); +harden(prepare); diff --git a/agoric/contract/src/kreadCommitteeCharter.js b/agoric/contract/src/kreadV1/kreadCommitteeCharter.js similarity index 100% rename from agoric/contract/src/kreadCommitteeCharter.js rename to agoric/contract/src/kreadV1/kreadCommitteeCharter.js diff --git a/agoric/contract/src/kreadKit.js b/agoric/contract/src/kreadV1/kreadKit.js similarity index 100% rename from agoric/contract/src/kreadKit.js rename to agoric/contract/src/kreadV1/kreadKit.js diff --git a/agoric/contract/src/market-metrics.js b/agoric/contract/src/kreadV1/market-metrics.js similarity index 100% rename from agoric/contract/src/market-metrics.js rename to agoric/contract/src/kreadV1/market-metrics.js diff --git a/agoric/contract/src/text.js b/agoric/contract/src/kreadV1/text.js similarity index 100% rename from agoric/contract/src/text.js rename to agoric/contract/src/kreadV1/text.js diff --git a/agoric/contract/src/type-guards.js b/agoric/contract/src/kreadV1/type-guards.js similarity index 100% rename from agoric/contract/src/type-guards.js rename to agoric/contract/src/kreadV1/type-guards.js diff --git a/agoric/contract/src/types.js b/agoric/contract/src/kreadV1/types.js similarity index 100% rename from agoric/contract/src/types.js rename to agoric/contract/src/kreadV1/types.js diff --git a/agoric/contract/src/utils.js b/agoric/contract/src/kreadV1/utils.js similarity index 98% rename from agoric/contract/src/utils.js rename to agoric/contract/src/kreadV1/utils.js index ecaf2fff0..2887bfc2c 100644 --- a/agoric/contract/src/utils.js +++ b/agoric/contract/src/kreadV1/utils.js @@ -48,7 +48,7 @@ export const provideRecorderKits = async ( paths, typeMatchers, ) => { - console.log('provideRecorderKits', paths, typeMatchers); + // console.log('provideRecorderKits', paths, typeMatchers); const keys = Object.keys(paths); // assume if any keys are defined they all are const inBaggage = baggage.has(keys[0]); diff --git a/agoric/contract/src/kreadV2/errors.js b/agoric/contract/src/kreadV2/errors.js new file mode 100644 index 000000000..9fb323040 --- /dev/null +++ b/agoric/contract/src/kreadV2/errors.js @@ -0,0 +1,34 @@ +export const errors = { + noConfig: `Configuration not found, use creatorFacet.initConfig() to enable this method`, + noNameArg: `Name argument required`, + allMinted: `All characters have been minted`, + invalidName: `Invalid name. String should only contain ASCII alphanumerics, underscores, and/or dashes.`, + mintFeeTooLow: `Provided mint fee is too low`, + unkwonwnArgInMintOffer: `Mint Character's offer "want" must only contain property "name"`, + noWantInOffer: `Offer must include "want" terms in the form of { want: { name: }}`, + nameTaken: (name) => + `Name ${name} is already in use, please select a different name`, + depositToSeatFailed: `Could not deposit nft into Seat`, + depositToFacetFailed: `Could not deposit nft into userFacet`, + character404: `Character not found`, + inventory404: `Character inventory not found`, + notifier404: `Character inventory notifier not found`, + updateMarketError: `There was a problem updating the market`, + privateState404: `Character private state not found`, + noKeyInInventory: `Could not find character key in inventory`, + invalidInventoryKey: `Brand of Inventory Key does not match the correct Issuer`, + inventoryKeyMismatch: `Wanted Key and Inventory Key do not match`, + noItemsRequested: `Offer missing requested item`, + duplicateCategoryInInventory: `Inventory cannot contain multiple items of the same category`, + seedInvalid: `Seed must be a number`, + itemNotInMarket: `Could not find Item in market`, + characterNotInMarket: `Could not find Character in market`, + invalidArg: `Invalid Argument`, + missingStorageNode: `Missing Storage Node, notifications are not enabled`, + sellerSeatMismatch: `Wanted Item amount does not match item in sellerSeat`, + insufficientFunds: `Provided payment is lower than the asking price for this Item`, + itemNotFound: (item) => `Couldn't find item record for ${item}`, + incorrectPaymentBrand: (paymentBrand) => + `Incorrect payment brand. Please use ${paymentBrand}`, + rearrangeError: 'Reallocating assets between seats failed' +}; diff --git a/agoric/contract/src/kreadV2/index.js b/agoric/contract/src/kreadV2/index.js new file mode 100644 index 000000000..1bcb3e26b --- /dev/null +++ b/agoric/contract/src/kreadV2/index.js @@ -0,0 +1,181 @@ +/* eslint-disable no-undef */ +// @ts-check +import '@agoric/zoe/exported.js'; + +import { AmountMath, AssetKind } from '@agoric/ertp'; +import { M } from '@agoric/store'; +import { provideAll } from '@agoric/zoe/src/contractSupport/durability.js'; +import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/recorder.js'; +import { makeRatio } from '@agoric/zoe/src/contractSupport/ratio.js'; +import { InvitationShape } from '@agoric/zoe/src/typeGuards.js'; +import { handleParamGovernance } from '@agoric/governance'; + +import { prepareKreadKit, provideKreadKitRecorderKits } from './kreadKit.js'; +import { provide } from '@agoric/vat-data'; +import { provideRecorderKits } from './utils.js'; + +/** + * This contract handles the mint of KREAd characters, + * along with its corresponding item inventories and keys. + * It also allows for equiping and unequiping items to + * and from the inventory, using a token as access + */ + +/** @typedef {import('@agoric/vat-data').Baggage} Baggage */ +/** @typedef {import('@agoric/time/src/types').Clock} Clock */ +/** @typedef {import('./type-guards.js').RatioObject} RatioObject */ + +/** @type {ContractMeta} */ +export const meta = { + privateArgsShape: M.splitRecord({ + initialPoserInvitation: InvitationShape, + seed: M.number(), + clock: M.eref(M.remotable('Clock')), + powers: { + storageNode: M.remotable('StorageNode'), + marshaller: M.remotable('Marshaller'), + }, + }), + customTermsShape: M.splitRecord({ + royaltyRate: { + numerator: M.lte(100n), + denominator: M.eq(100n), + }, + platformFeeRate: { + numerator: M.lte(100n), + denominator: M.eq(100n), + }, + mintFee: M.nat(), + mintRoyaltyRate: { + numerator: M.lte(100n), + denominator: M.eq(100n), + }, + mintPlatformFeeRate: { + numerator: M.lte(100n), + denominator: M.eq(100n), + }, + royaltyDepositFacet: M.any(), + platformFeeDepositFacet: M.any(), + assetNames: M.splitRecord({ character: M.string(), item: M.string() }), + }), +}; +harden(meta); + +/** + * @param {ZCF>} zcf + * @param {{ + * seed: number + * powers: { storageNode: StorageNode, marshaller: Marshaller }, + * clock: Clock + * defaultCharacters: object[], + * defaultItems: object[], + * initialPoserInvitation: Invitation + * }} privateArgs + * + * @param {Baggage} baggage + */ +export const prepare = async (zcf, privateArgs, baggage) => { + const terms = zcf.getTerms(); + + // TODO: move to proposal + const assetNames = terms.assetNames; + + // Setting up the mint capabilities here in the prepare function, as discussed with Turadg + // durability is not a concern with these, and defining them here, passing on what's needed + // ensures that the capabilities are where they need to be + const { characterMint, itemMint } = await provideAll(baggage, { + characterMint: () => + zcf.makeZCFMint(assetNames.character, AssetKind.COPY_BAG), + itemMint: () => zcf.makeZCFMint(assetNames.item, AssetKind.COPY_BAG), + }); + + const characterIssuerRecord = characterMint.getIssuerRecord(); + const itemIssuerRecord = itemMint.getIssuerRecord(); + + const { powers, clock, seed } = privateArgs; + + const { + mintFee, + royaltyRate, + platformFeeRate, + mintRoyaltyRate, + mintPlatformFeeRate, + royaltyDepositFacet, + platformFeeDepositFacet, + minUncommonRating, + brands: { Money: paymentBrand }, + } = terms; + + const { makeRecorderKit } = prepareRecorderKitMakers( + baggage, + powers.marshaller, + ); + + const recorderKits = await provideKreadKitRecorderKits( + baggage, + powers.storageNode, + makeRecorderKit, + ); + + assert(paymentBrand, 'missing paymentBrand'); + const mintFeeAmount = AmountMath.make(paymentBrand, mintFee); + + const objectToRatio = (brand, { numerator, denominator }) => { + return makeRatio(numerator, brand, denominator, brand); + }; + const mintRoyaltyRateRatio = objectToRatio(paymentBrand, mintRoyaltyRate); + const mintPlatformFeeRatio = objectToRatio(paymentBrand, mintPlatformFeeRate); + const royaltyRateRatio = objectToRatio(paymentBrand, royaltyRate); + const platformFeeRatio = objectToRatio(paymentBrand, platformFeeRate); + + const makeKreadKit = prepareKreadKit( + baggage, + zcf, + { + seed, + mintFeeAmount, + royaltyRate: royaltyRateRatio, + platformFeeRate: platformFeeRatio, + mintRoyaltyRate: mintRoyaltyRateRatio, + mintPlatformFeeRate: mintPlatformFeeRatio, + royaltyDepositFacet, + platformFeeDepositFacet, + paymentBrand, + minUncommonRating, + }, + harden({ + recorderKits, + characterMint, + characterIssuerRecord, + itemIssuerRecord, + itemMint, + clock, + storageNode: powers.storageNode, + makeRecorderKit, + }), + ); + + const kreadKit = provide(baggage, 'kitSingleton', () => makeKreadKit()); + + const { makeDurableGovernorFacet } = handleParamGovernance( + zcf, + privateArgs.initialPoserInvitation, + {}, + ); + + const { governorFacet } = makeDurableGovernorFacet( + baggage, + kreadKit.creator, + { + publishItemCollection: (price, itemsToSell) => + kreadKit.creator.publishItemCollection(price, itemsToSell), + }, + ); + return harden({ + creatorFacet: governorFacet, + // no governed parameters, so no need to augment. + publicFacet: kreadKit.public, + }); +}; + +harden(prepare); diff --git a/agoric/contract/src/kreadV2/kreadCommitteeCharter.js b/agoric/contract/src/kreadV2/kreadCommitteeCharter.js new file mode 100644 index 000000000..14ce22e31 --- /dev/null +++ b/agoric/contract/src/kreadV2/kreadCommitteeCharter.js @@ -0,0 +1,145 @@ +// @jessie-check + +import { M } from '@agoric/store'; +import { TimestampShape } from '@agoric/time'; +import { prepareExo, provideDurableMapStore } from '@agoric/vat-data'; +import '@agoric/zoe/exported.js'; +import { + InstallationShape, + InstanceHandleShape, +} from '@agoric/zoe/src/typeGuards.js'; +import { E } from '@endo/far'; + +import '@agoric/governance/exported.js'; +import '@agoric/zoe/src/contracts/exported.js'; + +/** + * @file This contract makes it possible for those who govern the KREAd contract + * to call for votes to pause offers. + */ + +export const INVITATION_MAKERS_DESC = 'charter member invitation'; + +/** @type {ContractMeta} */ +export const meta = { + customTermsShape: { + binaryVoteCounterInstallation: InstallationShape, + }, + upgradability: 'canUpgrade', +}; +harden(meta); + +/** + * @param {ZCF<{ binaryVoteCounterInstallation: Installation }>} zcf + * @param {undefined} privateArgs + * @param {import('@agoric/vat-data').Baggage} baggage + */ +export const start = async (zcf, privateArgs, baggage) => { + const { binaryVoteCounterInstallation: counter } = zcf.getTerms(); + /** @type {MapStore>} */ + const instanceToGovernor = provideDurableMapStore( + baggage, + 'instanceToGovernor', + ); + + const makeOfferFilterInvitation = (instance, strings, deadline) => { + const voteOnOfferFilterHandler = (seat) => { + seat.exit(); + + const governor = instanceToGovernor.get(instance); + return E(governor).voteOnOfferFilter(counter, deadline, strings); + }; + + return zcf.makeInvitation(voteOnOfferFilterHandler, 'vote on offer filter'); + }; + + /** + * @param {Instance} instance + * @param {string} methodName + * @param {string[]} methodArgs + * @param {import('@agoric/time').TimestampValue} deadline + */ + const makeApiInvocationInvitation = ( + instance, + methodName, + methodArgs, + deadline, + ) => { + const handler = (seat) => { + seat.exit(); + + const governor = instanceToGovernor.get(instance); + return E(governor).voteOnApiInvocation( + methodName, + methodArgs, + counter, + deadline, + ); + }; + return zcf.makeInvitation(handler, 'vote on API invocation'); + }; + + const MakerI = M.interface('Charter InvitationMakers', { + VoteOnPauseOffers: M.call( + InstanceHandleShape, + M.arrayOf(M.string()), + TimestampShape, + ).returns(M.promise()), + VoteOnApiCall: M.call( + InstanceHandleShape, + M.string(), + M.arrayOf(M.any()), + TimestampShape, + ).returns(M.promise()), + }); + + // durable so that when this contract is upgraded this ocap held + // by committee members (from their invitations) stay capable + const invitationMakers = prepareExo( + baggage, + 'Charter Invitation Makers', + MakerI, + { + VoteOnPauseOffers: makeOfferFilterInvitation, + VoteOnApiCall: makeApiInvocationInvitation, + }, + ); + + const charterMemberHandler = (seat) => { + seat.exit(); + return harden({ invitationMakers }); + }; + + const CharterCreatorI = M.interface('Charter creatorFacet', { + addInstance: M.call(InstanceHandleShape, M.any()) + .optional(M.string()) + .returns(), + makeCharterMemberInvitation: M.call().returns(M.promise()), + }); + + const creatorFacet = prepareExo( + baggage, + 'Charter creatorFacet', + CharterCreatorI, + { + /** + * @param {Instance} governedInstance + * @param {GovernorCreatorFacet} governorFacet + * @param {string} [label] for diagnostic use only + */ + addInstance: (governedInstance, governorFacet, label) => { + console.log('charter: adding instance', label); + instanceToGovernor.init(governedInstance, governorFacet); + }, + makeCharterMemberInvitation: () => + zcf.makeInvitation(charterMemberHandler, INVITATION_MAKERS_DESC), + }, + ); + + return harden({ creatorFacet }); +}; +harden(start); + +/** + * @typedef {import('@agoric/zoe/src/zoeService/utils.js').StartedInstanceKit} KreadCharterStartResult + */ diff --git a/agoric/contract/src/kreadV2/kreadKit.js b/agoric/contract/src/kreadV2/kreadKit.js new file mode 100644 index 000000000..bc4e8acaa --- /dev/null +++ b/agoric/contract/src/kreadV2/kreadKit.js @@ -0,0 +1,1832 @@ +/* eslint-disable no-bitwise */ +/* eslint-disable no-undef */ +// @ts-check +import { updateCollectionMetrics } from './market-metrics.js'; +import { assert, details as X } from '@agoric/assert'; +import { AmountMath, BrandShape } from '@agoric/ertp'; +import { prepareExoClassKit, M } from '@agoric/vat-data'; +import { makeDurableZone } from '@agoric/zone/durable.js'; + +import { E } from '@endo/eventual-send'; +import { errors } from './errors.js'; +import { + makeCharacterNftObjs, + makeCopyBagAmountShape, + addAllToMap, + provideRecorderKits, +} from './utils.js'; + +import { text } from './text.js'; +import { makeCopyBag, mustMatch } from '@agoric/store'; +import { + CharacterI, + HelperI, + ItemI, + MarketI, + PublicI, + CreatorI, + CharacterGuardBagShape, + ItemGuard, + ItemGuardBagShape, + ItemRecorderGuard, + MarketRecorderGuard, + MarketMetricsGuard, + RarityGuard, + BaseCharacterGuard, + MarketEntryGuard, + CharacterEntryGuard, + CharacterRecorderGuard, +} from './type-guards.js'; +import { atomicRearrange } from '@agoric/zoe/src/contractSupport/index.js'; +import { multiplyBy } from '@agoric/zoe/src/contractSupport/ratio.js'; + +import '@agoric/zoe/exported.js'; + +/** + * this provides the exoClassKit for our upgradable KREAd contract + * Utilizes capabilities from the prepare function suchs as mints + * timer service and values from the privateArgs + * + * + * @param {import('@agoric/vat-data').Baggage} baggage + * @param {ZCF} zcf + * @param {{ + * seed: number, + * mintFeeAmount: Amount<'nat'>, + * royaltyRate: Ratio, + * platformFeeRate: Ratio, + * mintRoyaltyRate: Ratio, + * mintPlatformFeeRate: Ratio, + * royaltyDepositFacet: DepositFacet, + * platformFeeDepositFacet: DepositFacet, + * paymentBrand: Brand, + * minUncommonRating: number + * }} privateArgs + * @param {{ + * characterIssuerRecord: IssuerRecord<"copyBag"> + * characterMint: ZCFMint<"copyBag"> + * itemIssuerRecord: IssuerRecord<"copyBag"> + * itemMint: ZCFMint<"copyBag"> + * clock: import('@agoric/time/src/types.js').Clock + * makeRecorderKit: import('@agoric/zoe/src/contractSupport').MakeRecorderKit, + * recorderKits: { + * characterKit: import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit; + * itemKit: import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit; + * marketCharacterKit: import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit; + * marketItemKit: import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit; + * marketCharacterMetricsKit: import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit; + * marketItemMetricsKit: import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit; + * }; + * }} powers + */ +export const prepareKreadKit = ( + baggage, + zcf, + { + seed, + mintFeeAmount, + royaltyRate, + platformFeeRate, + mintRoyaltyRate, + mintPlatformFeeRate, + royaltyDepositFacet, + platformFeeDepositFacet, + paymentBrand, + minUncommonRating, + }, + { + characterIssuerRecord, + characterMint, + itemIssuerRecord, + itemMint, + clock, + makeRecorderKit, + recorderKits: { + characterKit, + itemKit, + marketCharacterKit, + marketCharacterMetricsKit, + marketItemKit, + marketItemMetricsKit, + }, + }, +) => { + const { brand: characterBrand } = characterIssuerRecord; + const { brand: itemBrand } = itemIssuerRecord; + + const marketItemNode = marketItemKit.recorder.getStorageNode(); + const marketCharacterNode = marketCharacterKit.recorder.getStorageNode(); + + const characterNode = characterKit.recorder.getStorageNode(); + + const characterShape = makeCopyBagAmountShape( + characterBrand, + CharacterGuardBagShape, + ); + const itemShape = makeCopyBagAmountShape(itemBrand, ItemGuardBagShape); + + return prepareExoClassKit( + baggage, + 'KreadKit', + { + helper: HelperI, + character: CharacterI, + item: ItemI, + market: MarketI, + public: PublicI, + creator: CreatorI, + }, + () => { + const zone = makeDurableZone(baggage); + return { + character: harden({ + entries: zone.mapStore('characters', { + keyShape: M.string(), + valueShape: M.or(CharacterEntryGuard, M.arrayOf(M.string())), + }), + bases: zone.mapStore('baseCharacters', { + keyShape: M.number(), + valueShape: BaseCharacterGuard, + }), + }), + item: harden({ + entries: zone.mapStore('items', { + keyShape: M.number(), + valueShape: ItemRecorderGuard, + }), + bases: zone.mapStore('baseItems', { + keyShape: RarityGuard, + valueShape: M.arrayOf(ItemGuard), + }), + }), + market: harden({ + characterEntries: zone.mapStore('characterMarket', { + keyShape: M.string(), + valueShape: MarketEntryGuard, + }), + itemEntries: zone.mapStore('itemMarket', { + keyShape: M.number(), + valueShape: MarketEntryGuard, + }), + metrics: zone.mapStore('marketMetrics', { + keyShape: M.or('character', 'item'), + valueShape: MarketMetricsGuard, + }), + }), + }; + }, + { + helper: { + async getTimeStamp() { + return E(clock).getCurrentTimestamp(); + }, + randomNumber() { + seed |= 0; + seed = (seed + 0x6d2b79f5) | 0; + let t = Math.imul(seed ^ (seed >>> 15), 1 | seed); + // eslint-disable-next-line operator-assignment + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }, + }, + character: { + calculateLevel(name) { + const character = this.state.character.entries.get(name); + let level = character.character.level; + + const itemLevels = character.inventory + .getAmountAllocated('Item') + .value.payload.map(([value, _supply]) => { + return value.level; + }); + + level = itemLevels.reduce((acc, value) => acc + value, level); + return level; + }, + validateInventoryState(inventoryState) { + const itemTypes = inventoryState.map((item) => item.category); + return itemTypes.length === new Set(itemTypes).size; + }, + isNameUnique(name) { + return !this.state.character.entries.has(name); + }, + getRandomBaseIndex() { + const { helper } = this.facets; + const { character: characterState } = this.state; + const number = Math.floor( + helper.randomNumber() * characterState.bases.getSize(), + ); + return Array.from(characterState.bases.keys())[number]; + }, + initializeBaseCharacters(baseCharacters) { + const { character: characterState } = this.state; + if (characterState.bases.getSize() > 0) return; + addAllToMap(characterState.bases, baseCharacters); + }, + async makeInventoryRecorderKit(path) { + const node = await E(characterNode).makeChildNode( + `inventory-${path}`, + ); + return makeRecorderKit(node, M.arrayOf([ItemGuard, M.nat()])); + }, + mint() { + const handler = async (seat, offerArgs) => { + const { + helper, + character: characterFacet, + item, + market: marketFacet, + } = this.facets; + + const { character: characterState } = this.state; + + const { give } = seat.getProposal(); + mustMatch( + offerArgs, + M.splitRecord({ + name: M.string(harden({ stringLengthLimit: 20 })), + }), + 'offerArgs', + ); + + const newCharacterName = offerArgs.name; + + AmountMath.isGTE(give.Price, mintFeeAmount) || + assert.fail(errors.mintFeeTooLow); + + !characterState.entries.get('names').includes(newCharacterName) || + assert.fail(errors.nameTaken(newCharacterName)); + + characterState.bases.getSize() > 0 || + assert.fail(errors.allMinted); + + const re = /^[a-zA-Z0-9_-]*$/; + (re.test(newCharacterName) && newCharacterName !== 'names') || + assert.fail(errors.invalidName); + + const baseIndex = characterFacet.getRandomBaseIndex(); + const baseCharacter = characterState.bases.get(baseIndex); + + characterState.bases.delete(baseIndex); + + characterState.entries.set( + 'names', + harden([ + ...characterState.entries.get('names'), + newCharacterName, + ]), + ); + + // for @jessie.js/safe-await-operator + await null; + try { + const currentTime = await helper.getTimeStamp(); + + const [newCharacterAmount1, newCharacterAmount2] = + makeCharacterNftObjs( + newCharacterName, + baseCharacter, + characterState.entries.getSize(), + currentTime, + ).map((character) => + AmountMath.make( + characterBrand, + makeCopyBag(harden([[character, 1n]])), + ), + ); + + const { zcfSeat, userSeat } = zcf.makeEmptySeatKit(); + + const { zcfSeat: inventorySeat } = zcf.makeEmptySeatKit(); + // Mint character to user seat & inventorySeat + characterMint.mintGains({ Asset: newCharacterAmount1 }, seat); + characterMint.mintGains( + { CharacterKey: newCharacterAmount2 }, + inventorySeat, + ); + + const inventoryKit = + await characterFacet.makeInventoryRecorderKit(newCharacterName); + + await item.mintDefaultBatch(inventorySeat); + + const royaltyFee = multiplyBy(give.Price, mintRoyaltyRate); + const platformFee = multiplyBy(give.Price, mintPlatformFeeRate); + + /** @type {TransferPart[]} */ + const transfers = []; + transfers.push([ + seat, + zcfSeat, + { Price: royaltyFee }, + { Royalty: royaltyFee }, + ]); + transfers.push([ + seat, + zcfSeat, + { Price: platformFee }, + { PlatformFee: platformFee }, + ]); + + try { + atomicRearrange(zcf, harden(transfers)); + } catch (e) { + assert.fail(errors.rearrangeError); + } + + seat.exit(); + zcfSeat.exit(); + + const payouts = await E(userSeat).getPayouts(); + const royaltyPayout = await payouts.Royalty; + const platformFeePayout = await payouts.PlatformFee; + + await E(royaltyDepositFacet).receive(royaltyPayout); + await E(platformFeeDepositFacet).receive(platformFeePayout); + + // Add to state + const character = { + name: newCharacterName, + character: newCharacterAmount1.value.payload[0][0], + inventory: inventorySeat, + inventoryKit, + history: [ + { + type: 'mint', + data: newCharacterAmount1.value[0], + timestamp: currentTime, + }, + ], + }; + + addAllToMap(characterState.entries, [ + [character.name, harden(character)], + ]); + + // update metrics + marketFacet.updateMetrics('character', { + collectionSize: true, + averageLevel: { + type: 'add', + value: character.character.level, + }, + }); + + characterKit.recorder.write( + // write `character` minus `seat` prop + (({ seat: _omitSeat, ...char }) => char)(character), + ); + + // TODO: consider refactoring what we put in the inventory node + inventoryKit.recorder.write( + inventorySeat.getAmountAllocated('Item').value.payload, + ); + + return text.mintCharacterReturn; + } catch (e) { + // restore base char deletion and and name entry + addAllToMap(characterState.bases, [[baseIndex, baseCharacter]]); + characterState.entries.set( + 'names', + harden( + characterState.entries + .get('names') + .filter((name) => name !== newCharacterName), + ), + ); + return e; + } + }; + return zcf.makeInvitation( + handler, + 'mintCharacterNfts', + undefined, + undefined, + ); + }, + equip() { + const handler = (seat) => { + const { character: characterFacet } = this.facets; + const { character: characterState } = this.state; + + // Retrieve Items and Inventory key from user seat + const providedItemAmount = seat.getAmountAllocated('Item'); + const providedCharacterKeyAmount = + seat.getAmountAllocated('CharacterKey1'); + const providedCharacterKey = + providedCharacterKeyAmount.value.payload[0][0]; + const characterName = providedCharacterKey.name; + + // Find characterRecord entry based on provided key + const characterRecord = characterState.entries.get(characterName); + const inventorySeat = characterRecord.inventory; + + const { want } = seat.getProposal(); + const { CharacterKey2: wantedCharacter } = want; + + // Get current Character Key from inventorySeat + const inventoryCharacterKey = + inventorySeat.getAmountAllocated('CharacterKey'); + inventoryCharacterKey || assert.fail(errors.noKeyInInventory); + + AmountMath.isEqual( + wantedCharacter, + inventoryCharacterKey, + characterBrand, + ) || assert.fail(errors.inventoryKeyMismatch); + + // Ensure inventory STATE will be valid before reallocation + let inventory = inventorySeat + .getCurrentAllocation() + .Item.value.payload.map(([value, _supply]) => value); + if (providedItemAmount.value.payload[0]) + inventory = [ + ...inventory, + providedItemAmount.value.payload[0][0], + ]; + + characterFacet.validateInventoryState(inventory) || + assert.fail(errors.duplicateCategoryInInventory); + + /** @type {TransferPart[]} */ + const transfers = []; + transfers.push([seat, inventorySeat, { Item: providedItemAmount }]); + transfers.push([ + seat, + inventorySeat, + { CharacterKey1: providedCharacterKeyAmount }, + { CharacterKey: providedCharacterKeyAmount }, + ]); + transfers.push([ + inventorySeat, + seat, + { CharacterKey: inventoryCharacterKey }, + { CharacterKey2: inventoryCharacterKey }, + ]); + + try { + atomicRearrange(zcf, harden(transfers)); + } catch (e) { + assert.fail(errors.rearrangeError); + } + + characterRecord.inventoryKit.recorder.write( + inventorySeat.getAmountAllocated('Item').value.payload, + ); + + seat.exit(); + + return text.equipReturn; + }; + return zcf.makeInvitation( + handler, + 'addToInventory', + undefined, + M.splitRecord({ + give: { + CharacterKey1: M.splitRecord(characterShape), + Item: M.splitRecord(itemShape), + }, + want: { + CharacterKey2: M.splitRecord(characterShape), + }, + }), + ); + }, + unequip() { + const handler = async (seat) => { + const { character: characterState } = this.state; + + // Retrieve Character key from user seat + const providedCharacterKeyAmount = + seat.getAmountAllocated('CharacterKey1'); + const providedCharacterKey = + providedCharacterKeyAmount.value.payload[0][0]; + const characterName = providedCharacterKey.name; + + // Find character record entry based on provided key + const characterRecord = characterState.entries.get(characterName); + const inventorySeat = characterRecord.inventory; + providedCharacterKey || + assert.fail(errors.invalidCharacterKey); + + // Get reference to the wanted items and key + const { want } = seat.getProposal(); + const { Item: requestedItems, CharacterKey2: wantedCharacter } = + want; + + const inventoryCharacterKey = + inventorySeat.getAmountAllocated('CharacterKey'); + inventoryCharacterKey || assert.fail(errors.noKeyInInventory); + + // Ensure requested key and inventory key match + + AmountMath.isEqual( + wantedCharacter, + inventoryCharacterKey, + characterBrand, + ) || assert.fail(errors.inventoryKeyMismatch); + + /** @type {TransferPart[]} */ + const transfers = []; + transfers.push([inventorySeat, seat, { Item: requestedItems }]); + transfers.push([ + seat, + inventorySeat, + { CharacterKey1: providedCharacterKeyAmount }, + { CharacterKey: providedCharacterKeyAmount }, + ]); + transfers.push([ + inventorySeat, + seat, + { CharacterKey: wantedCharacter }, + { CharacterKey2: wantedCharacter }, + ]); + + try { + atomicRearrange(zcf, harden(transfers)); + } catch (e) { + assert.fail(errors.rearrangeError); + } + + characterRecord.inventoryKit.recorder.write( + inventorySeat.getAmountAllocated('Item').value.payload, + ); + + seat.exit(); + return text.unequipReturn; + }; + + return zcf.makeInvitation( + handler, + 'removeFromInventory', + undefined, + M.splitRecord({ + give: { + CharacterKey1: M.splitRecord(characterShape), + }, + want: { + CharacterKey2: M.splitRecord(characterShape), + Item: M.splitRecord(itemShape), + }, + }), + ); + }, + swap() { + const handler = (seat) => { + const { character: characterFacet } = this.facets; + const { character: characterState } = this.state; + + // Retrieve Items and Inventory key from user seat + const providedItemAmount = seat.getAmountAllocated('Item1'); + const providedCharacterKeyAmount = + seat.getAmountAllocated('CharacterKey1'); + const providedCharacterKey = + providedCharacterKeyAmount.value.payload[0][0]; + // const providedItems = providedItemAmount.value; + const characterName = providedCharacterKey.name; + + // Find character record entry based on provided key + const characterRecord = characterState.entries.get(characterName); + const inventorySeat = characterRecord.inventory; + providedCharacterKey || + assert.fail(errors.invalidCharacterKey); + + const { want } = seat.getProposal(); + const { + CharacterKey2: wantedCharacterAmount, + Item2: wantedItemsAmount, + } = want; + + // Ensure requested key and inventory key match + const inventoryCharacterKey = + inventorySeat.getAmountAllocated('CharacterKey'); + inventoryCharacterKey || assert.fail(errors.noKeyInInventory); + + AmountMath.isEqual( + wantedCharacterAmount, + inventoryCharacterKey, + characterBrand, + ) || assert.fail(errors.inventoryKeyMismatch); + + // Ensure inventory STATE is valid before reallocation + let inventory = inventorySeat + .getCurrentAllocation() + .Item.value.payload.map(([value, _supply]) => value); + + if (wantedItemsAmount.value.payload[0]) + inventory = inventory.filter( + (item) => + item.category !== + wantedItemsAmount.value.payload[0][0].category, + ); + if (providedItemAmount.value.payload[0]) + inventory = [ + ...inventory, + providedItemAmount.value.payload[0][0], + ]; + + characterFacet.validateInventoryState(inventory) || + assert.fail(errors.duplicateCategoryInInventory); + + /** @type {TransferPart[]} */ + const transfers = []; + transfers.push([ + seat, + inventorySeat, + { Item1: providedItemAmount }, + { Item: providedItemAmount }, + ]); + transfers.push([ + inventorySeat, + seat, + { Item: wantedItemsAmount }, + { Item2: wantedItemsAmount }, + ]); + transfers.push([ + seat, + inventorySeat, + { CharacterKey1: providedCharacterKeyAmount }, + { CharacterKey: providedCharacterKeyAmount }, + ]); + transfers.push([ + inventorySeat, + seat, + { CharacterKey: wantedCharacterAmount }, + { CharacterKey2: wantedCharacterAmount }, + ]); + + try { + atomicRearrange(zcf, harden(transfers)); + } catch (e) { + assert.fail(errors.rearrangeError); + } + + characterRecord.inventoryKit.recorder.write( + inventorySeat.getAmountAllocated('Item').value.payload, + ); + seat.exit(); + }; + + return zcf.makeInvitation( + handler, + 'itemInventorySwap', + undefined, + M.splitRecord({ + give: { + CharacterKey1: M.splitRecord(characterShape), + Item1: M.splitRecord(itemShape), + }, + want: M.splitRecord( + { + CharacterKey2: M.splitRecord(characterShape), + }, + { Item2: M.splitRecord(itemShape) }, + ), + }), + ); + }, + unequipAll() { + const handler = (seat) => { + const { character: characterState } = this.state; + + // Retrieve Character key from user seat + const providedCharacterKeyAmount = + seat.getAmountAllocated('CharacterKey1'); + const providedCharacterKey = + providedCharacterKeyAmount.value.payload[0][0]; + const characterName = providedCharacterKey.name; + + // Find character record entry based on provided key + const characterRecord = characterState.entries.get(characterName); + const inventorySeat = characterRecord.inventory; + providedCharacterKey || + assert.fail(errors.invalidCharacterKey); + + // Get reference to the wanted item + const { want } = seat.getProposal(); + const { CharacterKey2: wantedCharacter } = want; + + // Get Character Key from inventorySeat + const inventoryCharacterKey = + inventorySeat.getAmountAllocated('CharacterKey'); + inventoryCharacterKey || assert.fail(errors.noKeyInInventory); + + const items = inventorySeat.getAmountAllocated('Item', itemBrand); + + AmountMath.isEqual( + wantedCharacter, + inventoryCharacterKey, + characterBrand, + ) || assert.fail(errors.inventoryKeyMismatch); + + /** @type {TransferPart[]} */ + const transfers = []; + transfers.push([inventorySeat, seat, { Item: items }]); + transfers.push([ + seat, + inventorySeat, + { CharacterKey1: providedCharacterKeyAmount }, + { CharacterKey: providedCharacterKeyAmount }, + ]); + transfers.push([ + inventorySeat, + seat, + { CharacterKey: wantedCharacter }, + { CharacterKey2: wantedCharacter }, + ]); + + try { + atomicRearrange(zcf, harden(transfers)); + } catch (e) { + assert.fail(errors.rearrangeError); + } + seat.exit(); + + characterRecord.inventoryKit.recorder.write( + inventorySeat.getAmountAllocated('Item').value.payload, + ); + }; + + return zcf.makeInvitation( + handler, + 'removeAllItemsFromInventory', + undefined, + M.splitRecord({ + give: { + CharacterKey1: M.splitRecord(characterShape), + }, + want: { + CharacterKey2: M.splitRecord(characterShape), + }, + }), + ); + }, + }, + item: { + initializeBaseItems(baseItems) { + const { item: itemState } = this.state; + if (itemState.bases.getSize() > 0) return; + + const common = []; + const uncommonToLegendary = []; + + baseItems.forEach((item) => { + if (item.rarity < minUncommonRating) common.push(item); + else uncommonToLegendary.push(item); + }); + + addAllToMap(itemState.bases, [ + ['common', harden(common)], + ['uncommonToLegendary', harden(uncommonToLegendary)], + ]); + }, + // Mints the default set of items to a seat that doesn't exit + async mintDefaultBatch(seat) { + const { helper, market: marketFacet } = this.facets; + const { item: itemState } = this.state; + + let commonBases = itemState.bases.get('common'); + const index1 = Math.floor(helper.randomNumber() * commonBases.length); + const item1 = commonBases[index1]; + + commonBases = commonBases.filter( + (item) => item.category !== item1.category, + ); + const index2 = Math.floor(helper.randomNumber() * commonBases.length); + const item2 = commonBases[index2]; + + commonBases = commonBases.filter( + (item) => item.category !== item1.category, + ); + + const index3 = Math.floor(helper.randomNumber() * commonBases.length); + const item3 = commonBases[index3]; + + const uncommonToLegendary = itemState.bases + .get('uncommonToLegendary') + .filter( + (item) => + item.category !== item1.category && + item.category !== item2.category, + ); + const index4 = Math.floor( + helper.randomNumber() * uncommonToLegendary.length, + ); + const item4 = uncommonToLegendary[index4]; + + const items = [item1, item2, item3, item4]; + + const currentTime = await helper.getTimeStamp(); + + const newItemAmount = AmountMath.make( + itemBrand, + makeCopyBag(harden(items.map((item) => [item, 1n]))), + ); + + await itemMint.mintGains({ Item: newItemAmount }, seat); + + let id = itemState.entries.getSize(); + + items.forEach((i) => { + const item = { + id, + item: i, + history: [ + { + type: 'mint', + data: i, + timestamp: currentTime, + }, + ], + }; + + addAllToMap(itemState.entries, [[id, harden(item)]]); + itemKit.recorder.write(item); + + id += 1; + // update metrics + marketFacet.updateMetrics('item', { + collectionSize: true, + averageLevel: { + type: 'add', + value: item.item.level, + }, + }); + }); + + return text.mintItemReturn; + }, + /** + * + * @param {ZCFSeat} seat + * @param {[Item, bigint][]} itemBatch + * @returns {Promise} + */ + async mintBatch(seat, itemBatch) { + const { helper, market: marketFacet } = this.facets; + const { item: itemState } = this.state; + + const currentTime = await helper.getTimeStamp(); + + const newItemAmount = AmountMath.make( + itemBrand, + makeCopyBag(harden(itemBatch)), + ); + + await itemMint.mintGains({ Item: newItemAmount }, seat); + + let id = itemState.entries.getSize(); + + itemBatch.forEach((copyBagEntry) => { + const [itemAsset, itemSupply] = copyBagEntry; + + for (let n = 0; n < itemSupply; n += 1) { + const item = { + id, + item: itemAsset, + history: [ + { + type: 'mint', + data: itemAsset, + timestamp: currentTime, + }, + ], + }; + + addAllToMap(itemState.entries, [[id, harden(item)]]); + itemKit.recorder.write(item); + + id += 1; + // update metrics + marketFacet.updateMetrics('item', { + collectionSize: true, + averageLevel: { + type: 'add', + value: item.item.level, + }, + }); + } + }); + + return text.mintItemReturn; + }, + mint() { + const handler = async (seat) => { + const { helper, market: marketFacet } = this.facets; + const { item: itemState } = this.state; + + const { want } = seat.getProposal(); + + const currentTime = await helper.getTimeStamp(); + + const items = want.Item.value.payload.map(([item, supply]) => { + return [item, supply]; + }); + const newItemAmount = AmountMath.make( + itemBrand, + makeCopyBag(harden(items)), + ); + + itemMint.mintGains({ Asset: newItemAmount }, seat); + + seat.exit(); + + let id = itemState.entries.getSize(); + + items.forEach((j) => { + const i = j[0]; + const item = { + id, + item: i, + // Potentially have separate durable stores for the history + history: [ + { + type: 'mint', + data: i, + timestamp: currentTime, + }, + ], + }; + + addAllToMap(itemState.entries, [[id, harden(item)]]); + itemKit.recorder.write(item); + + id += 1; + // update metrics + marketFacet.updateMetrics('item', { + collectionSize: true, + averageLevel: { + type: 'add', + value: item.item.level, + }, + }); + }); + + return text.mintItemReturn; + }; + + return zcf.makeInvitation( + handler, + 'mintItemNfts', + undefined, + M.splitRecord({ + want: { + Item: M.splitRecord(itemShape), + }, + }), + ); + }, + }, + market: { + handleExitCharacter(entry) { + const { market } = this.state; + const { market: marketFacet, character: characterFacet } = + this.facets; + + const { seat, asset, recorderKit } = entry; + const characterLevel = characterFacet.calculateLevel(asset.name); + + const subscriber = E(seat).getSubscriber(); + void E.when(E(subscriber).getUpdateSince(), () => { + marketFacet.updateMetrics('character', { + marketplaceAverageLevel: { + type: 'remove', + value: characterLevel, + }, + }); + + market.characterEntries.delete(asset.name); + + void marketFacet.deleteNode(recorderKit.recorder.getStorageNode()); + }); + }, + handleExitItem(entry) { + const { market } = this.state; + const { market: marketFacet } = this.facets; + + const { seat, asset, id, recorderKit } = entry; + + const subscriber = E(seat).getSubscriber(); + E.when(E(subscriber).getUpdateSince(), () => { + marketFacet.updateMetrics('item', { + marketplaceAverageLevel: { + type: 'remove', + value: asset.level, + }, + }); + + market.itemEntries.delete(id); + void marketFacet.deleteNode(recorderKit.recorder.getStorageNode()); + }); + }, + /** + * @param {string} collection + * @param {UpdateMetrics} updateMetrics + * @returns {void} + */ + updateMetrics(collection, updateMetrics) { + const updatedMetrics = updateCollectionMetrics( + collection, + this.state, + updateMetrics, + ); + if (collection === 'character') { + void marketCharacterMetricsKit.recorder.write(updatedMetrics); + } else if (collection === 'item') { + void marketItemMetricsKit.recorder.write(updatedMetrics); + } + }, + async makeMarketItemRecorderKit(id) { + const path = `item-${String(id)}`; + const node = await E(marketItemNode).makeChildNode(path); + return makeRecorderKit(node, MarketRecorderGuard); + }, + async makeMarketCharacterRecorderKit(id) { + const path = `character-${id}`; + const node = await E(marketCharacterNode).makeChildNode(path); + return makeRecorderKit(node, MarketRecorderGuard); + }, + /** + * Caveat assumes parent is either `marketCharacterNode` or + * `marketItemNode` and only the latter has 'character' anywhere in its + * path. + * + * @param {StorageNode} node + */ + // STOPGAP until https://github.com/Agoric/agoric-sdk/issues/7405 is available in Mainnet + async deleteNode(node) { + const path = await E(node).getPath(); + const segments = path.split('.'); + const parent = path.includes('character') + ? marketCharacterNode + : marketItemNode; + const childSegment = segments.at(-1); + assert(childSegment, `missing child path segment in ${path}`); + const deletable = E(parent).makeChildNode(childSegment, { + sequence: false, + }); + await E(deletable).setValue(''); + }, + sellItem() { + const handler = async (seat) => { + const { market } = this.state; + const { market: marketFacet } = this.facets; + + // Inspect allocation of Character keyword in seller seat + const itemInSellSeat = seat.getAmountAllocated('Item'); + const { want } = seat.getProposal(); + + paymentBrand === want.Price.brand || + assert.fail(errors.incorrectPaymentBrand(paymentBrand)); + const askingPrice = { + brand: want.Price.brand, + value: want.Price.value, + }; + const royalty = multiplyBy(want.Price, royaltyRate); + const platformFee = multiplyBy(want.Price, platformFeeRate); + + const id = this.state.market.metrics.get('item').putForSaleCount; + + const entryRecorder = await marketFacet.makeMarketItemRecorderKit( + id, + ); + + const asset = itemInSellSeat.value.payload[0][0]; + // Add to store array + const newEntry = harden({ + seat, + askingPrice, + royalty, + platformFee, + id, + asset, + recorderKit: entryRecorder, + isFirstSale: false, + }); + + // update metrics + marketFacet.updateMetrics('item', { + marketplaceAverageLevel: { + type: 'add', + value: asset.level, + }, + }); + + addAllToMap(market.itemEntries, [[newEntry.id, newEntry]]); + + const { seat: _omitSeat, recorderKit, ...entry } = newEntry; + recorderKit.recorder.write(entry); + marketFacet.updateMetrics('item', { putForSaleCount: true }); + + marketFacet.handleExitItem(newEntry); + }; + + return zcf.makeInvitation( + handler, + 'Sell Item in KREAd marketplace', + undefined, + M.splitRecord({ + give: { + Item: M.splitRecord(itemShape), + }, + want: { + Price: M.splitRecord({ + brand: BrandShape, + value: M.nat(), + }), + }, + }), + ); + }, + sellCharacter() { + const handler = async (seat) => { + const { market } = this.state; + const { character: characterFacet, market: marketFacet } = + this.facets; + + // Inspect allocation of Character keyword in seller seat + const characterInSellSeat = seat.getAmountAllocated('Character'); + const { want } = seat.getProposal(); + + paymentBrand === want.Price.brand || + assert.fail(errors.incorrectPaymentBrand(paymentBrand)); + const askingPrice = { + brand: want.Price.brand, + value: want.Price.value, + }; + const royalty = multiplyBy(want.Price, royaltyRate); + const platformFee = multiplyBy(want.Price, platformFeeRate); + + const character = characterInSellSeat.value.payload[0][0]; + + const entryRecorder = + await marketFacet.makeMarketCharacterRecorderKit(character.name); + + // Add to store array + const newEntry = { + seat, + askingPrice, + royalty, + platformFee, + id: character.name, + asset: character, + recorderKit: entryRecorder, + isFirstSale: false, + }; + + // update metrics + const characterLevel = characterFacet.calculateLevel( + character.name, + ); + marketFacet.updateMetrics('character', { + marketplaceAverageLevel: { + type: 'add', + value: characterLevel, + }, + }); + + addAllToMap(market.characterEntries, [ + [newEntry.id, harden(newEntry)], + ]); + + const { seat: _omitSeat, recorderKit, ...entry } = newEntry; + recorderKit.recorder.write(entry); + marketFacet.updateMetrics('character', { putForSaleCount: true }); + + marketFacet.handleExitCharacter(newEntry); + }; + + return zcf.makeInvitation( + handler, + 'Sell Character in KREAd marketplace', + undefined, + M.splitRecord({ + give: { + Character: M.splitRecord(characterShape), + }, + want: { + Price: M.splitRecord({ + brand: BrandShape, + value: M.nat(), + }), + }, + }), + ); + }, + buyItem() { + const handler = async (buyerSeat, offerArgs) => { + const { market: marketFacet } = this.facets; + const { market } = this.state; + + // Find store record based on wanted character + const sellRecord = market.itemEntries.get(offerArgs.entryId); + sellRecord || + assert.fail(errors.itemNotFound(offerArgs.entryId)); + + const result = await (sellRecord.isFirstSale + ? marketFacet.buyFirstSaleItem( + sellRecord.seat, + buyerSeat, + sellRecord, + ) + : marketFacet.buySecondarySaleItem( + sellRecord.seat, + buyerSeat, + sellRecord, + )); + result.success || assert.fail(result.error); + }; + + return zcf.makeInvitation( + handler, + 'Buy Item in KREAd marketplace', + undefined, + M.splitRecord({ + give: { + Price: M.splitRecord({ + brand: BrandShape, + value: M.nat(), + }), + }, + want: { + Item: M.splitRecord(itemShape), + }, + }), + ); + }, + /** + * + * @param {ZCFSeat} sellerSeat + * @param {ZCFSeat} buyerSeat + * @param {ItemMarketRecord} sellRecord + * @returns {Promise} + */ + async buyFirstSaleItem(sellerSeat, buyerSeat, sellRecord) { + const { market: marketFacet } = this.facets; + const { market } = this.state; + + const { want, give } = buyerSeat.getProposal(); + const { Item: wantedItemAmount } = want; + + const itemForSaleAmount = harden({ + brand: itemBrand, + value: makeCopyBag([[sellRecord.asset, 1n]]), + }); + const itemForSalePrice = sellRecord.askingPrice; + // Inspect Price keyword from buyer seat + const { Price: providedMoneyAmount } = give; + + if ( + !AmountMath.isEqual(wantedItemAmount, itemForSaleAmount, itemBrand) + ) { + return { + success: false, + error: errors.sellerSeatMismatch, + }; + } + + if ( + !AmountMath.isGTE( + providedMoneyAmount, + AmountMath.add( + AmountMath.add(sellRecord.askingPrice, sellRecord.royalty), + sellRecord.platformFee, + ), + paymentBrand, + ) + ) { + return { + success: false, + error: errors.insufficientFunds, + }; + } + + const { zcfSeat, userSeat } = zcf.makeEmptySeatKit(); + + /** @type {TransferPart[]} */ + const transfers = []; + // Transfer item: seller -> buyer + transfers.push([sellerSeat, buyerSeat, { Item: itemForSaleAmount }]); + // Transfer artist royalty: buyer -> artist + transfers.push([ + buyerSeat, + zcfSeat, + { Price: sellRecord.royalty }, + { Royalty: sellRecord.royalty }, + ]); + // Transfer KREAd fees: buyer -> KREAd + transfers.push([ + buyerSeat, + zcfSeat, + { Price: sellRecord.platformFee }, + { PlatformFee: sellRecord.platformFee }, + ]); + + // Transfer askingPrice: buyer -> artist + transfers.push([ + buyerSeat, + zcfSeat, + { + Price: AmountMath.subtract( + providedMoneyAmount, + AmountMath.add(sellRecord.royalty, sellRecord.platformFee), + ), + }, + ]); + + try { + atomicRearrange(zcf, harden(transfers)); + } catch (e) { + assert.fail(errors.rearrangeError); + } + buyerSeat.exit(); + zcfSeat.exit(); + + const payouts = await E(userSeat).getPayouts(); + const royaltyPayout = await payouts.Royalty; + const platformFeePayout = await payouts.PlatformFee; + + await E(royaltyDepositFacet).receive(royaltyPayout); + await E(platformFeeDepositFacet).receive(platformFeePayout); + + const askingPricePayout = await payouts.Price; + await E(royaltyDepositFacet).receive(askingPricePayout); + + // Remove entry from market + marketFacet.updateMetrics('item', { + marketplaceAverageLevel: { + type: 'remove', + value: sellRecord.asset.level, + }, + }); + + market.itemEntries.delete(sellRecord.id); + void marketFacet.deleteNode( + sellRecord.recorderKit.recorder.getStorageNode(), + ); + + // update metrics + marketFacet.updateMetrics('item', { + amountSold: true, + latestSalePrice: Number(itemForSalePrice.value), + }); + return { + success: true, + error: '', + }; + }, + /** + * + * @param {ZCFSeat} sellerSeat + * @param {ZCFSeat} buyerSeat + * @param {ItemMarketRecord} sellRecord + * @returns {Promise} + */ + async buySecondarySaleItem(sellerSeat, buyerSeat, sellRecord) { + const { market: marketFacet } = this.facets; + + const { want, give } = buyerSeat.getProposal(); + const { Item: wantedItemAmount } = want; + + const itemForSaleAmount = sellerSeat.getProposal().give.Item; + const itemForSalePrice = sellerSeat.getProposal().want.Price; + + // Inspect Price keyword from buyer seat + const { Price: providedMoneyAmount } = give; + + if ( + !AmountMath.isEqual(wantedItemAmount, itemForSaleAmount, itemBrand) + ) { + return { + success: false, + error: errors.sellerSeatMismatch, + }; + } + + if ( + !AmountMath.isGTE( + providedMoneyAmount, + AmountMath.add( + AmountMath.add(sellRecord.askingPrice, sellRecord.royalty), + sellRecord.platformFee, + ), + paymentBrand, + ) + ) { + return { + success: false, + error: errors.insufficientFunds, + }; + } + + const { zcfSeat, userSeat } = zcf.makeEmptySeatKit(); + + /** @type {TransferPart[]} */ + const transfers = []; + // Transfer item: seller -> buyer + transfers.push([sellerSeat, buyerSeat, { Item: itemForSaleAmount }]); + // Transfer artist royalty: buyer -> artist + transfers.push([ + buyerSeat, + zcfSeat, + { Price: sellRecord.royalty }, + { Royalty: sellRecord.royalty }, + ]); + // Transfer KREAd fees: buyer -> KREAd + transfers.push([ + buyerSeat, + zcfSeat, + { Price: sellRecord.platformFee }, + { PlatformFee: sellRecord.platformFee }, + ]); + + // Transfer askingPrice: buyer -> seller + transfers.push([ + buyerSeat, + sellerSeat, + { + Price: AmountMath.subtract( + providedMoneyAmount, + AmountMath.add(sellRecord.royalty, sellRecord.platformFee), + ), + }, + ]); + + try { + atomicRearrange(zcf, harden(transfers)); + } catch (e) { + assert.fail(errors.rearrangeError); + } + + buyerSeat.exit(); + sellerSeat.exit(); + zcfSeat.exit(); + + const payouts = await E(userSeat).getPayouts(); + const royaltyPayout = await payouts.Royalty; + const platformFeePayout = await payouts.PlatformFee; + + await E(royaltyDepositFacet).receive(royaltyPayout); + await E(platformFeeDepositFacet).receive(platformFeePayout); + + // update metrics + marketFacet.updateMetrics('item', { + amountSold: true, + latestSalePrice: Number(itemForSalePrice.value), + }); + return { + success: true, + error: '', + }; + }, + buyCharacter() { + const handler = async (buyerSeat) => { + const { market: marketFacet } = this.facets; + const { market, character: characterState } = this.state; + + // Inspect Character keyword in buyer seat + const { want, give } = buyerSeat.getProposal(); + const { Character: wantedCharacterAmount } = want; + const character = wantedCharacterAmount.value.payload[0][0]; + + // Find characterRecord entry based on wanted character + const characterRecord = characterState.entries.get(character.name); + characterRecord || assert.fail(errors.character404); + + // Find store record based on wanted character + const sellRecord = market.characterEntries.get(character.name); + + sellRecord || assert.fail(errors.character404); + const sellerSeat = sellRecord.seat; + + // Inspect Price keyword from buyer seat + const { Price: providedMoneyAmount } = give; + const { Character: characterForSaleAmount } = + sellerSeat.getProposal().give; + + AmountMath.isEqual( + wantedCharacterAmount, + characterForSaleAmount, + characterBrand, + ) || assert.fail(errors.sellerSeatMismatch); + + const characterForSalePrice = sellRecord.askingPrice; + + AmountMath.isGTE( + providedMoneyAmount, + AmountMath.add( + AmountMath.add(sellRecord.askingPrice, sellRecord.royalty), + sellRecord.platformFee, + ), + paymentBrand, + ) || assert.fail(errors.insufficientFunds); + const { zcfSeat, userSeat } = zcf.makeEmptySeatKit(); + + /** @type {TransferPart[]} */ + const transfers = []; + transfers.push([ + sellerSeat, + buyerSeat, + { Character: characterForSaleAmount }, + ]); + transfers.push([ + buyerSeat, + zcfSeat, + { Price: sellRecord.royalty }, + { Royalty: sellRecord.royalty }, + ]); + transfers.push([ + buyerSeat, + zcfSeat, + { Price: sellRecord.platformFee }, + { PlatformFee: sellRecord.platformFee }, + ]); + transfers.push([ + buyerSeat, + sellerSeat, + { + Price: AmountMath.subtract( + providedMoneyAmount, + AmountMath.add(sellRecord.royalty, sellRecord.platformFee), + ), + }, + ]); + + try { + atomicRearrange(zcf, harden(transfers)); + } catch (e) { + assert.fail(errors.rearrangeError); + } + zcfSeat.exit(); + + const payouts = await E(userSeat).getPayouts(); + const royaltyPayout = await payouts.Royalty; + const platformFeePayout = await payouts.PlatformFee; + + await E(royaltyDepositFacet).receive(royaltyPayout); + await E(platformFeeDepositFacet).receive(platformFeePayout); + + // update metrics + marketFacet.updateMetrics('character', { + amountSold: true, + latestSalePrice: Number(characterForSalePrice.value), + }); + + buyerSeat.exit(); + sellerSeat.exit(); + }; + + return zcf.makeInvitation( + handler, + 'Buy Character in KREAd marketplace', + undefined, + M.splitRecord({ + give: { + Price: M.splitRecord({ + brand: BrandShape, + value: M.nat(), + }), + }, + want: { + Character: M.splitRecord(characterShape), + }, + }), + ); + }, + }, + creator: { + makeMintItemInvitation() { + const { item } = this.facets; + return item.mint(); + }, + initializeMetrics() { + const { market } = this.state; + if (market.metrics.getSize() > 0) return; + + addAllToMap( + market.metrics, + ['character', 'item'].map((key) => [ + key, + harden({ + collectionSize: 0, + averageLevel: 0, + marketplaceAverageLevel: 0, + amountSold: 0, + latestSalePrice: 0, + putForSaleCount: 0, + }), + ]), + ); + + marketCharacterMetricsKit.recorder.write( + market.metrics.get('character'), + ); + marketItemMetricsKit.recorder.write(market.metrics.get('item')); + }, + reviveMarketExitSubscribers() { + const { market } = this.state; + const { market: marketFacet } = this.facets; + + const characters = Array.from(market.characterEntries.values()); + characters.forEach((entry) => marketFacet.handleExitCharacter(entry)); + + const items = Array.from(market.itemEntries.values()); + items.forEach((entry) => marketFacet.handleExitItem(entry)); + }, + initializeBaseAssets(baseCharacters, baseItems) { + const { character, item } = this.facets; + character.initializeBaseCharacters(baseCharacters); + item.initializeBaseItems(baseItems); + }, + initializeCharacterNamesEntries() { + const { character } = this.state; + if (!character.entries.has('names')) { + character.entries.init('names', harden([])); + } + }, + /** + * + * @param {Amount} price + * @param {[Item, bigint][]} itemsToSell + */ + async publishItemCollection(price, itemsToSell) { + const { market } = this.state; + const { market: marketFacet, item } = this.facets; + + const { zcfSeat: internalSellSeat } = zcf.makeEmptySeatKit(); + await item.mintBatch(internalSellSeat, itemsToSell); + + const askingPrice = { + brand: price.brand, + value: price.value, + }; + const royalty = multiplyBy(price, royaltyRate); + const platformFee = multiplyBy(price, platformFeeRate); + const claimedIdAndRecorder = await Promise.all( + itemsToSell.map(async (copyBagEntry) => { + const [_, itemSupply] = copyBagEntry; + const supplyRange = Array.from(Array(Number(itemSupply)).keys()); + const idAndRecorder = await Promise.all( + supplyRange.map(async () => { + // putForSaleCount is incremented by updateMetrics() with each iteration of this loop + const id = + this.state.market.metrics.get('item').putForSaleCount; + await marketFacet.updateMetrics('item', { + putForSaleCount: true, + }); + const entryRecorder = + await marketFacet.makeMarketItemRecorderKit(id); + return [id, entryRecorder]; + }), + ); + return idAndRecorder; + }), + ); + + itemsToSell.forEach(async (copyBagEntry, i) => { + const [itemAsset, itemSupply] = copyBagEntry; + + for (let n = 0; n < itemSupply; n += 1) { + const [id, entryRecorder] = claimedIdAndRecorder[i][n]; + // Add to store array + const newEntry = { + seat: internalSellSeat, + askingPrice, + royalty, + platformFee, + id, + asset: itemAsset, + recorderKit: entryRecorder, + isFirstSale: true, + }; + + // update metrics + marketFacet.updateMetrics('item', { + marketplaceAverageLevel: { + type: 'add', + value: itemAsset.level, + }, + }); + + addAllToMap(market.itemEntries, [ + [newEntry.id, harden(newEntry)], + ]); + const { seat: _omitSeat, recorderKit, ...entry } = newEntry; + recorderKit.recorder.write(entry); + } + }); + }, + }, + public: { + makeMintCharacterInvitation() { + const { character } = this.facets; + return character.mint(); + }, + getCharacters() { + const characters = Array.from( + this.state.character.entries.values(), + ).filter((x) => !Array.isArray(x)); + return characters; + }, + getCharacterInventory(name) { + const character = this.state.character.entries.get(name); + const { inventory } = character; + const items = inventory.getAmountAllocated('Item', itemBrand).value + .payload; + return { items }; + }, + makeEquipInvitation() { + const { character } = this.facets; + return character.equip(); + }, + makeUnequipInvitation() { + const { character } = this.facets; + return character.unequip(); + }, + makeItemSwapInvitation() { + const { character } = this.facets; + return character.swap(); + }, + makeUnequipAllInvitation() { + const { character } = this.facets; + return character.unequipAll(); + }, + makeSellCharacterInvitation() { + const { market } = this.facets; + return market.sellCharacter(); + }, + makeBuyCharacterInvitation() { + const { market } = this.facets; + return market.buyCharacter(); + }, + getCharactersForSale() { + const characters = Array.from( + this.state.market.characterEntries.values(), + ); + return characters; + }, + makeSellItemInvitation() { + const { market } = this.facets; + return market.sellItem(); + }, + makeBuyItemInvitation() { + const { market } = this.facets; + return market.buyItem(); + }, + getItemsForSale() { + const items = Array.from(this.state.market.itemEntries.values()); + return items; + }, + getMarketMetrics() { + const { market } = this.state; + return { + character: market.metrics.get('character'), + item: market.metrics.get('item'), + }; + }, + getCharacterLevel(name) { + const { character } = this.facets; + return character.calculateLevel(name); + }, + }, + }, + ); +}; + +harden(prepareKreadKit); + +/** + * + * @param {import('@agoric/vat-data').Baggage} baggage + * @param {StorageNode} storageNode + * @param {import('@agoric/zoe/src/contractSupport/recorder.js').MakeRecorderKit} makeRecorderKit + * @returns + */ +export const provideKreadKitRecorderKits = ( + baggage, + storageNode, + makeRecorderKit, +) => + provideRecorderKits( + baggage, + storageNode, + makeRecorderKit, + { + infoKit: 'info', + characterKit: 'character', + itemKit: 'item', + marketCharacterKit: 'market-characters', + marketItemKit: 'market-items', + marketCharacterMetricsKit: 'market-metrics-character', + marketItemMetricsKit: 'market-metrics-item', + }, + { + characterKit: CharacterRecorderGuard, + itemKit: ItemRecorderGuard, + marketCharacterKit: M.arrayOf(MarketRecorderGuard), + marketItemKit: M.arrayOf(MarketRecorderGuard), + marketCharacterMetricsKit: MarketMetricsGuard, + marketItemMetricsKit: MarketMetricsGuard, + }, + ); diff --git a/agoric/contract/src/kreadV2/market-metrics.js b/agoric/contract/src/kreadV2/market-metrics.js new file mode 100644 index 000000000..b79f34693 --- /dev/null +++ b/agoric/contract/src/kreadV2/market-metrics.js @@ -0,0 +1,77 @@ +/** + * adds a value to the average + * + * @param {number} average + * @param {number} size + * @param {number} value + * @returns {number} + */ +function addToAverage(average, size, value) { + return (average * size + value) / (size + 1); +} + +/** + * Removes a value from the average + * + * @param {number} average + * @param {number} size + * @param {number} value + * @returns {number} + */ +function removeFromAverage(average, size, value) { + if (size === 1) { + return 0; + } + return (size * average - value) / (size - 1); +} + +/** + * Updates an average based on add or remove + * + * @param {string} type + * @param {number} average + * @param {number} size + * @param {number} value + */ +function updateAverage(type, average, size, value) { + if (type === 'add') { + return addToAverage(average, size, value); + } else if (type === 'remove') { + return removeFromAverage(average, size, value); + } +} + +/** + * Updates character metrics + * + * @param {string} collection + * @param {object} state + * @param {UpdateMetrics} updateMetrics + */ +export const updateCollectionMetrics = (collection, state, updateMetrics) => { + const metrics = { ...state.market.metrics.get(collection) }; + if (updateMetrics.averageLevel) { + metrics.averageLevel = updateAverage( + updateMetrics.averageLevel.type, + metrics.averageLevel, + metrics.collectionSize, + updateMetrics.averageLevel.value, + ); + } + if (updateMetrics.marketplaceAverageLevel) { + metrics.marketplaceAverageLevel = updateAverage( + updateMetrics.marketplaceAverageLevel.type, + metrics.marketplaceAverageLevel, + state.market[`${collection}Entries`].getSize(), + updateMetrics.marketplaceAverageLevel.value, + ); + } + if (updateMetrics.latestSalePrice) + metrics.latestSalePrice = updateMetrics.latestSalePrice; + if (updateMetrics.collectionSize) metrics.collectionSize += 1; + if (updateMetrics.amountSold) metrics.amountSold += 1; + if (updateMetrics.putForSaleCount) metrics.putForSaleCount += 1; + + state.market.metrics.set(collection, metrics); + return metrics; +}; diff --git a/agoric/contract/src/kreadV2/text.js b/agoric/contract/src/kreadV2/text.js new file mode 100644 index 000000000..686a6d290 --- /dev/null +++ b/agoric/contract/src/kreadV2/text.js @@ -0,0 +1,7 @@ +export const text = { + mintCharacterReturn: 'Character NFT minted successfully!', + mintItemReturn: 'Item NFT(s) minted successfully!', + tokenFacetReturn: 'Success', + equipReturn: 'Item(s) were equipped successfully', + unequipReturn: 'Item(s) were unequipped successfully', +}; diff --git a/agoric/contract/src/kreadV2/type-guards.js b/agoric/contract/src/kreadV2/type-guards.js new file mode 100644 index 000000000..62cf989ad --- /dev/null +++ b/agoric/contract/src/kreadV2/type-guards.js @@ -0,0 +1,248 @@ +import { M } from '@agoric/store'; +import { BrandShape } from '@agoric/ertp'; + +export const HelperI = M.interface( + 'helper', + {}, + // not exposed so sloppy okay + { sloppy: true }, +); + +export const BaseCharacterGuard = M.splitRecord({ + title: M.string(), + description: M.string(), + origin: M.string(), + level: M.gte(0), + artistMetadata: M.string(), + image: M.string(), + characterTraits: M.string(), +}); + +export const CharacterGuard = M.splitRecord({ + title: M.string(), + description: M.string(), + origin: M.string(), + level: M.gte(0), + artistMetadata: M.string(), + image: M.string(), + characterTraits: M.string(), + name: M.string({ stringLengthLimit: 20 }), + keyId: M.number(), + id: M.gte(0), + date: M.record(), +}); + +export const RatioObject = { + numerator: M.nat(), + denominator: M.nat(), +}; + +export const CharacterGuardBagShape = M.bagOf(CharacterGuard); + +export const ItemGuard = M.splitRecord({ + name: M.string(), + category: M.or( + 'hair', + 'headPiece', + 'mask', + 'filter1', + 'filter2', + 'perk1', + 'perk2', + 'garment', + 'patch', + 'background', + ), + description: M.string(), + functional: M.boolean(), + origin: M.string(), + image: M.string(), + thumbnail: M.string(), + rarity: M.gte(0), + level: M.gte(0), + filtering: M.gte(0), + weight: M.gte(0), + sense: M.gte(0), + reserves: M.gte(0), + durability: M.gte(0), + colors: M.arrayOf(M.string()), + artistMetadata: M.string(), +}); + +export const RarityGuard = M.or('common', 'uncommonToLegendary'); + +export const ItemGuardBagShape = M.bagOf(ItemGuard); + +export const MarketMetricsGuard = M.splitRecord({ + amountSold: M.gte(0), + collectionSize: M.gte(0), + averageLevel: M.gte(0), + marketplaceAverageLevel: M.gte(0), + latestSalePrice: M.gte(0), + putForSaleCount: M.gte(0), +}); + +export const UpdateMarketMetricsGuard = M.splitRecord( + {}, + { + amountSold: M.boolean(), + collectionSize: M.boolean(), + averageLevel: M.splitRecord({ + type: M.or('add', 'remove'), + value: M.gte(0), + }), + marketplaceAverageLevel: M.splitRecord({ + type: M.or('add', 'remove'), + value: M.gte(0), + }), + latestSalePrice: M.gte(0), + putForSaleCount: M.boolean(), + }, +); + +export const PublicI = M.interface('public', { + // Mint + makeMintCharacterInvitation: M.call().returns(M.promise()), + // Inventory + makeEquipInvitation: M.call().returns(M.promise()), + makeUnequipInvitation: M.call().returns(M.promise()), + makeUnequipAllInvitation: M.call().returns(M.promise()), + makeItemSwapInvitation: M.call().returns(M.promise()), + // Market + makeSellCharacterInvitation: M.call().returns(M.promise()), + makeBuyCharacterInvitation: M.call().returns(M.promise()), + makeSellItemInvitation: M.call().returns(M.promise()), + makeBuyItemInvitation: M.call().returns(M.promise()), + // Getters + getCharacters: M.call().returns(M.array()), + getCharacterInventory: M.call().returns(M.splitRecord({ items: M.array() })), + getCharactersForSale: M.call().returns(M.array()), + getItemsForSale: M.call().returns(M.array()), + getMarketMetrics: M.call().returns(M.record()), + getCharacterLevel: M.call(M.string()).returns(M.gte(0)), +}); + +export const CreatorI = M.interface('creator', { + makeMintItemInvitation: M.call().returns(M.promise()), + initializeMetrics: M.call().returns(), + reviveMarketExitSubscribers: M.call().returns(), + initializeBaseAssets: M.call( + M.arrayOf([M.number(), BaseCharacterGuard]), + M.arrayOf(ItemGuard), + ).returns(), + initializeCharacterNamesEntries: M.call().returns(), + publishItemCollection: M.call().returns(M.promise()), +}); + +export const CharacterI = M.interface('character', { + mint: M.call().returns(M.promise()), + equip: M.call().returns(M.promise()), + unequip: M.call().returns(M.promise()), + unequipAll: M.call().returns(M.promise()), + swap: M.call().returns(M.promise()), + validateInventoryState: M.call().returns(M.boolean()), + isNameUnique: M.call(M.string()).returns(M.boolean()), + getRandomBaseIndex: M.call().returns(M.any()), + calculateLevel: M.call(M.string()).returns(M.gte(0)), + makeInventoryRecorderKit: M.call(M.string()).returns( + M.promise(M.remotable('Notifier')), + ), + initializeBaseCharacters: M.call( + M.arrayOf([M.number(), BaseCharacterGuard]), + ).returns(), +}); + +export const ItemI = M.interface('item', { + mint: M.call().returns(M.promise()), + mintDefaultBatch: M.call().returns(M.promise(M.string())), + mintBatch: M.call().returns(M.promise(M.string())), + initializeBaseItems: M.call(M.arrayOf(ItemGuard)).returns(), +}); + +export const MarketRecorderGuard = M.or( + M.splitRecord({ + id: M.or(M.gte(0), M.string()), + askingPrice: M.splitRecord({ + brand: BrandShape, + value: M.nat(), + }), + royalty: M.splitRecord({ + brand: BrandShape, + value: M.nat(), + }), + platformFee: M.splitRecord({ + brand: BrandShape, + value: M.nat(), + }), + asset: M.or(CharacterGuard, ItemGuard), + isFirstSale: M.boolean(), + // history: M.arrayOf(HistoryGuard), + }), + M.string(''), +); + +export const MarketEntryGuard = M.splitRecord({ + id: M.or(M.gte(0), M.string()), + seat: M.eref(M.remotable('Seat')), + recorderKit: M.record(), // TODO: figure out how to type recorderkits + askingPrice: M.splitRecord({ + brand: BrandShape, + value: M.nat(), + }), + royalty: M.splitRecord({ + brand: BrandShape, + value: M.nat(), + }), + platformFee: M.splitRecord({ + brand: BrandShape, + value: M.nat(), + }), + asset: M.or(CharacterGuard, ItemGuard), + isFirstSale: M.boolean(), + // history: M.arrayOf(HistoryGuard), +}); + +export const MarketI = M.interface('market', { + sellItem: M.call().returns(M.promise()), + buyItem: M.call().returns(M.promise()), + buyFirstSaleItem: M.call().returns(M.promise()), + buySecondarySaleItem: M.call().returns(M.promise()), + handleExitItem: M.call(MarketEntryGuard).returns(), + handleExitCharacter: M.call(MarketEntryGuard).returns(), + makeMarketItemRecorderKit: M.call(M.number()).returns(M.promise()), + makeMarketCharacterRecorderKit: M.call(M.string()).returns(M.promise()), + deleteNode: M.call(M.remotable('StorageNode')).returns(M.promise(/* void */)), + sellCharacter: M.call().returns(M.promise()), + buyCharacter: M.call().returns(M.promise()), + updateMetrics: M.call( + M.or('character', 'item'), + UpdateMarketMetricsGuard, + ).returns(), +}); + +export const HistoryGuard = M.splitRecord({ + type: M.string(), + data: M.any(), + timestamp: M.record(), +}); + +export const CharacterEntryGuard = M.splitRecord({ + name: M.string(), + character: CharacterGuard, + inventory: M.eref(M.remotable('Seat')), + inventoryKit: M.record(), // TODO: figure out how to type recorderkits + history: M.arrayOf(HistoryGuard), +}); + +export const CharacterRecorderGuard = M.splitRecord({ + name: M.string(), + character: CharacterGuard, + inventoryKit: M.record(), // TODO: figure out how to type recorderkits + history: M.arrayOf(HistoryGuard), +}); + +export const ItemRecorderGuard = M.splitRecord({ + id: M.gte(0), + item: ItemGuard, + history: M.arrayOf(HistoryGuard), +}); diff --git a/agoric/contract/src/kreadV2/types.js b/agoric/contract/src/kreadV2/types.js new file mode 100644 index 000000000..f39049a7f --- /dev/null +++ b/agoric/contract/src/kreadV2/types.js @@ -0,0 +1,295 @@ +// @ts-check + +/** + * @typedef {{ + * mintFee: bigint, + * royaltyRate: RatioObject, + * platformFeeRate: RatioObject, + * mintRoyaltyRate: RatioObject, + * mintPlatformFeeRate: RatioObject, + * royaltyDepositFacet: DepositFacet, + * platformFeeDepositFacet: DepositFacet, + * assetNames: { character: string, item: string }, + * minUncommonRating: number + * }} KREAdTerms + */ + +/** + * Holds contract data + * + * @typedef {{ + * config: Config + * assetMints: AssetMints + * tokenInfo: TokenInfo + * notifiers: Notifiers + * characters: CharacterRecord[] + * items: ItemRecord[] + * randomNumber?: Function + * market: Market + * ready: boolean + * boardId?: string + * }} State + * + * Assets + * @typedef {{ + * character: ZCFMint<"copyBag"> + * item: ZCFMint<"copyBag"> + * }} AssetMints + * + * @typedef {{ + * character: CharacterMarketRecord[] + * item: ItemMarketRecord[] + * }} Market + * + * @typedef {{ + * character: { + * name: string + * brand: Brand + * issuer: Issuer<'set'> + * } + * item: { + * name: string + * brand: Brand + * issuer: Issuer<'set'> + * } + * }} TokenInfo + * + * @typedef {{ + * sellerSeat: ZCFSeat + * name: string + * character: object[] + * askingPrice: any + * }} CharacterMarketRecord + * + * @typedef {{ + * sellerSeat: ZCFSeat + * id: string + * asset: object[] + * askingPrice: any + * isFirstSale: boolean + * royalty: Amount + * platformFee: Amount + * recorderKit: import("./utils.js").RecorderKit + * }} ItemMarketRecord + * + * @typedef {{ + * storageNode: StorageNode + * marshaller: Marshaller + * }} Powers + * + * @typedef {{ + * tokenData: { + * characters: object[] + * items: object[] + * } + * defaultPaymentToken?: { + * brand: Brand<"nat"> + * issuer: Issuer<"nat"> + * } + * timerService: import('@agoric/time/src/types').TimerService + * powers: Powers + * }} Config + * + * @typedef {{ + * market: { + * characters: { + * subscriber: StoredSubscriber + * publisher: Publisher + * } + * items: { + * subscriber: StoredSubscriber + * publisher: Publisher + * } + * } + * inventory: { + * subscriber: StoredSubscriber + * publisher: Publisher + * } + * info: { + * subscriber: StoredSubscriber + * publisher: Publisher + * } + * }} Notifiers + * + * @typedef {{ + * name: string + * character: object + * inventory: ZCFSeat + * seat?: ZCFSeat + * notifier?: Notifier + * publisher: Publisher + * }} CharacterRecord + * + * @typedef {{ + * noseline?: Item; + * midBackground?: Item; + * mask?: Item; + * headPiece?: Item; + * hair?: Item; + * frontMask?: Item; + * liquid?: Item; + * background?: Item; + * airReservoir?: Item; + * clothing?: Item; + * }} DefaultItem + * + * @typedef {{ + * id: bigint + * character: object + * inventory: ZCFSeat + * seat?: ZCFSeat + * sell: { + * instance: Instance + * publicFacet: any + * price: bigint + * } + * }} CharacterInMarket + * + * @typedef {{ + * name: string; + * category: string; + * id: string; + * description: string; + * image: string; + * level: number; + * rarity: number; + * effectiveness?: number; + * layerComplexity?: number; + * forged: string; + * baseMaterial: string; + * colors: string[]; + * projectDescription: string; + * price: number; + * details: any; + * date: string; + * activity: any[]; + * }} Item + * + * @typedef {{ + * id: bigint + * item: object + * }} ItemRecord + * + * @typedef {{ + * id: bigint + * item: Item + * sell: { + * instance: Instance + * publicFacet: any + * price: bigint + * } + * }} ItemInMarket + * + * // PRIVATE STORAGE + * @typedef {{ + * id: bigint; + * add?: string[]; + * remove?: string[]; + * }} InventoryEvent + * + * @typedef {{ + * seat?: ZCFSeat; + * name: string; + * history: InventoryEvent[]; + * }} InventoryKeyRecord + * + * @typedef {InventoryKeyRecord[]} InventoryKeyStorage + * + * + * @typedef {{ + * isReady: () => boolean + * isConfigReady: () => boolean + * inventory: (name: string) => { items: Item[] } + * inventoryPublisher: (name: string) => Publisher + * characterKey: (name: string) => { key: Amount } + * characterCount: () => number + * itemCount: () => number + * character: (name: string) => CharacterRecord + * time: () => Promise + * randomBaseCharacter: () => object + * assetInfo: () => TokenInfo + * defaultItems: () => any[] + * powers: () => Powers + * config: () => Config + * randomItem: () => object + * marketPublisher: () => { + * characters: { + * subscriber: StoredSubscriber + * publisher: Publisher + * } + * items: { + * subscriber: StoredSubscriber + * publisher: Publisher + * } + * } + * }} KreadState_get + * + * @typedef {{ + * powers: (powers: Powers, notifiers: Notifiers) => void + * publishKreadInfo: (boardId: string, publicFacet: object) => void + * }} KreadState_set + * + * @typedef {{ + * characters: (characters: CharacterRecord[]) => void + * items: (items: ItemRecord[]) => void + * updateConfig: (newConfig: Config) => void + * }} KreadState_add + * + * @typedef {{ nameIsUnique: NameIsUniqueFn }} KreadState_validate + * + * @typedef {{ + * isReady: () => boolean + * isValidName: () => boolean + * getInventory: (name: string) => { items: Item[] } + * getCharacterKey: (name: string) => { key: Amount } + * getCount: () => { characters: bigint, items: bigint } + * getCharacter: (name: string) => CharacterRecord + * getTime: () => Promise + * getRandomBaseCharacter: () => object + * getTokenInfo: () => TokenInfo + * getDefaultItems: () => any[] + * getPowers: () => Powers | undefined + * getConfig: () => Config + * getRandomItem: () => object + * getCharacterCount: () => number + * getItemCount: () => number + * getCharacterMarket: () => CharacterMarketRecord[] + * getCharacterMarketRange: () => CharacterMarketRecord[] + * getItemMarket: () => ItemMarketRecord[] + * getItemMarketRange: () => ItemMarketRecord[] + * }} KreadState_public + * + * @typedef {{ + * get: KreadState_get + * set: KreadState_set + * add: KreadState_add + * validate: KreadState_validate + * public: KreadState_public + * }} KreadState + * + * @typedef {(name: string) => boolean} NameIsUniqueFn + * + * @typedef {Partial<{ + * averageLevel: UpdateAverage + * marketplaceAverageLevel: UpdateAverage + * latestSalePrice: number + * collectionSize: boolean + * amountSold: boolean, + * putForSaleCount: boolean + * }>} UpdateMetrics + * + * @typedef {{ + * type: ("add" | "remove") + * value: number + * }} UpdateAverage + * + * @typedef {{ + * numerator: bigint, + * denominator: bigint, + * }} RatioObject + * + * @typedef {{ + * success: boolean, + * error: string, + * }} HelperFunctionReturn + */ diff --git a/agoric/contract/src/kreadV2/utils.js b/agoric/contract/src/kreadV2/utils.js new file mode 100644 index 000000000..39fe4f110 --- /dev/null +++ b/agoric/contract/src/kreadV2/utils.js @@ -0,0 +1,109 @@ +// @ts-check +import { allValues, objectMap } from '@agoric/internal'; +import { E } from '@endo/eventual-send'; +import { M, matches, getCopyMapEntries } from '@endo/patterns'; + +const { Fail } = assert; + +/** + * @param {string} name + * @param {object} randomCharacterBase + * @param {number} newCharacterId + * @param {object} currentTime + * @returns {object[]} + */ +export const makeCharacterNftObjs = ( + name, + randomCharacterBase, + newCharacterId, + currentTime, +) => { + // Merge random base character with name input, id, and keyId + const newCharacter1 = { + ...randomCharacterBase, + date: currentTime, + id: newCharacterId, + name, + keyId: 1, + }; + const newCharacter2 = { + ...newCharacter1, + keyId: 2, + }; + return [newCharacter1, newCharacter2]; +}; + +/** + * @param {import('@agoric/vat-data').Baggage} baggage + * @param {ERef} storageNode + * @param {import('@agoric/zoe/src/contractSupport').MakeRecorderKit} makeRecorderKit + * @param {{[key: string]: string}} paths + * @param {{[key: string]: Pattern}} typeMatchers + * @returns {Promise<{[key: string]: import('@agoric/zoe/src/contractSupport').RecorderKit}>} + */ +export const provideRecorderKits = async ( + baggage, + storageNode, + makeRecorderKit, + paths, + typeMatchers, +) => { + // console.log('provideRecorderKits', paths, typeMatchers); + const keys = Object.keys(paths); + // assume if any keys are defined they all are + const inBaggage = baggage.has(keys[0]); + if (inBaggage) { + const obj = objectMap( + paths, + /** @type {(value: any, key: string) => any} */ + (_, k) => baggage.get(k), + ); + return Promise.resolve(harden(obj)); + } + + const keyedPromises = objectMap(paths, async (_path, key) => { + const node = await E(storageNode).makeChildNode(paths[key]); + return makeRecorderKit(node, typeMatchers[key]); + }); + + return allValues(keyedPromises).then((keyedVals) => { + for (const [k, v] of Object.entries(keyedVals)) { + baggage.init(k, v); + } + return keyedVals; + }); +}; + +/** + * @param {Brand} brand must be a 'nat' brand, not checked + * @param {NatValue} [min] + */ +export const makeNatAmountShape = (brand, min) => + harden({ brand, value: min ? M.gte(min) : M.nat() }); + +/** + * @param {Brand} brand must be a 'nat' brand, not checked + * @param {Pattern} shape + */ +export const makeCopyBagAmountShape = (brand, shape) => + harden({ brand, value: shape }); + +// Added in https://github.com/Agoric/agoric-sdk/issues/7632 but not yet available on Mainnet +// Adapted from https://github.com/Agoric/agoric-sdk/blob/3ff341c28af26f7879a02b4a7a228b545d552e4a/packages/swingset-liveslots/src/collectionManager.js#L645 +const isCopyMap = (m) => matches(m, M.map()); +export const addAllToMap = (map, mapEntries) => { + if (typeof mapEntries[Symbol.iterator] !== 'function') { + if (Object.isFrozen(mapEntries) && isCopyMap(mapEntries)) { + mapEntries = getCopyMapEntries(mapEntries); + } else { + Fail`provided data source is not iterable: ${mapEntries}`; + } + } + for (const [key, value] of mapEntries) { + if (map.has(key)) { + map.set(key, value); + } else { + map.init(key, value, true); + } + } +}; diff --git a/agoric/contract/src/proposal/kread-committee-script.js b/agoric/contract/src/proposal/kread-committee-script.js index 2925d23ca..380182e62 100644 --- a/agoric/contract/src/proposal/kread-committee-script.js +++ b/agoric/contract/src/proposal/kread-committee-script.js @@ -31,7 +31,7 @@ export const defaultProposalBuilder = async ( committeeName, kreadCommitteeCharterRef: publishRef( install( - '../kreadCommitteeCharter.js', + '../kreadV1/kreadCommitteeCharter.js', '../bundles/bundle-kreadCommitteeCharter.js', { persist: true, diff --git a/agoric/contract/src/proposal/start-kread-proposal.js b/agoric/contract/src/proposal/start-kread-proposal.js index c42facd25..c1e4dcdef 100644 --- a/agoric/contract/src/proposal/start-kread-proposal.js +++ b/agoric/contract/src/proposal/start-kread-proposal.js @@ -326,7 +326,6 @@ export const startKread = async (powers, config) => { issuers: { KREAdCHARACTER: characterIssuer, KREAdITEM: itemIssuer }, brands: { KREAdCHARACTER: characterBrand, KREAdITEM: itemBrand }, } = await E(zoe).getTerms(instance); - trace('adding to boardAux'); await publishBrandInfo(chainStorage, board, characterBrand); await publishBrandInfo(chainStorage, board, itemBrand); diff --git a/agoric/contract/src/proposal/start-kread-script.js b/agoric/contract/src/proposal/start-kread-script.js index f6af89d72..267225d71 100644 --- a/agoric/contract/src/proposal/start-kread-script.js +++ b/agoric/contract/src/proposal/start-kread-script.js @@ -28,7 +28,7 @@ export const defaultProposalBuilder = async ( royaltyAddr, platformFeeAddr, kreadKitRef: publishRef( - install('../index.js', '../bundles/bundle-kreadKit.js', { + install('../kreadV1/index.js', '../bundles/bundle-kreadKit.js', { persist: true, }), ), diff --git a/agoric/contract/test/bootstrap.js b/agoric/contract/test/bootstrap.js index 6607e2650..8fad24f1a 100644 --- a/agoric/contract/test/bootstrap.js +++ b/agoric/contract/test/bootstrap.js @@ -51,7 +51,7 @@ export const bootstrapContext = async (conf = undefined) => { const timerService = buildManualTimer(); // Bundle and install contract - const contractBundle = await bundleSource('./src/index.js'); + const contractBundle = await bundleSource('./src/kreadV1/index.js'); const installation = await E(zoe).install(contractBundle); const privateArgs = { powers: { diff --git a/agoric/contract/test/flow.js b/agoric/contract/test/flow.js index d25a07298..4fa6d7b64 100644 --- a/agoric/contract/test/flow.js +++ b/agoric/contract/test/flow.js @@ -1,4 +1,4 @@ -import { errors } from '../src/errors.js'; +import { errors } from '../src/kreadV2/errors.js'; import { text } from './text.js'; import { defaultItems } from './items.js'; diff --git a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-governance.js b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-governance.js new file mode 100644 index 000000000..a29385cfd --- /dev/null +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-governance.js @@ -0,0 +1,38 @@ +import { AmountMath } from '@agoric/ertp'; +import { makeCopyBag } from '@agoric/store'; +import { E } from '@endo/far'; +import { flow } from '../flow.js'; + +export async function blockMethods(context) { + /** @type {Context} */ + const { publicFacet, governorFacets, contractAssets, purses, zoe } = context; + + await E(governorFacets.creatorFacet).setFilters(['mintCharacterNfts']); + + console.log(`TG `, (await E(purses.item).getCurrentAmount()).value.payload); + + const { want } = flow.mintCharacter.expected; + + const mintCharacterInvitation = await E( + publicFacet, + ).makeMintCharacterInvitation(); + const copyBagAmount = makeCopyBag(harden([[want, 1n]])); + const proposal = harden({ + want: { + Asset: AmountMath.make( + contractAssets.character.brand, + harden(copyBagAmount), + ), + }, + }); + + try { + await E(zoe).offer(mintCharacterInvitation, proposal); + } catch (error) { + assert.equal( + error.message, + 'not accepting offer with description "mintCharacterNfts"', + ); + throw error; + } +} diff --git a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-inventory.js b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-inventory.js new file mode 100644 index 000000000..c71a9fa12 --- /dev/null +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-inventory.js @@ -0,0 +1,915 @@ +import { addCharacterToContext, addItemToContext } from './utils.js'; +import { flow } from '../flow.js'; +import { E } from '@endo/eventual-send'; +import { makeCopyBag, mustMatch } from '@agoric/store'; +import { AmountMath } from '@agoric/ertp'; +import { errors } from '../../../src/kreadV2/errors.js'; + +export async function setupInventoryTests(context) { + await addCharacterToContext(context); + return context; +} + +const unequipOffer = async (context) => { + /** @type {Context} */ + const { publicFacet, contractAssets, purses, zoe } = context; + const { characterName } = flow.inventory; + const characterInventory = await E(publicFacet).getCharacterInventory( + characterName, + ); + const uncommonToLegendary = characterInventory.items + .map((i) => i[0]) + .filter((i) => i.rarity > 19)[0]; + + const legendaryCopyBagAmount = makeCopyBag( + harden([[uncommonToLegendary, 1n]]), + ); + const unequipInvitation = await E(publicFacet).makeUnequipInvitation(); + + const characterCopyBagAmount = makeCopyBag( + harden([ + [(await E(purses.character).getCurrentAmount()).value.payload[0][0], 1n], + ]), + ); + const characterKeyAmount = AmountMath.make( + contractAssets.character.brand, + characterCopyBagAmount, + ); + + const payment = { + CharacterKey1: await E(purses.character).withdraw(characterKeyAmount), + }; + + const wantedKeyId = + characterKeyAmount.value.payload[0][0].keyId === 1 ? 2 : 1; + + const proposal = harden({ + give: { + CharacterKey1: characterKeyAmount, + }, + want: { + Item: AmountMath.make(contractAssets.item.brand, legendaryCopyBagAmount), + CharacterKey2: AmountMath.make( + contractAssets.character.brand, + makeCopyBag( + harden([ + [ + { ...characterKeyAmount.value.payload[0][0], keyId: wantedKeyId }, + 1n, + ], + ]), + ), + ), + }, + }); + + const userSeat = await E(zoe).offer(unequipInvitation, proposal, payment); + const itemPayout = await E(userSeat).getPayout('Item'); + const characterPayout = await E(userSeat).getPayout('CharacterKey2'); + await E(purses.item).deposit(itemPayout); + await E(purses.character).deposit(characterPayout); +}; + +export async function unequipItem(context) { + /** @type {Context} */ + const { publicFacet, purses, getFromVStorage } = context; + const { characterName } = flow.inventory; + + await unequipOffer(context); + + assert.equal( + (await E(purses.item).getCurrentAmount()).value.payload.length, + 1, + 'New Item was not added to item purse', + ); + assert.equal( + (await E(purses.character).getCurrentAmount()).value.payload[0][0].name, + characterName, + 'CharacterKey was not returned to character purse', + ); + const characterInventory = await E(publicFacet).getCharacterInventory( + characterName, + ); + assert.equal( + characterInventory.items.length, + 2, + 'Character Inventory does not contain 2 items', + ); + + const vStorageInventory = getFromVStorage( + `kread.character.inventory-${characterName}`, + ); + assert.equal(vStorageInventory.length, 2); +} + +export async function unequipAlreadyUnequippedItem(context) { + /** @type {Context} */ + const { publicFacet, contractAssets, purses, zoe } = context; + const { characterName } = flow.inventory; + + const unequipInvitation = await E(publicFacet).makeUnequipInvitation(); + const characterCopyBagAmount = makeCopyBag( + harden([ + [(await E(purses.character).getCurrentAmount()).value.payload[0][0], 1n], + ]), + ); + const characterKeyAmount = AmountMath.make( + contractAssets.character.brand, + characterCopyBagAmount, + ); + + const wantedKeyId = + characterKeyAmount.value.payload[0][0].keyId === 1 ? 2 : 1; + + const proposal = harden({ + give: { + CharacterKey1: characterKeyAmount, + }, + want: { + Item: AmountMath.make( + contractAssets.item.brand, + makeCopyBag( + harden([ + [(await E(purses.item).getCurrentAmount()).value.payload[0][0], 1n], + ]), + ), + ), + CharacterKey2: AmountMath.make( + contractAssets.character.brand, + makeCopyBag( + harden([ + [ + { ...characterKeyAmount.value.payload[0][0], keyId: wantedKeyId }, + 1n, + ], + ]), + ), + ), + }, + }); + + const payment = { + CharacterKey1: await E(purses.character).withdraw(characterKeyAmount), + }; + + const userSeat = await E(zoe).offer(unequipInvitation, proposal, payment); + try { + await E(userSeat).getOfferResult(); + } catch (error) { + assert.equal(error.message, errors.rearrangeError); + + const characterPayout = await E(userSeat).getPayout('CharacterKey1'); + await E(purses.character).deposit(characterPayout); + assert.equal( + (await E(purses.character).getCurrentAmount()).value.payload[0][0].name, + characterName, + 'CharacterKey was not returned to character purse', + ); + throw error; + } +} + +export async function unequipWithWrongCharacter(context) { + /** @type {Context} */ + const { publicFacet, contractAssets, purses, zoe } = context; + const { characterName } = flow.inventory; + const initialItemsInPurse = (await E(purses.item).getCurrentAmount()).value + .payload.length; + + const characterInventory = await E(publicFacet).getCharacterInventory( + characterName, + ); + const noseItem = characterInventory.items + .map((i) => i[0]) + .filter((i) => i.rarity < 59)[0]; + const noseItemCopyBagAmount = makeCopyBag(harden([[noseItem, 1n]])); + const characterCopyBagAmount = makeCopyBag( + harden([ + [(await E(purses.character).getCurrentAmount()).value.payload[0][0], 1n], + ]), + ); + const characterKeyAmount = AmountMath.make( + contractAssets.character.brand, + characterCopyBagAmount, + ); + const unequipInvitation = await E(publicFacet).makeUnequipInvitation(); + + // incorrectly calculate wantedKeyId + const wantedKeyId = + characterKeyAmount.value.payload[0][0].keyId === 1 ? 1 : 2; + + const proposal = harden({ + give: { + CharacterKey1: characterKeyAmount, + }, + want: { + Item: AmountMath.make(contractAssets.item.brand, noseItemCopyBagAmount), + CharacterKey2: AmountMath.make( + contractAssets.character.brand, + makeCopyBag( + harden([ + [ + { ...characterKeyAmount.value.payload[0][0], keyId: wantedKeyId }, + 1n, + ], + ]), + ), + ), + }, + }); + + const payment = { + CharacterKey1: await E(purses.character).withdraw(characterKeyAmount), + }; + + const userSeat = await E(zoe).offer(unequipInvitation, proposal, payment); + try { + await E(userSeat).getOfferResult(); + } catch (error) { + assert.equal(error.message, 'Wanted Key and Inventory Key do not match'); + + const itemPayout = await E(userSeat).getPayout('Item'); + const characterPayout = await E(userSeat).getPayout('CharacterKey1'); + await E(purses.item).deposit(itemPayout); + await E(purses.character).deposit(characterPayout); + + assert.equal( + (await E(purses.item).getCurrentAmount()).value.payload.length, + initialItemsInPurse, + 'No new Item was not added to item purse', + ); + assert.equal( + (await E(purses.character).getCurrentAmount()).value.payload[0][0].name, + characterName, + 'CharacterKey was nnot returned to character purse', + ); + throw error; + } +} + +export async function equipItem(context) { + /** @type {Context} */ + const { publicFacet, contractAssets, purses, zoe, getFromVStorage } = context; + const { + characterName, + equip: { message }, + } = flow.inventory; + + let characterInventory = await E(publicFacet).getCharacterInventory( + characterName, + ); + const initialInventorySize = characterInventory.items.length; + + const item = (await E(purses.item).getCurrentAmount()).value.payload[0]; + + const itemCopyBagAmount = makeCopyBag(harden([[item[0], 1n]])); + const invitation = await E(publicFacet).makeEquipInvitation(); + const characterCopyBagAmount = makeCopyBag( + harden([ + [(await E(purses.character).getCurrentAmount()).value.payload[0][0], 1n], + ]), + ); + const characterKeyAmount = AmountMath.make( + contractAssets.character.brand, + characterCopyBagAmount, + ); + const itemAmount = AmountMath.make( + contractAssets.item.brand, + itemCopyBagAmount, + ); + + const payment = { + CharacterKey1: await E(purses.character).withdraw(characterKeyAmount), + Item: await E(purses.item).withdraw(itemAmount), + }; + + const wantedKeyId = + characterKeyAmount.value.payload[0][0].keyId === 1 ? 2 : 1; + + const proposal = harden({ + give: { + CharacterKey1: characterKeyAmount, + Item: AmountMath.make(contractAssets.item.brand, itemCopyBagAmount), + }, + want: { + CharacterKey2: AmountMath.make( + contractAssets.character.brand, + harden( + makeCopyBag([ + [ + { ...characterKeyAmount.value.payload[0][0], keyId: wantedKeyId }, + 1n, + ], + ]), + ), + ), + }, + }); + + const userSeat = await E(zoe).offer(invitation, proposal, payment); + const result = await E(userSeat).getOfferResult(); + assert.equal(result, message, 'Equip does not return success message'); + + const itemPayout = await E(userSeat).getPayout('Item'); + const characterPayout = await E(userSeat).getPayout('CharacterKey2'); + + await E(purses.item).deposit(itemPayout); + await E(purses.character).deposit(characterPayout); + assert.equal( + (await E(purses.item).getCurrentAmount()).value.payload.length, + 0, + 'Item was not removed from item purse', + ); + assert.equal( + (await E(purses.character).getCurrentAmount()).value.payload[0][0].name, + characterName, + 'CharacterKey was not returned to character purse', + ); + + characterInventory = await E(publicFacet).getCharacterInventory( + characterName, + ); + assert.equal( + characterInventory.items.length, + initialInventorySize + 1, + 'Character Inventory size did not increase by one item', + ); + + const vStorageInventory = getFromVStorage( + `kread.character.inventory-${characterName}`, + ); + mustMatch( + vStorageInventory.find( + ([inventoryItem, _]) => inventoryItem.category === item[0].category, + ), + item, + ); + // t.not(characterInventory.items.find(item => item.id === hairItem.id), undefined, "Character Inventory contains new item") +} + +export async function equipItemDuplicateCategory(context) { + /** @type {Context} */ + const { publicFacet, contractAssets, purses, zoe } = context; + const { characterName } = flow.inventory; + const inventory = await E(publicFacet).getCharacterInventory(characterName); + const existingCategory = inventory.items[0][0].category; + await addItemToContext(context, { + name: 'New item', + category: existingCategory, + thumbnail: '', + origin: 'Tempet', + description: '', + functional: false, + rarity: 65, + level: 0, + filtering: 0, + weight: 0, + sense: 0, + reserves: 0, + durability: 0, + date: {}, + colors: [''], + id: 10000, + image: '', + artistMetadata: '', + }); + const item = (await E(purses.item).getCurrentAmount()).value.payload.find( + (i) => i[0].category === existingCategory, + )[0]; + const itemCopyBagAmount = makeCopyBag(harden([[item, 1n]])); + const invitation = await E(publicFacet).makeEquipInvitation(); + const characterCopyBagAmount = makeCopyBag( + harden([ + [(await E(purses.character).getCurrentAmount()).value.payload[0][0], 1n], + ]), + ); + const characterKeyAmount = AmountMath.make( + contractAssets.character.brand, + characterCopyBagAmount, + ); + const hairAmount = AmountMath.make( + contractAssets.item.brand, + itemCopyBagAmount, + ); + + const wantedKeyId = + characterKeyAmount.value.payload[0][0].keyId === 1 ? 2 : 1; + + const proposal = harden({ + give: { + CharacterKey1: characterKeyAmount, + Item: hairAmount, + }, + want: { + CharacterKey2: AmountMath.make( + contractAssets.character.brand, + harden( + makeCopyBag([ + [ + { ...characterKeyAmount.value.payload[0][0], keyId: wantedKeyId }, + 1n, + ], + ]), + ), + ), + }, + }); + + const payment = { + CharacterKey1: await E(purses.character).withdraw(characterKeyAmount), + Item: await E(purses.item).withdraw(hairAmount), + }; + const userSeat = await E(zoe).offer(invitation, proposal, payment); + try { + await E(userSeat).getOfferResult(); + } catch (error) { + assert.equal(error.message, errors.duplicateCategoryInInventory); + + const characterPayout = await E(userSeat).getPayout('CharacterKey1'); + const itemPayout = await E(userSeat).getPayout('Item'); + + await E(purses.character).deposit(characterPayout); + await E(purses.item).deposit(itemPayout); + + const characterInventory = await E(publicFacet).getCharacterInventory( + characterName, + ); + assert.equal( + characterInventory.items.find( + (equippedItem) => equippedItem.id === item.id, + undefined, + ), + ); + assert.equal( + (await E(purses.character).getCurrentAmount()).value.payload[0][0].name, + characterName, + 'CharacterKey was not returned to character purse', + ); + assert.equal( + (await E(purses.item).getCurrentAmount()).value.payload[0][0], + item, + 'Item was not returned to item purse', + ); + throw error; + } +} + +export async function swapItems(context) { + /** @type {Context} */ + const { publicFacet, contractAssets, purses, zoe, getFromVStorage } = context; + const { characterName } = flow.inventory; + + const invitation = await E(publicFacet).makeItemSwapInvitation(); + const characterCopyBagAmount = makeCopyBag( + harden([ + [(await E(purses.character).getCurrentAmount()).value.payload[0][0], 1n], + ]), + ); + const characterKeyAmount = AmountMath.make( + contractAssets.character.brand, + characterCopyBagAmount, + ); + + const item = (await E(purses.item).getCurrentAmount()).value.payload + .map((i) => i[0]) + .find(({ rarity }) => rarity > 19); + + const itemGiveCopyBagAmount = makeCopyBag(harden([[item, 1n]])); + const itemGive = AmountMath.make( + contractAssets.item.brand, + itemGiveCopyBagAmount, + ); + + const characterInventory = await E(publicFacet).getCharacterInventory( + characterName, + ); + const itemWantValue = characterInventory.items + .map((i) => i[0]) + .find(({ rarity }) => rarity > 19); + const itemWantCopyBagAmount = makeCopyBag(harden([[itemWantValue, 1n]])); + const itemWant = AmountMath.make( + contractAssets.item.brand, + itemWantCopyBagAmount, + ); + + const wantedKeyId = + characterKeyAmount.value.payload[0][0].keyId === 1 ? 2 : 1; + + const proposal = harden({ + give: { + CharacterKey1: characterKeyAmount, + Item1: itemGive, + }, + want: { + CharacterKey2: AmountMath.make( + contractAssets.character.brand, + makeCopyBag( + harden([ + [ + { ...characterKeyAmount.value.payload[0][0], keyId: wantedKeyId }, + 1n, + ], + ]), + ), + ), + Item2: itemWant, + }, + }); + + const payment = { + CharacterKey1: await E(purses.character).withdraw(characterKeyAmount), + Item1: await E(purses.item).withdraw(itemGive), + }; + + const userSeat = await E(zoe).offer(invitation, proposal, payment); + + const itemPayout = await E(userSeat).getPayout('Item2'); + const characterPayout = await E(userSeat).getPayout('CharacterKey2'); + + await E(purses.item).deposit(itemPayout); + await E(purses.character).deposit(characterPayout); + + assert.equal( + (await E(purses.character).getCurrentAmount()).value.payload[0][0].name, + characterName, + 'CharacterKey was not returned to character purse', + ); + mustMatch( + (await E(purses.item).getCurrentAmount()).value.payload[0][0], + itemWantValue, + ); + + const updatedCharacterInventory = await E(publicFacet).getCharacterInventory( + characterName, + ); + mustMatch( + updatedCharacterInventory.items + .map((i) => i[0]) + .find((inventoryItem) => inventoryItem.rarity > 19), + itemGive.value.payload[0][0], + ); + assert.equal(updatedCharacterInventory.items.length, 3); + + const vStorageInventory = getFromVStorage( + `kread.character.inventory-${characterName}`, + ).filter( + ([inventoryItem, _]) => inventoryItem.category === itemWantValue.category, + ); + assert.equal(vStorageInventory.length, 1); + mustMatch(vStorageInventory[0][0], item); +} + +export async function swapItemsInitiallyEmpty(context) { + /** @type {Context} */ + const { publicFacet, contractAssets, purses, zoe, getFromVStorage } = context; + const { characterName } = flow.inventory; + + await unequipOffer(context); + + const invitation = await E(publicFacet).makeItemSwapInvitation(); + const characterCopyBagAmount = makeCopyBag( + harden([ + [(await E(purses.character).getCurrentAmount()).value.payload[0][0], 1n], + ]), + ); + const characterKeyAmount = AmountMath.make( + contractAssets.character.brand, + characterCopyBagAmount, + ); + + const otherItemGiveCopyBagAmount = makeCopyBag( + harden([ + [ + (await E(purses.item).getCurrentAmount()).value.payload.find( + ([item, _supply]) => item.rarity > 19, + )[0], + 1n, + ], + ]), + ); + const otherItemGive = AmountMath.make( + contractAssets.item.brand, + otherItemGiveCopyBagAmount, + ); + + const wantedKeyId = + characterKeyAmount.value.payload[0][0].keyId === 1 ? 2 : 1; + + const proposal = harden({ + give: { + CharacterKey1: characterKeyAmount, + Item1: otherItemGive, + }, + want: { + CharacterKey2: AmountMath.make( + contractAssets.character.brand, + makeCopyBag( + harden([ + [ + { ...characterKeyAmount.value.payload[0][0], keyId: wantedKeyId }, + 1n, + ], + ]), + ), + ), + Item2: AmountMath.make( + contractAssets.item.brand, + makeCopyBag(harden([])), + ), + }, + }); + + const payment = { + CharacterKey1: await E(purses.character).withdraw(characterKeyAmount), + Item1: await E(purses.item).withdraw(otherItemGive), + }; + + const userSeat = await E(zoe).offer(invitation, proposal, payment); + + const itemPayout = await E(userSeat).getPayout('Item2'); + const characterPayout = await E(userSeat).getPayout('CharacterKey2'); + + await E(purses.item).deposit(itemPayout); + await E(purses.character).deposit(characterPayout); + + assert.equal( + (await E(purses.character).getCurrentAmount()).value.payload[0][0].name, + characterName, + 'CharacterKey was not returned to character purse', + ); + assert.equal( + (await E(purses.item).getCurrentAmount()).value.payload.length, + 1, + ); + const updatedCharacterInventory = await E(publicFacet).getCharacterInventory( + characterName, + ); + mustMatch( + updatedCharacterInventory.items + .map((i) => i[0]) + .find((inventoryItem) => inventoryItem.rarity > 19), + otherItemGive.value.payload[0][0], + 'New Item was not added to inventory', + ); + + const vStorageInventory = getFromVStorage( + `kread.character.inventory-${characterName}`, + ); + + mustMatch( + vStorageInventory + .map((i) => i[0]) + .find((inventoryItem) => inventoryItem.rarity > 19), + otherItemGive.value.payload[0][0], + ); +} + +export async function swapItemsDifferentCategories(context) { + /** @type {Context} */ + const { publicFacet, contractAssets, purses, zoe } = context; + const { characterName } = flow.inventory; + + const invitation = await E(publicFacet).makeItemSwapInvitation(); + const characterCopyBagAmount = makeCopyBag( + harden([ + [(await E(purses.character).getCurrentAmount()).value.payload[0][0], 1n], + ]), + ); + const characterKeyAmount = AmountMath.make( + contractAssets.character.brand, + characterCopyBagAmount, + ); + + const itemGiveCopyBagAmount = makeCopyBag( + harden([ + [ + (await E(purses.item).getCurrentAmount()).value.payload.find( + ([item, _supply]) => item.rarity > 19, + )[0], + 1n, + ], + ]), + ); + + const itemGive = AmountMath.make( + contractAssets.item.brand, + itemGiveCopyBagAmount, + ); + + const characterInventory = await E(publicFacet).getCharacterInventory( + characterName, + ); + const itemWantValue = characterInventory.items + .map((i) => i[0]) + .find((item) => item.rarity < 20); + const clothingItemWantCopyBagAmount = makeCopyBag( + harden([[itemWantValue, 1n]]), + ); + const itemWant = AmountMath.make( + contractAssets.item.brand, + clothingItemWantCopyBagAmount, + ); + + const wantedKeyId = + characterKeyAmount.value.payload[0][0].keyId === 1 ? 2 : 1; + + const proposal = harden({ + give: { + CharacterKey1: characterKeyAmount, + Item1: itemGive, + }, + want: { + CharacterKey2: AmountMath.make( + contractAssets.character.brand, + makeCopyBag( + harden([ + [ + { ...characterKeyAmount.value.payload[0][0], keyId: wantedKeyId }, + 1n, + ], + ]), + ), + ), + Item2: itemWant, + }, + }); + + const payment = { + CharacterKey1: await E(purses.character).withdraw(characterKeyAmount), + Item1: await E(purses.item).withdraw(itemGive), + }; + + const userSeat = await E(zoe).offer(invitation, proposal, payment); + try { + await E(userSeat).getOfferResult(); + } catch (error) { + assert.equal(error.message, errors.duplicateCategoryInInventory); + + const itemPayout = await E(userSeat).getPayout('Item1'); + const characterPayout = await E(userSeat).getPayout('CharacterKey1'); + + await E(purses.item).deposit(itemPayout); + await E(purses.character).deposit(characterPayout); + assert.equal( + (await E(purses.character).getCurrentAmount()).value.payload[0][0].name, + characterName, + 'CharacterKey was not returned to character purse', + ); + mustMatch( + await E(purses.item).getCurrentAmount(), + itemGive, + 'Item not returned to purse', + ); + + const updatedInventory = await E(publicFacet).getCharacterInventory( + characterName, + ); + mustMatch( + updatedInventory.items.map((i) => i[0]).find((item) => item.rarity < 20), + itemWantValue, + 'Clothing item was not still in inventory', + ); + throw error; + } +} + +export async function unequipAll(context) { + const { publicFacet, contractAssets, purses, zoe, getFromVStorage } = context; + const { characterName } = flow.inventory; + + let characterInventory = await E(publicFacet).getCharacterInventory( + characterName, + ); + const initialInventorySize = characterInventory.items.length; + const initialPurseSize = (await E(purses.item).getCurrentAmount()).value + .payload.length; + + const invitation = await E(publicFacet).makeUnequipAllInvitation(); + const characterCopyBagAmount = makeCopyBag( + harden([ + [(await E(purses.character).getCurrentAmount()).value.payload[0][0], 1n], + ]), + ); + const characterKeyAmount = AmountMath.make( + contractAssets.character.brand, + characterCopyBagAmount, + ); + + const wantedKeyId = + characterKeyAmount.value.payload[0][0].keyId === 1 ? 2 : 1; + + const proposal = harden({ + give: { + CharacterKey1: characterKeyAmount, + }, + want: { + CharacterKey2: AmountMath.make( + contractAssets.character.brand, + makeCopyBag( + harden([ + [ + { ...characterKeyAmount.value.payload[0][0], keyId: wantedKeyId }, + 1n, + ], + ]), + ), + ), + }, + }); + + const payment = { + CharacterKey1: await E(purses.character).withdraw(characterKeyAmount), + }; + + const userSeat = await E(zoe).offer(invitation, proposal, payment); + + const itemPayout = await E(userSeat).getPayout('Item'); + const characterPayout = await E(userSeat).getPayout('CharacterKey2'); + + await E(purses.item).deposit(itemPayout); + await E(purses.character).deposit(characterPayout); + + assert.equal( + (await E(purses.character).getCurrentAmount()).value.payload[0][0].name, + characterName, + 'CharacterKey was not returned to character purse', + ); + assert.equal( + (await E(purses.item).getCurrentAmount()).value.payload.length, + initialInventorySize + initialPurseSize, + ); + + characterInventory = await E(publicFacet).getCharacterInventory( + characterName, + ); + assert.equal(characterInventory.items.length, 0); + + mustMatch( + getFromVStorage(`kread.character.inventory-${characterName}`), + harden([]), + ); +} + +export async function unequipAllEmptyInventory(context) { + const { publicFacet, contractAssets, purses, zoe, getFromVStorage } = context; + + const { characterName } = flow.inventory; + + const invitation = await E(publicFacet).makeUnequipAllInvitation(); + const characterCopyBagAmount = makeCopyBag( + harden([ + [(await E(purses.character).getCurrentAmount()).value.payload[0][0], 1n], + ]), + ); + const characterKeyAmount = AmountMath.make( + contractAssets.character.brand, + characterCopyBagAmount, + ); + + const wantedKeyId = + characterKeyAmount.value.payload[0][0].keyId === 1 ? 2 : 1; + + const proposal = harden({ + give: { + CharacterKey1: characterKeyAmount, + }, + want: { + CharacterKey2: AmountMath.make( + contractAssets.character.brand, + makeCopyBag( + harden([ + [ + { ...characterKeyAmount.value.payload[0][0], keyId: wantedKeyId }, + 1n, + ], + ]), + ), + ), + }, + }); + + const payment = { + CharacterKey1: await E(purses.character).withdraw(characterKeyAmount), + }; + + const userSeat = await E(zoe).offer(invitation, proposal, payment); + + const itemPayout = await E(userSeat).getPayout('Item'); + const characterPayout = await E(userSeat).getPayout('CharacterKey2'); + await E(purses.character).deposit(characterPayout); + + mustMatch( + (await E(contractAssets.item.issuer).getAmountOf(itemPayout)).value.payload, + harden([]), + 'Inventory was not empty', + ); + assert.equal( + (await E(purses.character).getCurrentAmount()).value.payload[0][0].name, + characterName, + 'CharacterKey was not returned to character purse', + ); + mustMatch( + getFromVStorage(`kread.character.inventory-${characterName}`), + harden([]), + ); +} diff --git a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market-metrics.js b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market-metrics.js new file mode 100644 index 000000000..0d9dcadd7 --- /dev/null +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market-metrics.js @@ -0,0 +1,298 @@ +import { makeKreadUser } from './make-bootstrap-users.js'; +import { AmountMath } from '@agoric/ertp'; +import { makeCopyBag } from '@agoric/store'; +import { E } from '@endo/far'; +import { flow } from '../flow.js'; + +async function sellCharacter(context, user, characterName, askingPrice) { + /** @type {Context} */ + const { publicFacet, contractAssets, zoe, paymentAsset } = context; + + const characterToSell = (await user.getCharacters()).find( + (c) => c.name === characterName, + ); + const copyBagAmount = makeCopyBag(harden([[characterToSell, 1n]])); + const characterToSellAmount = AmountMath.make( + contractAssets.character.brand, + copyBagAmount, + ); + const priceAmount = AmountMath.make(paymentAsset.brandMockIST, askingPrice); + + const sellCharacterInvitation = await E( + publicFacet, + ).makeSellCharacterInvitation(); + const proposal = harden({ + give: { Character: characterToSellAmount }, + want: { Price: priceAmount }, + }); + const payment = { + Character: await user.withdrawCharacters(characterToSellAmount), + }; + + const userSeat = await E(zoe).offer( + sellCharacterInvitation, + proposal, + payment, + ); + await E(userSeat).getOfferResult(); + + user.setMarketSeat(userSeat); + + return userSeat; +} + +async function buyCharacter(context, user, characterName, seller) { + /** @type {Context} */ + const { publicFacet, contractAssets, zoe } = context; + + const charactersForSale = await E(publicFacet).getCharactersForSale(); + const characterToBuy = charactersForSale.find( + ({ asset }) => asset.name === characterName, + ); + + const copyBagAmount = makeCopyBag(harden([[characterToBuy.asset, 1n]])); + const characterToBuyAmount = AmountMath.make( + contractAssets.character.brand, + copyBagAmount, + ); + const priceAmount = AmountMath.add( + AmountMath.add(characterToBuy.askingPrice, characterToBuy.royalty), + characterToBuy.platformFee, + ); + + const buyCharacterInvitation = await E( + publicFacet, + ).makeBuyCharacterInvitation(); + + const proposal = harden({ + give: { Price: priceAmount }, + want: { Character: characterToBuyAmount }, + }); + const payment = { Price: await user.withdrawPayment(priceAmount) }; + + const userSeat = await E(zoe).offer( + buyCharacterInvitation, + proposal, + payment, + ); + + const characterPayout = await E(userSeat).getPayout('Character'); + await user.depositCharacters(characterPayout); + + const pricePayout = await E(seller.getSeat().market).getPayout('Price'); + await seller.depositPayment(pricePayout); +} + +export async function setupMarketMetricsTests(context) { + const { purses, paymentAsset } = context; + const bob = makeKreadUser('bob', purses); + + const payout = paymentAsset.mintMockIST.mintPayment( + AmountMath.make(paymentAsset.brandMockIST, harden(100n)), + ); + bob.depositPayment(payout); + + return { + ...context, + users: { bob }, + }; +} + +export async function initialization(context) { + /** @type {Context} */ + const { publicFacet, getFromVStorage } = context; + + const metrics = await E(publicFacet).getMarketMetrics(); + + for (const collection of Object.values(metrics)) { + for (const value of Object.values(collection)) { + assert.equal(value, 0); + } + } + const vStorageCharacterMetrics = getFromVStorage('kread.market-metrics-character'); + for (const value of Object.values(vStorageCharacterMetrics)) { + assert.equal(value, 0); + } + + const vStorageItemMetrics = getFromVStorage('kread.market-metrics-item'); + for (const value of Object.values(vStorageItemMetrics)) { + assert.equal(value, 0); + } +} + +export async function collectionSize(context) { + /** @type {Context} */ + const { + publicFacet, + paymentAsset, + zoe, + users: { bob }, + getFromVStorage + } = context; + + const { give, offerArgs } = flow.mintCharacter.expected; + + const mintCharacterInvitation = await E( + publicFacet, + ).makeMintCharacterInvitation(); + const payment = { + Price: paymentAsset.mintMockIST.mintPayment( + AmountMath.make(paymentAsset.brandMockIST, harden(30000000n)), + ), + }; + const priceAmount = AmountMath.make(paymentAsset.brandMockIST, give.Price); + + const proposal = harden({ + give: { Price: priceAmount }, + }); + + const userSeat = await E(zoe).offer( + mintCharacterInvitation, + proposal, + payment, + offerArgs, + ); + await E(userSeat).getOfferResult(); + + const payout = await E(userSeat).getPayout('Asset'); + await bob.depositCharacters(payout); + const metrics = await E(publicFacet).getMarketMetrics(); + assert.equal(metrics.character.collectionSize, 1); + assert.equal(metrics.item.collectionSize, 3); + + const vStorageCharacterMetrics = getFromVStorage('kread.market-metrics-character'); + assert.equal(vStorageCharacterMetrics.collectionSize, 1) + + const vStorageItemMetrics = getFromVStorage('kread.market-metrics-item'); + assert.equal(vStorageItemMetrics.collectionSize, 3) +} + +export async function averageLevelsCharacter(context) { + /** @type {Context} */ + const { + publicFacet, + getFromVStorage, + users: { bob }, + } = context; + + const { + market: { + bob: { + give: { character: characterName }, + }, + }, + } = flow; + + const character = (await bob.getCharacters()).find( + (c) => c.name === characterName, + ); + + await sellCharacter(context, bob, characterName, 40n); + + const metrics = await E(publicFacet).getMarketMetrics(); + const characterLevel = await E(publicFacet).getCharacterLevel(characterName); + assert.equal(metrics.character.averageLevel, character.level); + assert.equal(metrics.character.marketplaceAverageLevel, characterLevel); + assert.equal(metrics.character.putForSaleCount, 1); + + const defaultItemsAverageLevel = 0; + + assert.equal(metrics.item.averageLevel, defaultItemsAverageLevel); + + const vStorageCharacterMetrics = getFromVStorage('kread.market-metrics-character'); + assert.equal(vStorageCharacterMetrics.averageLevel, character.level) + assert.equal(vStorageCharacterMetrics.marketplaceAverageLevel, characterLevel) + assert.equal(vStorageCharacterMetrics.putForSaleCount, 1) + + const vStorageItemMetrics = getFromVStorage('kread.market-metrics-item'); + assert.equal(vStorageItemMetrics.averageLevel, defaultItemsAverageLevel) +} + +export async function amountSoldCharacter(context) { + /** @type {Context} */ + const { + publicFacet, + users: { bob }, + getFromVStorage + } = context; + + const { + market: { + bob: { + give: { character: characterName }, + }, + }, + } = flow; + await buyCharacter(context, bob, characterName, bob); + + const metrics = await E(publicFacet).getMarketMetrics(); + + const character = (await bob.getCharacters()).find( + (c) => c.name === characterName, + ); + + assert.equal(metrics.character.averageLevel, character.level); + assert.equal(metrics.character.marketplaceAverageLevel, 0); + assert.equal(metrics.character.amountSold, 1); + assert.equal(metrics.character.latestSalePrice, 40); + + assert.equal(metrics.item.amountSold, 0); + assert.equal(metrics.item.latestSalePrice, 0); + + const vStorageCharacterMetrics = getFromVStorage('kread.market-metrics-character'); + assert.equal(vStorageCharacterMetrics.averageLevel, character.level) + assert.equal(vStorageCharacterMetrics.marketplaceAverageLevel, 0) + assert.equal(vStorageCharacterMetrics.amountSold, 1) + assert.equal(vStorageCharacterMetrics.latestSalePrice, 40) + + const vStorageItemMetrics = getFromVStorage('kread.market-metrics-item'); + assert.equal(vStorageItemMetrics.amountSold, 0) + assert.equal(vStorageItemMetrics.latestSalePrice, 0) +} + +export async function latestSalePriceCharacter(context) { + /** @type {Context} */ + const { + publicFacet, + users: { bob }, + getFromVStorage + } = context; + + const { + market: { + bob: { + give: { character: characterName }, + }, + }, + } = flow; + + await sellCharacter(context, bob, characterName, 20n); + + await buyCharacter(context, bob, flow.market.bob.give.character, bob); + + const metrics = await E(publicFacet).getMarketMetrics(); + + const character = (await bob.getCharacters()).find( + (c) => c.name === characterName, + ); + + assert.equal(metrics.character.averageLevel, character.level); + assert.equal(metrics.character.marketplaceAverageLevel, 0); + assert.equal(metrics.character.amountSold, 2); + assert.equal(metrics.character.putForSaleCount, 2); + assert.equal(metrics.character.latestSalePrice, 20); + + assert.equal(metrics.item.amountSold, 0); + assert.equal(metrics.item.latestSalePrice, 0); + + const vStorageCharacterMetrics = getFromVStorage('kread.market-metrics-character'); + assert.equal(vStorageCharacterMetrics.averageLevel, character.level) + assert.equal(vStorageCharacterMetrics.marketplaceAverageLevel, 0) + assert.equal(vStorageCharacterMetrics.amountSold, 2) + assert.equal(vStorageCharacterMetrics.putForSaleCount, 2) + assert.equal(vStorageCharacterMetrics.latestSalePrice, 20) + + const vStorageItemMetrics = getFromVStorage('kread.market-metrics-item'); + assert.equal(vStorageItemMetrics.amountSold, 0) + assert.equal(vStorageItemMetrics.latestSalePrice, 0) +} diff --git a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market.js b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market.js new file mode 100644 index 000000000..5be921f8a --- /dev/null +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market.js @@ -0,0 +1,902 @@ +import { AmountMath } from '@agoric/ertp'; +import { E } from '@endo/eventual-send'; +import { flow } from '../flow.js'; +import { makeCopyBag, mustMatch } from '@agoric/store'; +import { addCharacterToContext, addItemToContext } from './utils.js'; +import { makeKreadUser } from './make-bootstrap-users.js'; +import { errors } from '../../../src/kreadV2/errors.js'; +import { multiplyBy } from '@agoric/zoe/src/contractSupport/ratio.js'; +import { defaultItems } from '../items.js'; +import { TimerBrandShape } from '@agoric/time'; + +export async function setupMarketTests(context) { + await addCharacterToContext(context); + await addItemToContext(context, { + name: 'New item2', + category: 'hair', + thumbnail: '', + origin: 'Elphia', + description: '', + functional: false, + rarity: 65, + level: 0, + filtering: 0, + weight: 0, + sense: 0, + reserves: 0, + durability: 0, + date: {}, + colors: [''], + id: 10000, + image: '', + artistMetadata: '', + }); + + const { purses, contractAssets, paymentAsset } = context; + + const bob = makeKreadUser('bob', purses); + const alice = makeKreadUser('alice', { + character: await E(contractAssets.character.issuer).makeEmptyPurse(), + item: await E(contractAssets.item.issuer).makeEmptyPurse(), + payment: paymentAsset.issuerMockIST.makeEmptyPurse(), + }); + + const payout = await E(paymentAsset.mintMockIST).mintPayment( + AmountMath.make(paymentAsset.brandMockIST, harden(100n)), + ); + await alice.depositPayment(payout); + return { + ...context, + users: { bob, alice }, + }; +} + +export async function sellCharacter(context) { + /** @type {Context} */ + const { + publicFacet, + contractAssets, + zoe, + users: { bob }, + paymentAsset, + getFromVStorage, + royaltyRate, + platformFeeRate, + storageKit, + } = context; + const { + market: { + bob: { + give: { character }, + }, + }, + } = flow; + + const characterToSell = (await bob.getCharacters()).find( + (c) => c.name === character, + ); + + const copyBagAmount = makeCopyBag(harden([[characterToSell, 1n]])); + + const characterToSellAmount = AmountMath.make( + contractAssets.character.brand, + copyBagAmount, + ); + + const priceAmount = AmountMath.make(paymentAsset.brandMockIST, 40n); + + const sellCharacterInvitation = await E( + publicFacet, + ).makeSellCharacterInvitation(); + + const proposal = harden({ + give: { Character: characterToSellAmount }, + want: { Price: priceAmount }, + }); + + const payment = { + Character: await bob.withdrawCharacters(characterToSellAmount), + }; + + const userSeat = await E(zoe).offer( + sellCharacterInvitation, + proposal, + payment, + ); + + await E(userSeat).getOfferResult(); + + bob.setMarketSeat(userSeat); + + const charactersForSale = await E(publicFacet).getCharactersForSale(); + assert.equal( + charactersForSale.length, + 1, + 'Character was not added to market', + ); + assert.equal( + (await bob.getCharacters()).length, + 0, + "Character is still in bob's wallet", + ); + const vStorageCharacterMarket = getFromVStorage( + `kread.market-characters.character-${characterToSell.name}`, + ); + // Due to ocaps the remotable objects read from the storage node are not correct references and hence comparing + // them to the actual remote objects does not work. (brands in this case) + // That is why here we deconstruct the amounts and compare the actual values. + mustMatch( + vStorageCharacterMarket.asset, + harden({ + ...characterToSell, + date: { ...characterToSell.date, timerBrand: TimerBrandShape }, + }), + ); + assert.equal(vStorageCharacterMarket.id, characterToSell.name); + assert.equal(vStorageCharacterMarket.askingPrice.value, priceAmount.value); + assert.equal( + vStorageCharacterMarket.royalty.value, + multiplyBy(priceAmount, royaltyRate).value, + ); + assert.equal( + vStorageCharacterMarket.platformFee.value, + multiplyBy(priceAmount, platformFeeRate).value, + ); + assert.equal(vStorageCharacterMarket.isFirstSale, false); +} + +export async function buyCharacterOfferLessThanAskingPrice(context) { + /** @type {Context} */ + const { + publicFacet, + contractAssets, + zoe, + users: { alice }, + paymentAsset, + } = context; + const { + market: { + bob: { + give: { character }, + }, + }, + } = flow; + + const initialBalance = await alice.getPaymentBalance(); + const charactersForSale = await E(publicFacet).getCharactersForSale(); + const characterToBuy = charactersForSale.find( + (c) => c.asset.name === character, + ); + + const copyBagAmount = makeCopyBag(harden([[characterToBuy.asset, 1n]])); + const characterToBuyAmount = AmountMath.make( + contractAssets.character.brand, + copyBagAmount, + ); + + const priceAmount = AmountMath.make( + paymentAsset.brandMockIST, + characterToBuy.askingPrice.value / 2n, + ); + + const buyCharacterInvitation = await E( + publicFacet, + ).makeBuyCharacterInvitation(); + const proposal = harden({ + give: { Price: priceAmount }, + want: { Character: characterToBuyAmount }, + }); + const payment = { Price: await alice.withdrawPayment(priceAmount) }; + + const userSeat = await E(zoe).offer( + buyCharacterInvitation, + proposal, + payment, + ); + + try { + await E(userSeat).getOfferResult(); + } catch (error) { + assert.equal(error.message, errors.insufficientFunds); + const payout = await E(userSeat).getPayout('Price'); + await alice.depositPayment(payout); + assert.equal(await alice.getPaymentBalance(), initialBalance); + throw error; + } +} + +export async function buyCharacter(context) { + /** @type {Context} */ + const { + publicFacet, + contractAssets, + zoe, + users: { bob, alice }, + royaltyPurse, + platformFeePurse, + royaltyRate, + platformFeeRate, + getFromVStorage, + } = context; + + const { + market: { + bob: { + give: { character }, + }, + }, + } = flow; + + let charactersForSale = await E(publicFacet).getCharactersForSale(); + const characterToBuy = charactersForSale.find( + ({ asset }) => asset.name === character, + ); + + const royaltyPursePre = royaltyPurse.getCurrentAmount().value; + const platformFeePursePre = platformFeePurse.getCurrentAmount().value; + + const copyBagAmount = makeCopyBag(harden([[characterToBuy.asset, 1n]])); + const characterToBuyAmount = AmountMath.make( + contractAssets.character.brand, + copyBagAmount, + ); + const priceAmount = AmountMath.add( + AmountMath.add(characterToBuy.askingPrice, characterToBuy.royalty), + characterToBuy.platformFee, + ); + + const buyCharacterInvitation = await E( + publicFacet, + ).makeBuyCharacterInvitation(); + const proposal = harden({ + give: { Price: priceAmount }, + want: { Character: characterToBuyAmount }, + }); + const payment = { Price: await alice.withdrawPayment(priceAmount) }; + + const userSeat = await E(zoe).offer( + buyCharacterInvitation, + proposal, + payment, + ); + + await E(userSeat).getOfferResult(); + + const characterPayout = await E(userSeat).getPayout('Character'); + await alice.depositCharacters(characterPayout); + assert.equal( + (await alice.getCharacters()).length, + 1, + "Character is not in alice's wallet", + ); + + const bobsPayout = await E(bob.getSeat().market).getPayout('Price'); + await bob.depositPayment(bobsPayout); + assert.equal( + await bob.getPaymentBalance(), + 40n, + 'Bob did not received payout', + ); + + charactersForSale = await E(publicFacet).getCharactersForSale(); + assert.equal( + charactersForSale.length, + 0, + 'Character was not removed from market', + ); + + assert.equal( + royaltyPurse.getCurrentAmount().value, + royaltyPursePre + multiplyBy(characterToBuy.askingPrice, royaltyRate).value, + ); + assert.equal( + platformFeePurse.getCurrentAmount().value, + platformFeePursePre + + multiplyBy(characterToBuy.askingPrice, platformFeeRate).value, + ); + try { + getFromVStorage( + `kread.market-characters.character-${characterToBuy.asset.name}`, + ); + } catch (error) { + assert.equal( + error.message, + `no data for "kread.market-characters.character-${characterToBuy.asset.name}"`, + ); + } +} + +export async function buyCharacterNotOnMarket(context) { + /** @type {Context} */ + const { + publicFacet, + contractAssets, + zoe, + users: { alice }, + paymentAsset, + } = context; + const { + market: { + bob: { + give: { character }, + }, + }, + } = flow; + const initialBalance = await alice.getPaymentBalance(); + + const characterToBuy = (await alice.getCharacters()).find( + (c) => c.name === character, + ); + const copyBagAmount = makeCopyBag(harden([[characterToBuy, 1n]])); + const characterToBuyAmount = AmountMath.make( + contractAssets.character.brand, + copyBagAmount, + ); + const priceAmount = AmountMath.make(paymentAsset.brandMockIST, 5n); + + const buyCharacterInvitation = await E( + publicFacet, + ).makeBuyCharacterInvitation(); + const proposal = harden({ + give: { Price: priceAmount }, + want: { Character: characterToBuyAmount }, + }); + const payment = { Price: await alice.withdrawPayment(priceAmount) }; + + const userSeat = await E(zoe).offer( + buyCharacterInvitation, + proposal, + payment, + ); + + try { + await E(userSeat).getOfferResult(); + } catch (error) { + assert.equal(error.message, 'Character not found'); + const payout = await E(userSeat).getPayout('Price'); + alice.depositPayment(payout); + assert.equal(alice.getPaymentBalance(), initialBalance); + } +} + +export async function sellItem(context) { + /** @type {Context} */ + const { + publicFacet, + contractAssets, + zoe, + users: { bob }, + paymentAsset, + getFromVStorage, + royaltyRate, + platformFeeRate, + } = context; + + const itemToSellValue = (await bob.getItems()).find( + (item) => item.category === 'hair', + ); + const itemToSellCopyBagAmount = makeCopyBag(harden([[itemToSellValue, 1n]])); + const itemToSell = AmountMath.make( + contractAssets.item.brand, + itemToSellCopyBagAmount, + ); + const priceAmount = AmountMath.make(paymentAsset.brandMockIST, 5n); + + const sellItemInvitation = await E(publicFacet).makeSellItemInvitation(); + const proposal = harden({ + give: { Item: itemToSell }, + want: { Price: priceAmount }, + }); + + const payment = { Item: await bob.withdrawItems(itemToSell) }; + + const userSeat = await E(zoe).offer(sellItemInvitation, proposal, payment); + await E(userSeat).getOfferResult(); + + bob.setMarketSeat(userSeat); + + const itemsForSale = await E(publicFacet).getItemsForSale(); + assert.equal(itemsForSale.length, 1, 'Item is was not added to market'); + + assert.equal( + (await bob.getItems()).length, + 0, + "Item is still in bob's wallet", + ); + const vStorageItemMarket = getFromVStorage(`kread.market-items.item-0`); // this is the first item on sale so we know it will be assigned id 0 + // Due to ocaps the remotable objects read from the storage node are not correct references and hence comparing + // them to the actual remote objects does not work. (brands in this case) + // That is why here we deconstruct the amounts and compare the actual values. + mustMatch(vStorageItemMarket.asset, itemToSellValue); + assert.equal(vStorageItemMarket.id, 0); + assert.equal(vStorageItemMarket.askingPrice.value, priceAmount.value); + assert.equal( + vStorageItemMarket.royalty.value, + multiplyBy(priceAmount, royaltyRate).value, + ); + assert.equal( + vStorageItemMarket.platformFee.value, + multiplyBy(priceAmount, platformFeeRate).value, + ); + assert.equal(vStorageItemMarket.isFirstSale, false); +} + +export async function buyItemOfferLessThanAskingPrice(context) { + /** @type {Context} */ + const { + publicFacet, + contractAssets, + zoe, + users: { alice }, + paymentAsset, + } = context; + + const initialBalance = await alice.getPaymentBalance(); + + const itemsForSale = await E(publicFacet).getItemsForSale(); + const itemToBuy = itemsForSale.find( + (itemEntry) => itemEntry.asset.category === 'hair', + ); + const itemToBuyCopyBagAmount = makeCopyBag(harden([[itemToBuy.asset, 1n]])); + const itemToBuyAmount = AmountMath.make( + contractAssets.item.brand, + itemToBuyCopyBagAmount, + ); + + const priceAmount = AmountMath.make( + paymentAsset.brandMockIST, + itemToBuy.askingPrice.value / 2n, + ); + + const buyItemInvitation = await E(publicFacet).makeBuyItemInvitation(); + const proposal = harden({ + give: { Price: priceAmount }, + want: { Item: itemToBuyAmount }, + }); + const payment = { Price: await alice.withdrawPayment(priceAmount) }; + const offerArgs = { entryId: itemToBuy.id }; + + const userSeat = await E(zoe).offer( + buyItemInvitation, + proposal, + payment, + offerArgs, + ); + try { + await E(userSeat).getOfferResult(); + } catch (error) { + assert.equal(errors.insufficientFunds); + const payout = await E(userSeat).getPayout('Price'); + await alice.depositPayment(payout); + assert.equal(await alice.getPaymentBalance(), initialBalance); + } +} + +export async function buyItem(context) { + /** @type {Context} */ + const { + publicFacet, + contractAssets, + zoe, + users: { bob, alice }, + getFromVStorage, + } = context; + + let itemsForSale = await E(publicFacet).getItemsForSale(); + const itemToBuy = itemsForSale.find( + (itemEntry) => itemEntry.asset.category === 'hair', + ); + const itemToBuyCopyBagAmount = makeCopyBag(harden([[itemToBuy.asset, 1n]])); + const itemToBuyAmount = AmountMath.make( + contractAssets.item.brand, + itemToBuyCopyBagAmount, + ); + + const priceAmount = AmountMath.add( + AmountMath.add(itemToBuy.askingPrice, itemToBuy.royalty), + itemToBuy.platformFee, + ); + const buyItemInvitation = await E(publicFacet).makeBuyItemInvitation(); + + const proposal = harden({ + give: { Price: priceAmount }, + want: { Item: itemToBuyAmount }, + }); + const payment = { Price: await alice.withdrawPayment(priceAmount) }; + const offerArgs = { entryId: itemToBuy.id }; + + const userSeat = await E(zoe).offer( + buyItemInvitation, + proposal, + payment, + offerArgs, + ); + await E(userSeat).getOfferResult(); + // assert.equal(result.itemMarket.length, 0, 'Offer returns empty market entry'); + + const itemPayout = await E(userSeat).getPayout('Item'); + await alice.depositItems(itemPayout); + assert.equal( + (await alice.getItems()).length, + 1, + "Item is not in alice's wallet", + ); + + const bobsPayout = await E(bob.getSeat().market).getPayout('Price'); + await bob.depositPayment(bobsPayout); + assert.equal( + await bob.getPaymentBalance(), + 45n, + 'Bob did not received payout', + ); + + itemsForSale = await E(publicFacet).getItemsForSale(); + assert.equal(itemsForSale.length, 0, 'Item was not removed from market'); + + try { + getFromVStorage(`kread.market-items.item-0`); + } catch (error) { + assert.equal(error.message, `no data for "kread.market-items.item-0"`); + } +} + +export async function buyItemNotOnMarket(context) { + /** @type {Context} */ + const { + publicFacet, + contractAssets, + zoe, + users: { alice }, + paymentAsset, + } = context; + const initialBalance = await alice.getPaymentBalance(); + + const itemToBuy = (await alice.getItems()).find( + (item) => item.category === 'hair', + ); + const itemToBuyCopyBagAmount = makeCopyBag(harden([[itemToBuy, 1n]])); + const itemToBuyAmount = AmountMath.make( + contractAssets.item.brand, + itemToBuyCopyBagAmount, + ); + const priceAmount = AmountMath.make(paymentAsset.brandMockIST, 5n); + + const buyItemInvitation = await E(publicFacet).makeBuyItemInvitation(); + const proposal = harden({ + give: { Price: priceAmount }, + want: { Item: itemToBuyAmount }, + }); + const payment = { Price: await alice.withdrawPayment(priceAmount) }; + const offerArgs = { entryId: 66 }; + const userSeat = await E(zoe).offer( + buyItemInvitation, + proposal, + payment, + offerArgs, + ); + + try { + await E(userSeat).getOfferResult(); + } catch (error) { + assert.equal(error.message, 'Item not found'); + const payout = await E(userSeat).getPayout('Price'); + await alice.depositPayment(payout); + assert.equal(await alice.getPaymentBalance(), initialBalance); + } +} + +export async function buyCharacterOfferMoreThanAskingPrice(context) { + /** @type {Context} */ + const { + publicFacet, + contractAssets, + zoe, + users: { bob, alice }, + paymentAsset, + } = context; + const { + market: { + bob: { + give: { character }, + }, + }, + } = flow; + + // alice puts character for sale + const balanceAlice = await alice.getPaymentBalance(); + const characterToSell = (await alice.getCharacters()).find( + (c) => c.name === character, + ); + + const characterToSellCopyBagAmount = makeCopyBag( + harden([[characterToSell, 1n]]), + ); + const characterToSellAmount = AmountMath.make( + contractAssets.character.brand, + characterToSellCopyBagAmount, + ); + let priceAmount = AmountMath.make(paymentAsset.brandMockIST, 10n); + + const sellCharacterInvitation = await E( + publicFacet, + ).makeSellCharacterInvitation(); + let proposal = harden({ + give: { Character: characterToSellAmount }, + want: { Price: priceAmount }, + }); + let payment = { + Character: await alice.withdrawCharacters(characterToSellAmount), + }; + + let userSeat = await E(zoe).offer(sellCharacterInvitation, proposal, payment); + await E(userSeat).getOfferResult(); + + alice.setMarketSeat(userSeat); + + let charactersForSale = await E(publicFacet).getCharactersForSale(); + assert.equal( + charactersForSale.length, + 1, + 'Character was not added to market', + ); + + // bob buys character + const characterToBuy = charactersForSale.find( + ({ asset }) => asset.name === character, + ); + const characterToBuyCopyBagAmount = makeCopyBag( + harden([[characterToBuy.asset, 1n]]), + ); + const characterToBuyAmount = AmountMath.make( + contractAssets.character.brand, + characterToBuyCopyBagAmount, + ); + priceAmount = AmountMath.make( + paymentAsset.brandMockIST, + (characterToBuy.askingPrice.value + + characterToBuy.royalty.value + + characterToBuy.platformFee.value) * + 2n, + ); + + const buyCharacterInvitation = await E( + publicFacet, + ).makeBuyCharacterInvitation(); + proposal = harden({ + give: { Price: priceAmount }, + want: { Character: characterToBuyAmount }, + }); + payment = { Price: await bob.withdrawPayment(priceAmount) }; + + userSeat = await E(zoe).offer(buyCharacterInvitation, proposal, payment); + await E(userSeat).getOfferResult(); + + const characterPayout = await E(userSeat).getPayout('Character'); + await bob.depositCharacters(characterPayout); + assert.equal( + (await bob.getCharacters()).length, + 1, + "Character is not in bob's wallet", + ); + + const alicesPayout = await E(alice.getSeat().market).getPayout('Price'); + + await alice.depositPayment(alicesPayout); + assert.equal( + await alice.getPaymentBalance(), + balanceAlice + + priceAmount.value - + characterToBuy.royalty.value - + characterToBuy.platformFee.value, + 'Alice did not received payout', + ); + + charactersForSale = await E(publicFacet).getCharactersForSale(); + assert.equal( + charactersForSale.length, + 0, + 'Character was not removed from market', + ); +} + +export async function buyItemOfferMoreThanAskingPrice(context) { + /** @type {Context} */ + const { + publicFacet, + contractAssets, + zoe, + users: { bob, alice }, + paymentAsset, + } = context; + + // alice puts item for sale + const aliceBalance = await alice.getPaymentBalance(); + const itemToSell = (await alice.getItems()).find( + (item) => item.category === 'hair', + ); + const itemToSellCopyBagAmount = makeCopyBag(harden([[itemToSell, 1n]])); + const itemToSellAmount = AmountMath.make( + contractAssets.item.brand, + itemToSellCopyBagAmount, + ); + let priceAmount = AmountMath.make(paymentAsset.brandMockIST, 5n); + + const sellItemInvitation = await E(publicFacet).makeSellItemInvitation(); + let proposal = harden({ + give: { Item: itemToSellAmount }, + want: { Price: priceAmount }, + }); + let payment = { Item: await alice.withdrawItems(itemToSellAmount) }; + + let userSeat = await E(zoe).offer(sellItemInvitation, proposal, payment); + await E(userSeat).getOfferResult(); + + alice.setMarketSeat(userSeat); + + let itemsForSale = await E(publicFacet).getItemsForSale(); + assert.equal(itemsForSale.length, 1, 'Item was not added to market'); + + // bob attempts to buy item + const itemToBuy = itemsForSale.find(({ asset }) => asset.category === 'hair'); + const itemToBuyCopyBagAmount = makeCopyBag(harden([[itemToBuy.asset, 1n]])); + const itemToBuyAmount = AmountMath.make( + contractAssets.item.brand, + itemToBuyCopyBagAmount, + ); + priceAmount = AmountMath.make( + paymentAsset.brandMockIST, + itemToBuy.askingPrice.value * 2n, + ); + + const buyItemInvitation = await E(publicFacet).makeBuyItemInvitation(); + proposal = harden({ + give: { Price: priceAmount }, + want: { Item: itemToBuyAmount }, + }); + payment = { Price: await bob.withdrawPayment(priceAmount) }; + const offerArgs = { entryId: itemToBuy.id }; + + userSeat = await E(zoe).offer( + buyItemInvitation, + proposal, + payment, + offerArgs, + ); + await E(userSeat).getOfferResult(); + + const itemPayout = await E(userSeat).getPayout('Item'); + await bob.depositItems(itemPayout); + assert.equal((await bob.getItems()).length, 1, "Item is not in bob's wallet"); + + const alicesPayout = await E(alice.getSeat().market).getPayout('Price'); + await alice.depositPayment(alicesPayout); + assert.equal( + await alice.getPaymentBalance(), + aliceBalance + + priceAmount.value - + itemToBuy.royalty.value - + itemToBuy.platformFee.value, + 'Alice did not receive payout', + ); + + itemsForSale = await E(publicFacet).getItemsForSale(); + assert.equal(itemsForSale.length, 0, 'Item was not removed to market'); +} + +export async function internalSellItemBatch(context) { + /** @type {Context} */ + const { + publicFacet, + creatorFacet, + paymentAsset, + getFromVStorage, + royaltyRate, + platformFeeRate, + } = context; + + const itemCollection = Object.values(defaultItems).map((item) => [item, 3n]); + const itemsToSell = harden(itemCollection); + const priceAmount = AmountMath.make(paymentAsset.brandMockIST, 5n); + + await E(creatorFacet).publishItemCollection(priceAmount, itemsToSell); + + const itemsForSale = await E(publicFacet).getItemsForSale(); + assert.equal(itemsForSale.length, 27, 'Item was not added to market'); + + itemCollection.forEach((value, i) => { + [0, 1, 2].forEach((j) => { + const id = 2 + j + 3 * i; + const vStorageItemMarket = getFromVStorage( + `kread.market-items.item-${id}`, + ); + mustMatch(vStorageItemMarket.asset, value[0]); + assert.equal(vStorageItemMarket.id, id); + assert.equal(vStorageItemMarket.askingPrice.value, priceAmount.value); + assert.equal( + vStorageItemMarket.royalty.value, + multiplyBy(priceAmount, royaltyRate).value, + ); + assert.equal( + vStorageItemMarket.platformFee.value, + multiplyBy(priceAmount, platformFeeRate).value, + ); + assert.equal(vStorageItemMarket.isFirstSale, true); + }); + }); +} + +export async function buyBatchSoldItem(context) { + /** @type {Context} */ + const { + publicFacet, + contractAssets, + zoe, + users: { bob }, + paymentAsset, + getFromVStorage, + } = context; + + const itemsForSale = await E(publicFacet).getItemsForSale(); + const itemToBuy = itemsForSale.find( + ({ asset }) => asset.category === 'background', + ); + const itemToBuyCopyBagAmount = makeCopyBag(harden([[itemToBuy.asset, 1n]])); + + const itemToBuyAmount = AmountMath.make( + contractAssets.item.brand, + itemToBuyCopyBagAmount, + ); + const priceAmount = AmountMath.make( + paymentAsset.brandMockIST, + itemToBuy.askingPrice.value + + itemToBuy.royalty.value + + itemToBuy.platformFee.value, + ); + + const buyItemInvitation = await E(publicFacet).makeBuyItemInvitation(); + const proposal = harden({ + give: { Price: priceAmount }, + want: { Item: itemToBuyAmount }, + }); + + const initialItemCountBob = (await bob.getItems()).length; + const payment = { + Price: await bob.withdrawPayment(priceAmount), + }; + + const offerArgs = { entryId: itemToBuy.id }; + + const userSeat = await E(zoe).offer( + buyItemInvitation, + proposal, + payment, + offerArgs, + ); + + await E(userSeat).getOfferResult(); + const itemPayout = await E(userSeat).getPayout('Item'); + await bob.depositItems(itemPayout); + + assert.equal( + (await bob.getItems()).length, + initialItemCountBob + 1, + "Item is not in bob's wallet", + ); + + const itemsForSaleAfter = await E(publicFacet).getItemsForSale(); + + assert.equal( + itemsForSaleAfter.length, + itemsForSale.length - 1, + 'Item was not removed from the market', + ); + + try { + getFromVStorage(`kread.market-items.item-${itemToBuy.id}`); + } catch (error) { + assert.equal( + error.message, + `no data for "kread.market-items.item-${itemToBuy.id}"`, + ); + } +} diff --git a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-mint.js b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-mint.js new file mode 100644 index 000000000..6e7e8d5de --- /dev/null +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-mint.js @@ -0,0 +1,615 @@ +import { E } from '@endo/eventual-send'; +import { AmountMath } from '@agoric/ertp'; +import { flow } from '../flow.js'; +import { makeKreadUser } from './make-bootstrap-users.js'; +import { makeCopyBag, mustMatch } from '@agoric/store'; +import { TimestampRecordShape } from '@agoric/time'; + +export async function setupMintTests(context) { + const { contractAssets, paymentAsset } = context; + + const alice = makeKreadUser('alice', { + character: await E(contractAssets.character.issuer).makeEmptyPurse(), + item: await E(contractAssets.item.issuer).makeEmptyPurse(), + payment: paymentAsset.issuerMockIST.makeEmptyPurse(), + }); + + const payout = await E(paymentAsset.mintMockIST).mintPayment( + AmountMath.make(paymentAsset.brandMockIST, harden(100000000000n)), + ); + await alice.depositPayment(payout); + return { + ...context, + users: { alice }, + }; +} +export async function mintTooLongName(context) { + /** @type {Context} */ + const { + publicFacet, + paymentAsset, + users: { alice }, + zoe, + } = context; + const { message, give, offerArgs } = flow.mintCharacter.invalidName1; + + const mintCharacterInvitation = await E( + publicFacet, + ).makeMintCharacterInvitation(); + const priceAmount = AmountMath.make(paymentAsset.brandMockIST, give.Price); + + const payment = { Price: await alice.withdrawPayment(priceAmount) }; + + const proposal = harden({ + give: { Price: priceAmount }, + }); + + const userSeat = await E(zoe).offer( + mintCharacterInvitation, + proposal, + payment, + offerArgs, + ); + + try { + await E(userSeat).getOfferResult(); + } catch (error) { + assert.equal(error.message, message); + const characters = await E(publicFacet).getCharacters(); + assert.equal( + characters.length, + 0, + 'New character was not added to contract registry due to mint error', + ); + + await alice.depositPayment(await E(userSeat)).getPayout('Price'); + } +} + +export async function mintInvalidCharsInname(context) { + /** @type {Context} */ + const { + publicFacet, + users: { alice }, + paymentAsset, + zoe, + } = context; + const { message, give, offerArgs } = flow.mintCharacter.invalidName2; + + const mintCharacterInvitation = await E( + publicFacet, + ).makeMintCharacterInvitation(); + + const priceAmount = AmountMath.make(paymentAsset.brandMockIST, give.Price); + const payment = { Price: await alice.withdrawPayment(priceAmount) }; + + const proposal = harden({ + give: { + Price: priceAmount, + }, + }); + + const userSeat = await E(zoe).offer( + mintCharacterInvitation, + proposal, + payment, + offerArgs, + ); + + try { + await E(userSeat).getOfferResult(); + } catch (error) { + assert.equal(error.message, message); + + const characters = await E(publicFacet).getCharacters(); + assert.equal( + characters.length, + 0, + 'New character was not added to contract registry due to mint error', + ); + throw error; + } +} + +export async function mintForbiddenName(context) { + /** @type {Context} */ + const { + publicFacet, + users: { alice }, + paymentAsset, + zoe, + } = context; + const { message, give, offerArgs } = flow.mintCharacter.invalidName3; + + const mintCharacterInvitation = await E( + publicFacet, + ).makeMintCharacterInvitation(); + + const priceAmount = AmountMath.make(paymentAsset.brandMockIST, give.Price); + const payment = { Price: await alice.withdrawPayment(priceAmount) }; + + const proposal = harden({ + give: { + Price: priceAmount, + }, + }); + + const userSeat = await E(zoe).offer( + mintCharacterInvitation, + proposal, + payment, + offerArgs, + ); + + try { + await E(userSeat).getOfferResult(); + } catch (error) { + assert.equal(error.message, message); + + const characters = await E(publicFacet).getCharacters(); + assert.equal( + characters.length, + 0, + 'New character was not added to contract registry due to mint error', + ); + throw error; + } +} + +export async function mintExpectedFlow(context) { + /** @type {Context} */ + const { + publicFacet, + purses, + paymentAsset, + users: { alice }, + zoe, + getFromVStorage, + } = context; + const { message, give, offerArgs } = flow.mintCharacter.expected; + + const mintCharacterInvitation = await E( + publicFacet, + ).makeMintCharacterInvitation(); + const priceAmount = AmountMath.make(paymentAsset.brandMockIST, give.Price); + + const proposal = harden({ + give: { Price: priceAmount }, + }); + + const payment = { Price: await alice.withdrawPayment(priceAmount) }; + + const userSeat = await E(zoe).offer( + mintCharacterInvitation, + proposal, + payment, + offerArgs, + ); + + const result = await E(userSeat).getOfferResult(); + assert.equal(result, message, 'Offer returns success message'); + + const characters = await E(publicFacet).getCharacters(); + assert.equal( + characters[0].name, + offerArgs.name, + 'New character is added to contract registry', + ); + const vStorageCharacterData = getFromVStorage('kread.character'); + assert.equal(vStorageCharacterData.name, offerArgs.name); + const vStorageInventoryData = getFromVStorage( + `kread.character.inventory-${offerArgs.name}`, + ); + + assert.equal(vStorageInventoryData.length, 3); + + const payout = await E(userSeat).getPayout('Asset'); + await E(purses.character).deposit(payout); + assert.equal( + (await E(purses.character).getCurrentAmount()).value.payload[0][0].name, + offerArgs.name, + 'New Character was added to character purse successfully', + ); +} + +export async function mintFeeTooLow(context) { + /** @type {Context} */ + const { + publicFacet, + paymentAsset, + users: { alice }, + zoe, + } = context; + const { offerArgs, message, give } = flow.mintCharacter.feeTooLow; + + const mintCharacterInvitation = await E( + publicFacet, + ).makeMintCharacterInvitation(); + const priceAmount = AmountMath.make(paymentAsset.brandMockIST, give.Price); + + const proposal = harden({ + give: { Price: priceAmount }, + }); + + const payment = { Price: await alice.withdrawPayment(priceAmount) }; + + const userSeat = await E(zoe).offer( + mintCharacterInvitation, + proposal, + payment, + offerArgs, + ); + + try { + await E(userSeat).getOfferResult(); + } catch (error) { + assert.equal(error.message, message); + + const characters = await E(publicFacet).getCharacters(); + assert.equal( + characters.length, + 1, + 'New character was not added to contract registry due to mint error', + ); + throw error; + } +} + +export async function mintNoOfferArgs(context) { + /** @type {Context} */ + const { + publicFacet, + paymentAsset, + users: { alice }, + zoe, + } = context; + const { give, offerArgs, message } = flow.mintCharacter.noArgs; + + const mintCharacterInvitation = await E( + publicFacet, + ).makeMintCharacterInvitation(); + const priceAmount = AmountMath.make(paymentAsset.brandMockIST, give.Price); + + const proposal = harden({ + give: { Price: priceAmount }, + }); + + const payment = { Price: await alice.withdrawPayment(priceAmount) }; + + const userSeat = await E(zoe).offer( + mintCharacterInvitation, + proposal, + payment, + offerArgs, + ); + + try { + await E(userSeat).getOfferResult(); + } catch (error) { + assert.equal(error.message, message); + + const characters = await E(publicFacet).getCharacters(); + assert.equal( + characters.length, + 1, + 'New character was not added to contract registry due to mint error', + ); + throw error; + } +} + +export async function mintDuplicateName(context) { + /** @type {Context} */ + const { + publicFacet, + paymentAsset, + users: { alice }, + zoe, + } = context; + const { offerArgs, message, give } = flow.mintCharacter.duplicateName; + + const mintCharacterInvitation = await E( + publicFacet, + ).makeMintCharacterInvitation(); + const priceAmount = AmountMath.make(paymentAsset.brandMockIST, give.Price); + + const proposal = harden({ + give: { Price: priceAmount }, + }); + + const payment = { Price: await alice.withdrawPayment(priceAmount) }; + + const userSeat = await E(zoe).offer( + mintCharacterInvitation, + proposal, + payment, + offerArgs, + ); + + try { + await E(userSeat).getOfferResult(); + } catch (error) { + assert.equal(error.message, message); + + const characters = await E(publicFacet).getCharacters(); + assert.equal( + characters.length, + 1, + 'New character was not added to contract registry due to mint error', + ); + throw error; + } +} + +export async function mintNoName(context) { + /** @type {Context} */ + const { + publicFacet, + paymentAsset, + users: { alice }, + zoe, + } = context; + + const { offerArgs, give, message } = flow.mintCharacter.noName; + + const mintCharacterInvitation = await E( + publicFacet, + ).makeMintCharacterInvitation(); + const priceAmount = AmountMath.make(paymentAsset.brandMockIST, give.Price); + + const proposal = harden({ + give: { Price: priceAmount }, + }); + + const payment = { Price: await alice.withdrawPayment(priceAmount) }; + const userSeat = await E(zoe).offer( + mintCharacterInvitation, + proposal, + payment, + offerArgs, + ); + + try { + await E(userSeat).getOfferResult(); + } catch (error) { + assert.equal(error.message, message); + + const characters = await E(publicFacet).getCharacters(); + assert.equal( + characters.length, + 1, + 'New character was not added to contract registry due to mint error', + ); + throw error; + } +} + +export async function mintNoCharactersAvailable(context) { + /** @type {Context} */ + const { + publicFacet, + paymentAsset, + users: { alice }, + zoe, + } = context; + const { offerArgs, message, give } = flow.mintCharacter.noAvailability; + + const mintCharacterInvitation = await E( + publicFacet, + ).makeMintCharacterInvitation(); + const priceAmount = AmountMath.make(paymentAsset.brandMockIST, give.Price); + + const proposal = harden({ + give: { Price: priceAmount }, + }); + + const payment = { Price: await alice.withdrawPayment(priceAmount) }; + + const userSeat = await E(zoe).offer( + mintCharacterInvitation, + proposal, + payment, + offerArgs, + ); + + try { + await E(userSeat).getOfferResult(); + } catch (error) { + assert.equal(error.message, message); + + const characters = await E(publicFacet).getCharacters(); + assert.equal( + characters.length, + 1, + 'New character was not added to contract registry due to mint error', + ); + throw error; + } +} + +export async function mintInventoryCheck(context) { + /** @type {Context} */ + const { publicFacet, getFromVStorage } = context; + const { offerArgs } = flow.mintCharacter.expected; + + const characterInventory = await E(publicFacet).getCharacterInventory( + offerArgs.name, + ); + + const mappedInventory = characterInventory.items.map((i) => i[0]); + + assert.equal( + mappedInventory.length, + 3, + 'New character inventory does not contain 3 items', + ); + + assert.equal( + new Set(mappedInventory.map((i) => i.category)).size, + 3, + 'Two or more items have the same category', + ); + + const vStorageInventoryItems = getFromVStorage( + `kread.character.inventory-${offerArgs.name}`, + ); + mustMatch(vStorageInventoryItems, characterInventory.items); +} + +export async function mintItemExpectedFlow(context) { + /** @type {Context} */ + const { creatorFacet, contractAssets, purses, zoe, getFromVStorage } = + context; + const { want, message } = flow.mintItem.expected; + + const mintItemInvitation = await E(creatorFacet).makeMintItemInvitation(); + const proposal = harden({ + want: { + Item: AmountMath.make( + contractAssets.item.brand, + makeCopyBag(harden([[want, 1n]])), + ), + }, + }); + + const userSeat = await E(zoe).offer(mintItemInvitation, proposal); + + const result = await E(userSeat).getOfferResult(); + assert.equal(result, message, 'Offer does not return success message'); + + const payout = await E(userSeat).getPayout('Asset'); + await E(purses.item).deposit(payout); + assert.equal( + (await E(purses.item).getCurrentAmount()).value.payload[0][0].name, + want.name, + 'New Item was not added to character purse', + ); + + const vStorageItem = getFromVStorage('kread.item'); + mustMatch( + harden(Object.keys(vStorageItem).sort()), + harden(['history', 'id', 'item']), + ); + + mustMatch(vStorageItem.item, want); + assert.equal(vStorageItem.id, 3); + mustMatch( + vStorageItem.history, + harden([ + { + type: 'mint', + data: want, + timestamp: TimestampRecordShape, + }, + ]), + ); +} + +export async function mintSameItemSFT(context) { + /** @type {Context} */ + const { creatorFacet, contractAssets, purses, zoe, getFromVStorage } = context; + const { want, message } = flow.mintItem.expected; + + const mintItemInvitation = await E(creatorFacet).makeMintItemInvitation(); + const proposal = harden({ + want: { + Item: AmountMath.make( + contractAssets.item.brand, + makeCopyBag(harden([[want, 1n]])), + ), + }, + }); + + const userSeat = await E(zoe).offer(mintItemInvitation, proposal); + + const result = await E(userSeat).getOfferResult(); + assert.equal(result, message, 'Offer does not return success message'); + + const payout = await E(userSeat).getPayout('Asset'); + await E(purses.item).deposit(payout); + assert.equal( + (await E(purses.item).getCurrentAmount()).value.payload[0][0].name, + want.name, + 'New Item was not added to character purse', + ); + + assert.equal( + (await E(purses.item).getCurrentAmount()).value.payload[0][1], + 2n, + 'Supply of item not increased to 2', + ); + assert.equal( + (await E(purses.item).getCurrentAmount()).value.payload.length, + 1, + ); + + const vStorageItem = getFromVStorage('kread.item'); + mustMatch(vStorageItem.item, want) + assert.equal(vStorageItem.id, 4) + +} + +export async function mintItemMultipleFlow(context) { + /** @type {Context} */ + const { creatorFacet, contractAssets, purses, zoe } = context; + const { want, message } = flow.mintItem.multiple; + + const mintItemInvitation = await E(creatorFacet).makeMintItemInvitation(); + const proposal = harden({ + want: { + Item: AmountMath.make( + contractAssets.item.brand, + makeCopyBag(harden([[want, 2n]])), + ), + }, + }); + + const userSeat = await E(zoe).offer(mintItemInvitation, proposal); + + const result = await E(userSeat).getOfferResult(); + assert.equal(result, message, 'Offer does not return success message'); + + const payout = await E(userSeat).getPayout('Asset'); + + await E(purses.item).deposit(payout); + + const totalItems = ( + await E(purses.item).getCurrentAmount() + ).value.payload.reduce((acc, [_item, supply]) => { + return acc + supply; + }, 0n); + assert.equal(totalItems, 4n); + assert.equal( + (await E(purses.item).getCurrentAmount()).value.payload.length, + 2, + ); +} + +export async function mintItemMultipleDifferentFlow(context) { + /** @type {Context} */ + const { creatorFacet, contractAssets, purses, zoe } = context; + const { want, message } = flow.mintItem.multipleUnique; + + const mintItemInvitation = await E(creatorFacet).makeMintItemInvitation(); + const proposal = harden({ + want: { + Item: AmountMath.make( + contractAssets.item.brand, + makeCopyBag(harden(want.map((item) => [item, 1n]))), + ), + }, + }); + + const userSeat = await E(zoe).offer(mintItemInvitation, proposal); + + const result = await E(userSeat).getOfferResult(); + assert.equal(result, message, 'Offer does not return success message'); + + const payout = await E(userSeat).getPayout('Asset'); + + await E(purses.item).deposit(payout); + assert.equal( + (await E(purses.item).getCurrentAmount()).value.payload.length, + 4, + ); +} diff --git a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-null-upgrade.js b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-null-upgrade.js new file mode 100644 index 000000000..3f1287af0 --- /dev/null +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-null-upgrade.js @@ -0,0 +1,35 @@ +import { AmountMath } from '@agoric/ertp'; +import { flow } from '../flow'; +import { E } from '@endo/eventual-send'; + +export async function mint(context) { + /** @type {Context} */ + const { publicFacet, purses, paymentAsset, zoe, } = context; + + const payout = await E(paymentAsset.mintMockIST).mintPayment( + AmountMath.make(paymentAsset.brandMockIST, harden(100000000000n)), + ); + await E(purses.payment).deposit(payout); + + const { give, offerArgs } = flow.mintCharacter.expected; + + const mintCharacterInvitation = await E( + publicFacet, + ).makeMintCharacterInvitation(); + const priceAmount = AmountMath.make(paymentAsset.brandMockIST, give.Price); + + const proposal = harden({ + give: { Price: priceAmount }, + }); + + const payment = { Price: await E(purses.payment).withdraw(priceAmount) }; + + const userSeat = await E(zoe).offer( + mintCharacterInvitation, + proposal, + payment, + offerArgs, + ); + + const result = await E(userSeat).getOfferResult(); +} diff --git a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js new file mode 100644 index 000000000..77a8bc197 --- /dev/null +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js @@ -0,0 +1,519 @@ +import { Far, deeplyFulfilled, makeMarshal } from '@endo/marshal'; +import { E } from '@endo/eventual-send'; +import { makeFakeStorageKit } from '@agoric/internal/src/storage-test-utils'; +import { buildManualTimer } from '@agoric/swingset-vat/tools/manual-timer'; +import { makeFakeBoard } from '@agoric/vats/tools/board-utils'; +import { makeTracer } from '@agoric/internal'; +import { Fail, NonNullish } from '@agoric/assert'; +import { makeIssuerKit } from '@agoric/ertp'; +import { CONTRACT_ELECTORATE, ParamTypes } from '@agoric/governance'; +import { makePromiseKit } from '@endo/promise-kit'; +import { defaultCharacters } from '../../characters.js'; +import { defaultItems } from '../../items.js'; +import { + setupMintTests, + mintTooLongName, + mintInvalidCharsInname, + mintDuplicateName, + mintExpectedFlow, + mintFeeTooLow, + mintForbiddenName, + mintInventoryCheck, + mintItemExpectedFlow, + mintItemMultipleDifferentFlow, + mintNoName, + mintNoCharactersAvailable, + mintSameItemSFT, + mintItemMultipleFlow, + mintNoOfferArgs, +} from './bootstrap-mint.js'; +import { + setupMarketTests, + sellCharacter, + buyCharacterOfferLessThanAskingPrice, + buyCharacter, + buyCharacterNotOnMarket, + sellItem, + buyItemOfferLessThanAskingPrice, + buyItem, + buyItemNotOnMarket, + buyCharacterOfferMoreThanAskingPrice, + buyItemOfferMoreThanAskingPrice, + internalSellItemBatch, + buyBatchSoldItem, +} from './bootstrap-market.js'; +import { + unequipAll, + unequipAllEmptyInventory, + unequipAlreadyUnequippedItem, + unequipWithWrongCharacter, + unequipItem, + swapItems, + swapItemsDifferentCategories, + swapItemsInitiallyEmpty, + setupInventoryTests, + equipItemDuplicateCategory, + equipItem, +} from './bootstrap-inventory.js'; +import { + initialization, + setupMarketMetricsTests, + collectionSize, + averageLevelsCharacter, + amountSoldCharacter, + latestSalePriceCharacter, +} from './bootstrap-market-metrics.js'; +import { blockMethods } from './bootstrap-governance.js'; +import { makeRatio } from '@agoric/zoe/src/contractSupport/index.js'; +import { + setupUpgradeTests, + testFunctionalityAfterUpgrade, + testFunctionalityBeforeUpgrade, +} from './bootstrap-upgrade-v2.js'; +import { unmarshalFromVstorage } from '@agoric/vats/tools/board-utils'; +import { mint } from './bootstrap-null-upgrade.js'; + +const trace = makeTracer('kreadBootUpgrade'); + +const kreadV1BundleName = 'kreadV1'; +const kreadV2BundleName = 'kreadV2'; + +export const buildRootObject = async () => { + let vatAdmin; + let initialPoserInvitation; + let electorateInvitationAmount; + let governedInstance; + /** @type {Context} */ + let context; + /** @type {import('@agoric/governance/tools/puppetContractGovernor').PuppetContractGovernorKit} */ + let governorFacets; + + const storageKit = makeFakeStorageKit('kread'); + const timer = buildManualTimer(); + const clock = await E(timer).getClock(); + const marshaller = makeFakeBoard().getReadonlyMarshaller(); + const installations = {}; + + /** @type {PromiseKit} */ + const { promise: zoe, ...zoePK } = makePromiseKit(); + const { promise: committeeCreator, ...ccPK } = makePromiseKit(); + + const { + mint: mintMockIST, + issuer: issuerMockIST, + brand: brandMockIST, + } = makeIssuerKit('IST-mock', 'nat'); + + const royaltyPurse = issuerMockIST.makeEmptyPurse(); + const platformFeePurse = issuerMockIST.makeEmptyPurse(); + const royaltyDepositFacet = royaltyPurse.getDepositFacet(); + const platformFeeDepositFacet = platformFeePurse.getDepositFacet(); + + const royaltyRate = { + numerator: 20n, + denominator: 100n, + }; + const platformFeeRate = { + numerator: 20n, + denominator: 100n, + }; + + const mintRoyaltyRate = { + numerator: 85n, + denominator: 100n, + }; + const mintPlatformFeeRate = { + numerator: 15n, + denominator: 100n, + }; + + const kreadTerms = { + mintFee: 30000000n, + royaltyRate, + platformFeeRate, + mintRoyaltyRate, + mintPlatformFeeRate, + royaltyDepositFacet, + platformFeeDepositFacet, + assetNames: harden({ + character: 'KREAdCHARACTER', + item: 'KREAdITEM', + }), + minUncommonRating: 20, + }; + + const staticPrivateArgs = { + powers: { + storageNode: storageKit.rootNode, + marshaller, + }, + clock, + seed: 303, + }; + + const pathSegmentPattern = /^[a-zA-Z0-9_-]{1,100}$/; + /** @type {(name: string) => void} */ + const assertPathSegment = (name) => { + pathSegmentPattern.test(name) || + Fail`Path segment names must consist of 1 to 100 characters limited to ASCII alphanumerics, underscores, and/or dashes: ${name}`; + }; + + const sanitizePathSegment = (name) => { + const candidate = name.replace(/[ ,]/g, '_'); + assertPathSegment(candidate); + return candidate; + }; + /** + * Reads the data from the vstorage at the given path + * Will throw an error if path doesnt exist + * @param {string} path + * @param {number} index index of the desired value in a deserialized stream cell (-1 for latest) + */ + const getFromVStorage = (path, index = -1) => { + const { fromCapData } = makeMarshal( + undefined, + (slot, iface) => { + return Far((iface ?? '').replace(/^Alleged: /, ''), {}); + }, + { + serializeBodyFormat: 'smallcaps', + }, + ); + return unmarshalFromVstorage(storageKit.data, path, fromCapData, index); + }; + + const committeeName = 'KREAd Committee'; + + return Far('root', { + /** + * + * @param {{ + * vatAdmin: ReturnType, + * zoe: ReturnType, + * }} vats + * @param {*} devices + */ + bootstrap: async (vats, devices) => { + vatAdmin = await E(vats.vatAdmin).createVatAdminService(devices.vatAdmin); + const { zoeService } = await E(vats.zoe).buildZoe( + vatAdmin, + undefined, + 'zcf', + ); + zoePK.resolve(zoeService); + trace('Starting!'); + + const v1BundleId = await E(vatAdmin).getBundleIDByName(kreadV1BundleName); + trace('Got kread bundle'); + + v1BundleId || Fail('Bundle id must not be empty.'); + installations.kreadV1 = await E(zoe).installBundleID(v1BundleId); + trace('Installed kread bundle'); + + installations.puppetContractGovernor = await E(zoe).installBundleID( + await E(vatAdmin).getBundleIDByName('puppetContractGovernor'), + ); + trace('Installed governor bundle'); + + installations.committee = await E(zoe).installBundleID( + await E(vatAdmin).getBundleIDByName('committee'), + ); + trace('Installed committee bundle'); + + const committeeStartResult = await E(zoe).startInstance( + installations.committee, + harden({}), + { + committeeName: 'KREAd Committee', + committeeSize: 2, + }, + { + storageNode: storageKit.rootNode + .makeChildNode('committees') + .makeChildNode(sanitizePathSegment(committeeName)), + marshaller, + }, + ); + + trace('Started committee'); + + ccPK.resolve(committeeStartResult.creatorFacet); + + const poserInvitationP = E(committeeCreator).getPoserInvitation(); + [initialPoserInvitation, electorateInvitationAmount] = await Promise.all([ + poserInvitationP, + E(E(zoe).getInvitationIssuer()).getAmountOf(poserInvitationP), + ]); + }, + buildV1: async ( + baseCharacters = defaultCharacters, + baseItems = defaultItems, + ) => { + trace(`BOOT buildV1 start`); + + const governorTerms = await deeplyFulfilled( + harden({ + timer, + governedContractInstallation: NonNullish(installations.kreadV1), + governed: { + terms: { + ...kreadTerms, + governedParams: { + [CONTRACT_ELECTORATE]: { + type: ParamTypes.INVITATION, + value: electorateInvitationAmount, + }, + }, + }, + issuerKeywordRecord: { Money: issuerMockIST }, + label: 'KREAd', + }, + }), + ); + + trace('got governorTerms'); + + trace(`BOOT buildV1 startInstance`); + governorFacets = await E(zoe).startInstance( + NonNullish(installations.puppetContractGovernor), + {}, + governorTerms, + { + economicCommitteeCreatorFacet: committeeCreator, + governed: { + ...staticPrivateArgs, + initialPoserInvitation, + }, + }, + ); + trace('BOOT buildV1 started instance'); + governedInstance = E(governorFacets.creatorFacet).getInstance(); + + const publicFacet = await E(governorFacets.creatorFacet).getPublicFacet(); + const creatorFacet = await E( + governorFacets.creatorFacet, + ).getCreatorFacet(); + + await E(creatorFacet).initializeBaseAssets(baseCharacters, baseItems); + await E(creatorFacet).initializeCharacterNamesEntries(); + await E(creatorFacet).initializeMetrics(); + + const terms = await E(zoe).getTerms(governedInstance); + const { + issuers: { KREAdCHARACTER: characterIssuer, KREAdITEM: itemIssuer }, + brands: { KREAdCHARACTER: characterBrand, KREAdITEM: itemBrand }, + } = terms; + + context = { + storageKit, + getFromVStorage, + contractAssets: { + character: { issuer: characterIssuer, brand: characterBrand }, + item: { issuer: itemIssuer, brand: itemBrand }, + }, + purses: { + character: await E(characterIssuer).makeEmptyPurse(), + item: await E(itemIssuer).makeEmptyPurse(), + payment: issuerMockIST.makeEmptyPurse(), + }, + paymentAsset: { + mintMockIST, + issuerMockIST, + brandMockIST, + }, + publicFacet, + creatorFacet, + governorFacets, + zoe, + royaltyPurse, + platformFeePurse, + royaltyRate: makeRatio( + royaltyRate.numerator, + brandMockIST, + royaltyRate.denominator, + brandMockIST, + ), + platformFeeRate: makeRatio( + platformFeeRate.numerator, + brandMockIST, + platformFeeRate.denominator, + brandMockIST, + ), + }; + }, + setupMintTests: async () => { + context = await setupMintTests(context); + }, + mintTooLongName: async () => { + await mintTooLongName(context); + }, + mintInvalidCharsInname: async () => { + await mintInvalidCharsInname(context); + }, + mintForbiddenName: async () => { + await mintForbiddenName(context); + }, + mintExpectedFlow: async () => { + await mintExpectedFlow(context); + }, + mintFeeTooLow: async () => { + await mintFeeTooLow(context); + }, + mintDuplicateName: async () => { + await mintDuplicateName(context); + }, + mintNoOfferArgs: async () => { + await mintNoOfferArgs(context); + }, + mintNoName: async () => { + await mintNoName(context); + }, + mintNoCharactersAvailable: async () => { + await mintNoCharactersAvailable(context); + }, + mintInventoryCheck: async () => { + await mintInventoryCheck(context); + }, + mintItemExpectedFlow: async () => { + await mintItemExpectedFlow(context); + }, + mintSameItemSFT: async () => { + await mintSameItemSFT(context); + }, + mintItemMultipleFlow: async () => { + await mintItemMultipleFlow(context); + }, + mintItemMultipleDifferentFlow: async () => { + await mintItemMultipleDifferentFlow(context); + }, + setupMarketTests: async () => { + context = await setupMarketTests(context); + }, + sellCharacter: async () => { + await sellCharacter(context); + }, + buyCharacterOfferLessThanAskingPrice: async () => { + await buyCharacterOfferLessThanAskingPrice(context); + }, + buyCharacter: async () => { + await buyCharacter(context); + }, + buyCharacterNotOnMarket: async () => { + await buyCharacterNotOnMarket(context); + }, + sellItem: async () => { + await sellItem(context); + }, + buyItemOfferLessThanAskingPrice: async () => { + await buyItemOfferLessThanAskingPrice(context); + }, + buyItem: async () => { + await buyItem(context); + }, + buyItemNotOnMarket: async () => { + await buyItemNotOnMarket(context); + }, + buyCharacterOfferMoreThanAskingPrice: async () => { + await buyCharacterOfferMoreThanAskingPrice(context); + }, + buyItemOfferMoreThanAskingPrice: async () => { + await buyItemOfferMoreThanAskingPrice(context); + }, + internalSellItemBatch: async () => { + await internalSellItemBatch(context); + }, + buyBatchSoldItem: async () => { + await buyBatchSoldItem(context); + }, + setupInventoryTests: async () => { + context = await setupInventoryTests(context); + }, + unequipItem: async () => { + await unequipItem(context); + }, + unequipAlreadyUnequippedItem: async () => { + await unequipAlreadyUnequippedItem(context); + }, + unequipWithWrongCharacter: async () => { + await unequipWithWrongCharacter(context); + }, + equipItem: async () => { + await equipItem(context); + }, + equipItemDuplicateCategory: async () => { + await equipItemDuplicateCategory(context); + }, + swapItems: async () => { + await swapItems(context); + }, + swapItemsDifferentCategories: async () => { + await swapItemsDifferentCategories(context); + }, + swapItemsInitiallyEmpty: async () => { + await swapItemsInitiallyEmpty(context); + }, + unequipAll: async () => { + await unequipAll(context); + }, + unequipAllEmptyInventory: async () => { + await unequipAllEmptyInventory(context); + }, + setupMarketMetricsTests: async () => { + context = await setupMarketMetricsTests(context); + }, + initialization: async () => { + await initialization(context); + }, + collectionSize: async () => { + await collectionSize(context); + }, + averageLevelsCharacter: async () => { + await averageLevelsCharacter(context); + }, + amountSoldCharacter: async () => { + await amountSoldCharacter(context); + }, + latestSalePriceCharacter: async () => { + await latestSalePriceCharacter(context); + }, + blockMethods: async () => { + await blockMethods(context); + }, + nullUpgrade: async () => { + trace('start null upgrade'); + const bundleId = await E(vatAdmin).getBundleIDByName(kreadV1BundleName); + + const kreadAdminFacet = await E( + governorFacets.creatorFacet, + ).getAdminFacet(); + const upgradeResult = await E(kreadAdminFacet).upgradeContract(bundleId, { + ...staticPrivateArgs, + initialPoserInvitation, + }); + assert.equal(upgradeResult.incarnationNumber, 1); + trace('null upgrade completed'); + }, + upgradeV2: async () => { + const bundleId = await E(vatAdmin).getBundleIDByName(kreadV2BundleName); + const kreadAdminFacet = await E( + governorFacets.creatorFacet, + ).getAdminFacet(); + const upgradeResult = await E(kreadAdminFacet).upgradeContract(bundleId, { + ...staticPrivateArgs, + initialPoserInvitation, + }); + }, + testFunctionalityBeforeUpgrade: async () => { + await testFunctionalityBeforeUpgrade(context); + }, + setupUpgradeTests: async () => { + context = await setupUpgradeTests(context); + }, + testFunctionalityAfterUpgrade: async () => { + await testFunctionalityAfterUpgrade(context); + }, + mint: async () => { + await mint(context) + } + }); +}; diff --git a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgrade-v2.js b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgrade-v2.js new file mode 100644 index 000000000..1c4257231 --- /dev/null +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgrade-v2.js @@ -0,0 +1,87 @@ +import { AmountMath } from '@agoric/ertp'; +import { E } from '@endo/eventual-send'; + +export async function setupUpgradeTests(context) { + /** @type {Context} */ + const { paymentAsset, purses } = context; + const payment = await E(paymentAsset.mintMockIST).mintPayment( + AmountMath.make(paymentAsset.brandMockIST, 100000000n), + ); + await E(purses.payment).deposit(payment); + return { + ...context, + purses, + }; +} + +export async function testFunctionalityBeforeUpgrade(context) { + /** @type {Context} */ + const { publicFacet, zoe, purses, paymentAsset, getFromVStorage } = context; + const mintCharacterInvitation = await E( + publicFacet, + ).makeMintCharacterInvitation(); + + const proposal = { + give: { Price: AmountMath.make(paymentAsset.brandMockIST, 30000000n) }, + }; + const payment = { + Price: await E(purses.payment).withdraw( + AmountMath.make(paymentAsset.brandMockIST, 30000000n), + ), + }; + const offerArgs = { name: 'example' }; + + const userSeat = await E(zoe).offer( + mintCharacterInvitation, + proposal, + payment, + offerArgs, + ); + + const result = await E(userSeat).getOfferResult(); + const characterInventory = await E(publicFacet).getCharacterInventory( + 'example', + ); + assert.equal(characterInventory.items.length, 3); + + const vStorageInventory = getFromVStorage( + `kread.character.inventory-example`, + ); + assert.equal(vStorageInventory.length, 3); +} + +export async function testFunctionalityAfterUpgrade(context) { + /** @type {Context} */ + const { publicFacet, zoe, purses, paymentAsset, getFromVStorage } = context; + const mintCharacterInvitation = await E( + publicFacet, + ).makeMintCharacterInvitation(); + + const proposal = { + give: { Price: AmountMath.make(paymentAsset.brandMockIST, 30000000n) }, + }; + const payment = { + Price: await E(purses.payment).withdraw( + AmountMath.make(paymentAsset.brandMockIST, 30000000n), + ), + }; + const offerArgs = { name: 'example2' }; + + const userSeat = await E(zoe).offer( + mintCharacterInvitation, + proposal, + payment, + offerArgs, + ); + + const result = await E(userSeat).getOfferResult(); + const characterInventory = await E(publicFacet).getCharacterInventory( + 'example2', + ); + assert.equal(characterInventory.items.length, 4); + + const vStorageInventory = getFromVStorage( + `kread.character.inventory-example2`, + ); + assert.equal(vStorageInventory.length, 4); +} diff --git a/agoric/contract/test/swingsetTests/bootstrap/make-bootstrap-users.js b/agoric/contract/test/swingsetTests/bootstrap/make-bootstrap-users.js new file mode 100644 index 000000000..c6c4782e5 --- /dev/null +++ b/agoric/contract/test/swingsetTests/bootstrap/make-bootstrap-users.js @@ -0,0 +1,57 @@ +import { E } from '@endo/eventual-send'; + +/** + * Creates a user with its own purses and seats + * includes methods for checking balances and + * depositing/widthdrawing assets + * + * This ensures a user only has access to its own + * purses and seats, which better resembles the + * real flow + * + * @param {string} name + * @param {Purses} purses + * @returns {KreadUser} + */ +export const makeKreadUser = (name, purses) => { + const seat = { + market: undefined, + }; + const getItems = async () => + (await E(purses.item).getCurrentAmount()).value.payload.map( + ([value, _]) => value, + ); + const getCharacters = async () => + (await E(purses.character).getCurrentAmount()).value.payload.map( + ([value, _]) => value, + ); + const getPaymentBalance = async () => + (await E(purses.payment).getCurrentAmount()).value; + + const depositItems = async (items) => E(purses.item).deposit(items); + const depositCharacters = async (characters) => + E(purses.character).deposit(characters); + const depositPayment = async (payment) => E(purses.payment).deposit(payment); + + const withdrawItems = async (items) => E(purses.item).withdraw(items); + const withdrawCharacters = async (characters) => + E(purses.character).withdraw(characters); + const withdrawPayment = async (payment) => + E(purses.payment).withdraw(payment); + + return { + name, + purses, + getItems, + getCharacters, + getPaymentBalance, + depositItems, + depositCharacters, + depositPayment, + withdrawItems, + withdrawCharacters, + withdrawPayment, + getSeat: () => seat, + setMarketSeat: (marketSeat) => (seat.market = marketSeat), + }; +}; diff --git a/agoric/contract/test/swingsetTests/bootstrap/utils.js b/agoric/contract/test/swingsetTests/bootstrap/utils.js new file mode 100644 index 000000000..00a419157 --- /dev/null +++ b/agoric/contract/test/swingsetTests/bootstrap/utils.js @@ -0,0 +1,149 @@ +import { AmountMath, AssetKind, makeIssuerKit } from '@agoric/ertp'; +import { E } from '@endo/eventual-send'; +import { defaultAssets } from '../data.js'; +import { flow } from '../flow.js'; +import { makeCopyBag } from '@agoric/store'; + +/** + * Mint character and deposit on Context character purse + * + * @param {Context} context + */ +export const addCharacterToContext = async (context) => { + /** @type {Context} */ + const { publicFacet, paymentAsset, purses, zoe } = context; + const { offerArgs, give } = flow.mintCharacter.expected; + + const mintCharacterInvitation = await E( + publicFacet, + ).makeMintCharacterInvitation(); + + const payment = { + Price: paymentAsset.mintMockIST.mintPayment( + AmountMath.make(paymentAsset.brandMockIST, harden(30000000n)), + ), + }; + + const priceAmount = AmountMath.make(paymentAsset.brandMockIST, give.Price); + + const proposal = harden({ + give: { Price: priceAmount }, + }); + + const userSeat = await E(zoe).offer( + mintCharacterInvitation, + proposal, + payment, + offerArgs, + ); + + const payout = await E(userSeat).getPayout('Asset'); + await E(purses.character).deposit(payout); +}; + +/** + * Mint item and deposit on Bootstrap item purse + * + * @param {Context} context + */ +export const addItemToContext = async (context, item) => { + /** @type {Context} */ + const { creatorFacet, contractAssets, purses, zoe } = context; + + const mintItemInvitation = await E(creatorFacet).makeMintItemInvitation(); + const itemAmount = AmountMath.make( + contractAssets.item.brand, + makeCopyBag(harden([[item, 1n]])), + ); + + const proposal = harden({ + want: { Item: itemAmount }, + }); + + const userSeat = await E(zoe).offer(mintItemInvitation, proposal); + const payout = await E(userSeat).getPayout('Asset'); + await E(purses.item).deposit(payout); +}; +harden(addItemToContext); + +/** + * @param {AssetConf} [conf] + * @returns { Assets } + */ +export const setupAssets = (conf) => { + // fallback to empty arrays when conf undefined + if (!conf) + conf = { + fts: [], + nfts: [], + }; + if (!conf.fts) conf.fts = []; + if (!conf.nfts) conf.nfts = []; + + /** @type {Record} */ + const fts = {}; + + /** @type {Record} */ + const nfts = {}; + + /** @type {Record} */ + const all = {}; + + // Merge asset conf with default assets + const assetNames = { + fts: [...new Set([...defaultAssets.fts, ...conf.fts])], + nfts: [...new Set([...defaultAssets.nfts, ...conf.nfts])], + all: [...new Set([...defaultAssets.all, ...conf.nfts, ...conf.fts])], + }; + + // Helper methods for creating amounts and payments of a given brand + /** @type {(brand: Brand) => (value: any) => Amount} */ + const makeAmount = (brand) => (value) => AmountMath.make(brand, value); + const makePayment = (mint, brand) => (value) => + mint.mintPayment(AmountMath.make(brand, value)); + + // Create and store asset object for each nft and ft + assetNames.nfts.forEach((nftName) => { + const kit = makeIssuerKit(nftName, AssetKind.SET); + const assetObject = { + kit, + makeAmount: makeAmount(kit.brand), + makePayment: makePayment(kit.mint, kit.brand), + name: nftName, + }; + nfts[nftName] = assetObject; + all[nftName] = assetObject; + }); + + assetNames.fts.forEach((ftName) => { + const kit = makeIssuerKit(ftName); + const assetObject = { + kit, + makeAmount: makeAmount(kit.brand), + makePayment: makePayment(kit.mint, kit.brand), + name: ftName, + }; + fts[ftName] = assetObject; + all[ftName] = assetObject; + }); + + // Create issuerKeywordRecord for use with the contract + const issuerKeywordRecord = {}; + + Object.entries(all).forEach(([name, { kit }]) => { + const [first, ...rest] = name; + const keyword = first.toUpperCase() + rest.join(''); + issuerKeywordRecord[keyword] = kit.issuer; + }); + + const assets = { + nfts, + fts, + all, + issuerKeywordRecord, + }; + + harden(assets); + return assets; +}; +harden(setupAssets); diff --git a/agoric/contract/test/swingsetTests/data.js b/agoric/contract/test/swingsetTests/data.js new file mode 100644 index 000000000..fb286c779 --- /dev/null +++ b/agoric/contract/test/swingsetTests/data.js @@ -0,0 +1,12 @@ +const fts = ['Moola', 'Bucks', 'Pesos']; +const nfts = ['NFTa', 'NFTb', 'NFTc']; +export const defaultAssets = { + fts, + nfts, + all: [...fts, ...nfts], +}; + +export const flow = { + sad: `👎 sad flow`, + happy: `👍 happy flow`, +}; diff --git a/agoric/contract/test/swingsetTests/flow.js b/agoric/contract/test/swingsetTests/flow.js new file mode 100644 index 000000000..a304f8912 --- /dev/null +++ b/agoric/contract/test/swingsetTests/flow.js @@ -0,0 +1,112 @@ +import { errors } from '../../src/kreadV2/errors.js'; +import { text } from './text.js'; +import { defaultItems } from './items.js'; + +const mintCharacter = { + expected: { + offerArgs: { name: 'TestCharacter' }, + give: { Price: 30000000n }, + message: text.characterMintSuccess, + }, + feeTooLow: { + offerArgs: { name: 'TestCharacterBadFlow' }, + give: { Price: 10000000n }, + message: errors.mintFeeTooLow, + }, + duplicateName: { + offerArgs: { name: 'TestCharacter' }, + give: { Price: 30000000n }, + message: errors.nameTaken('TestCharacter'), + }, + noArgs: { + offerArgs: undefined, + give: { Price: 30000000n }, + message: errors.noNameArg, + }, + noName: { + offerArgs: { name: undefined }, + give: { Price: 30000000n }, + message: errors.noNameArg, + }, + noAvailability: { + offerArgs: { name: 'TestCharacterBadFlow' }, + give: { Price: 30000000n }, + message: errors.allMinted, + }, + extraProperties: { + offerArgs: { name: 'TestCharacter', bloodType: 'blue', married: true }, + give: { Price: 30000000n }, + message: '', + }, + invalidName1: { + offerArgs: { name: '012345678901234567890123' }, + give: { Price: 30000000n }, + message: errors.invalidName, + }, + invalidName2: { + offerArgs: { name: 'TestCharacter!' }, + give: { Price: 30000000n }, + message: errors.invalidName, + }, + invalidName3: { + offerArgs: { name: 'names' }, + give: { Price: 30000000n }, + message: errors.invalidName, + }, +}; + +const mintItem = { + expected: { + want: defaultItems.filter(({ rarity }) => rarity > 59)[0], + message: text.itemMintSuccess, + }, + multiple: { + want: defaultItems.filter(({ rarity }) => rarity < 20)[0], + message: text.itemMintSuccess, + }, + multipleUnique: { + want: [ + defaultItems.filter(({ rarity }) => rarity > 39 && rarity < 60)[0], + defaultItems.filter(({ rarity }) => rarity > 19 && rarity < 40)[0], + ], + message: text.itemMintSuccess, + }, +}; + +const inventory = { + characterName: 'TestCharacter', + unequip: { + message: text.unequipSuccess, + }, + equip: { + message: text.equipSuccess, + }, +}; + +const market = { + bob: { + give: { + character: 'TestCharacter', + }, + want: { + item: 'hair', + payment: 20n, + }, + }, + alice: { + give: { + item: 'hair', + payment: 20n, + }, + want: { + character: 'TestCharacter', + }, + }, +}; + +export const flow = { + mintCharacter, + mintItem, + inventory, + market, +}; diff --git a/agoric/contract/test/swingsetTests/items.js b/agoric/contract/test/swingsetTests/items.js new file mode 100644 index 000000000..92e64a949 --- /dev/null +++ b/agoric/contract/test/swingsetTests/items.js @@ -0,0 +1,192 @@ +export const defaultItems = [ + { + name: 'AirTox: Fairy Dust Elite', + category: 'perk1', + functional: false, + description: + 'This is an all-purpose air filter and air temperature regulator with minimal water analyzing technology. Suitable for warm hostile places, weather, and contaminated areas. Not so good for the dead zone.', + origin: 'Elphia', + image: + 'https://ipfs.io/ipfs/QmayqpgHTDQ8qQCWv8DPax2mfhtXey3kmzWqEBHdphyAZx', + thumbnail: + 'https://ipfs.io/ipfs/QmeSU6u5jQgcjfTyQuhwjqaFgM5vZmZeoxYPBTeEev9ALs', + rarity: 15, + level: 0, + filtering: 0, + weight: 0, + sense: 0, + reserves: 0, + durability: 0, + colors: ['#B1A2A2', '#7B5B7B', '#968996', '#FFFFFF'], + artistMetadata: '', + }, + { + name: 'AirTox: Fairy Dust Elite', + category: 'patch', + functional: false, + origin: 'Elphia', + description: + 'This is an all-purpose air filter and air temperature regulator with minimal water analyzing technology. Suitable for warm hostile places, weather, and contaminated areas. Not so good for the dead zone.', + image: + 'https://ipfs.io/ipfs/QmTdg7MpcL3rKfLiBAfLkedBp74uPKx3CTGrYdwARspy4e', + thumbnail: + 'https://ipfs.io/ipfs/QmS3fkmVaToE7imZn9jtMZMbSTTeeyGZBHLocUUw7u5z4T', + rarity: 15, + level: 0, + filtering: 0, + weight: 0, + sense: 0, + reserves: 0, + durability: 0, + colors: ['#7B5B7B'], + artistMetadata: '', + }, + { + name: 'AirTox: Fairy Dust Elite', + category: 'perk2', + functional: false, + origin: 'Elphia', + description: + 'This is an all-purpose air filter and air temperature regulator with minimal water analyzing technology. Suitable for warm hostile places, weather, and contaminated areas. Not so good for the dead zone.', + image: + 'https://ipfs.io/ipfs/QmadebCRvkLSHdeSTnPJv58XHtjk5DwYbeUigNNcWPs2Vn', + thumbnail: + 'https://ipfs.io/ipfs/QmYmyyNeoyeAQ8qPHkufum848mA1P5Q1KHZp7n6vhwGsgd', + rarity: 15, + level: 0, + filtering: 0, + weight: 0, + sense: 0, + reserves: 0, + durability: 0, + colors: ['#B1A2A2', '#968996', '#FFFFFF'], + artistMetadata: '', + }, + { + name: 'AirTox: Fairy Dust Elite', + category: 'headPiece', + functional: false, + origin: 'Elphia', + description: + 'This is an all-purpose air filter and air temperature regulator with minimal water analyzing technology. Suitable for warm hostile places, weather, and contaminated areas. Not so good for the dead zone.', + image: + 'https://ipfs.io/ipfs/Qmb1ZXVuJifqQ28fEqiSEB7kgXXRqKfwYA7CD4aeLUbrtR', + thumbnail: + 'https://ipfs.io/ipfs/QmciQkft6W2oZeGQuahosN5LaBawXYkJZTVq8VCEMLZVVG', + rarity: 15, + level: 0, + filtering: 0, + weight: 0, + sense: 0, + reserves: 0, + durability: 0, + colors: ['#B1A2A2', '#7B5B7B', '#968996', '#FFFFFF'], + artistMetadata: '', + }, + { + name: 'AirTox: Fairy Dust Elite', + category: 'filter2', + functional: false, + origin: 'Elphia', + description: + 'This is an all-purpose air filter and air temperature regulator with minimal water analyzing technology. Suitable for warm hostile places, weather, and contaminated areas. Not so good for the dead zone.', + image: + 'https://ipfs.io/ipfs/QmdmgvWYz1bEdBH6eSTPCfkcjZQhZvbREYULGjJd4EQPZy', + thumbnail: + 'https://ipfs.io/ipfs/QmZMyeV2dvMs9i5kjj9gCvmkvMz5nTBapmyKARk7dkbRqX', + rarity: 15, + level: 0, + filtering: 0, + weight: 0, + sense: 0, + reserves: 0, + durability: 0, + colors: ['#B1A2A2', '#7B5B7B', '#968996', '#FFFFFF'], + artistMetadata: '', + }, + { + name: 'AirTox: Fairy Dust Elite', + category: 'garment', + functional: false, + origin: 'Elphia', + description: + 'This is an all-purpose air filter and air temperature regulator with minimal water analyzing technology. Suitable for warm hostile places, weather, and contaminated areas. Not so good for the dead zone.', + image: + 'https://ipfs.io/ipfs/QmShge9z81i5sRjgHUjH5EBwtKPvSRNap5JHbp4imLgJ4H', + thumbnail: + 'https://ipfs.io/ipfs/QmdVLuhUPRvpHzmERTSsChHBexAhc6TUK6SPHsGnqQ7QaM', + rarity: 15, + level: 0, + filtering: 0, + weight: 0, + sense: 0, + reserves: 0, + durability: 0, + colors: ['#B1A2A2', '#7B5B7B', '#968996', '#FFFFFF'], + artistMetadata: '', + }, + { + name: 'AirTox: Fairy Dust Elite', + category: 'hair', + functional: false, + origin: 'Elphia', + description: + 'This is an all-purpose air filter and air temperature regulator with minimal water analyzing technology. Suitable for warm hostile places, weather, and contaminated areas. Not so good for the dead zone.', + image: + 'https://ipfs.io/ipfs/QmdMyDHeudz3RJDFbrwgAvtC2Kx6rmcMbbF85hyqnnNcfE', + thumbnail: + 'https://ipfs.io/ipfs/QmPQbf3NkPbSv6HABKPeEkbGdsoXVyVnpY5kXY3mS8Q4yR', + rarity: 68, + level: 0, + filtering: 0, + weight: 0, + sense: 0, + reserves: 0, + durability: 0, + colors: ['#B1A2A2', '#7B5B7B', '#968996', '#FFFFFF'], + artistMetadata: '', + }, + { + name: 'AirTox: Fairy Dust Elite', + category: 'filter1', + functional: false, + origin: 'Elphia', + description: + 'This is an all-purpose air filter and air temperature regulator with minimal water analyzing technology. Suitable for warm hostile places, weather, and contaminated areas. Not so good for the dead zone.', + origin: 'elphia', + image: + 'https://ipfs.io/ipfs/QmP7iMiQLRWy1fF8yLXR5xQg1kpEqzy19uwzEJxxZmGUjS', + thumbnail: + 'https://ipfs.io/ipfs/QmcVTwEMR8XzsF8hThYMpajTb6oL7TtvfuH3MEpwyvZUMi', + rarity: 40, + level: 0, + filtering: 0, + weight: 0, + sense: 0, + reserves: 0, + durability: 0, + colors: ['#B1A2A2', '#7B5B7B', '#968996', '#FFFFFF'], + artistMetadata: '', + }, + { + name: 'AirTox: Fairy Dust Elite', + category: 'background', + functional: false, + origin: 'Elphia', + description: + 'This is an all-purpose air filter and air temperature regulator with minimal water analyzing technology. Suitable for warm hostile places, weather, and contaminated areas. Not so good for the dead zone.', + image: + 'https://ipfs.io/ipfs/QmaYb31m9CRTKJQzVgi2NaHfPqdUTYuXQq5jLzTbTa2YVx', + thumbnail: + 'https://ipfs.io/ipfs/QmYRmByVnzK2D6akMDEfMj3LPLSJw15R9s9gdu9oGcEV7E', + rarity: 30, + level: 0, + filtering: 0, + weight: 0, + sense: 0, + reserves: 0, + durability: 0, + colors: ['#B1A2A2', '#7B5B7B', '#968996', '#FFFFFF'], + artistMetadata: '', + }, +]; diff --git a/agoric/contract/test/swingsetTests/swingset-setup.js b/agoric/contract/test/swingsetTests/swingset-setup.js new file mode 100644 index 000000000..1ab48e3ad --- /dev/null +++ b/agoric/contract/test/swingsetTests/swingset-setup.js @@ -0,0 +1,85 @@ +import { test } from '../prepare-test-env-ava.js'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { resolve as importMetaResolve } from 'import-meta-resolve'; +import { buildVatController } from '@agoric/swingset-vat'; +import { assert } from '@agoric/assert'; + +const bfile = (name) => new URL(name, import.meta.url).pathname; +const kreadV1BundleName = 'kreadV1'; +const kreadV2BundleName = 'kreadV2'; + +let c; + +/** + * Queues a function into the bootstrap vat with the given arguments + * + * @param {string} name + * @param {any} args + * @returns + */ +export const run = async (name, args = []) => { + assert(Array.isArray(args)); + const kpid = c.queueToVatRoot('bootstrap', name, args); + await c.run(); + const status = c.kpStatus(kpid); + const capdata = c.kpResolution(kpid); + return [status, capdata]; +}; + +/** + * Sets up swingset and starts v1 of kread contract + */ +export async function setup() { + const config = { + includeDevDependencies: true, + defaultManagerType: 'local', + bootstrap: 'bootstrap', + vats: { + bootstrap: { + sourceSpec: bfile('bootstrap/bootstrap-upgradable.js'), + }, + zoe: { + sourceSpec: await importMetaResolve( + '@agoric/vats/src/vat-zoe.js', + import.meta.url, + ).then((href) => new URL(href).pathname), + }, + }, + bundles: { + zcf: { + sourceSpec: await importMetaResolve( + '@agoric/zoe/src/contractFacet/vatRoot.js', + import.meta.url, + ).then((href) => new URL(href).pathname), + }, + committee: { + sourceSpec: await importMetaResolve( + '@agoric/governance/src/committee.js', + import.meta.url, + ).then((href) => new URL(href).pathname), + }, + puppetContractGovernor: { + sourceSpec: await importMetaResolve( + '@agoric/governance/tools/puppetContractGovernor.js', + import.meta.url, + ).then((href) => new URL(href).pathname), + }, + [kreadV1BundleName]: { + sourceSpec: await importMetaResolve( + '../../src/kreadV1/index.js', + import.meta.url, + ).then((href) => new URL(href).pathname), + }, + [kreadV2BundleName]: { + sourceSpec: await importMetaResolve( + '../../src/kreadV2/index.js', + import.meta.url, + ).then((href) => new URL(href).pathname), + }, + }, + }; + + c = await buildVatController(config); + c.pinVatRoot('bootstrap'); + await c.run(); +} diff --git a/agoric/contract/test/swingsetTests/test-governance.js b/agoric/contract/test/swingsetTests/test-governance.js new file mode 100644 index 000000000..0e51325b5 --- /dev/null +++ b/agoric/contract/test/swingsetTests/test-governance.js @@ -0,0 +1,14 @@ +import { test } from '../prepare-test-env-ava.js'; +import { run, setup } from './swingset-setup.js'; + +test.before(async (t) => { + await setup(); + + const [build] = await run('buildV1'); + t.is(build, "fulfilled") +}); + +test.serial('| GOVERNANCE - Block Methods', async (t) => { + const [result] = await run('blockMethods'); + t.is(result, 'rejected'); +}); diff --git a/agoric/contract/test/swingsetTests/test-inventory.js b/agoric/contract/test/swingsetTests/test-inventory.js new file mode 100644 index 000000000..29897e00d --- /dev/null +++ b/agoric/contract/test/swingsetTests/test-inventory.js @@ -0,0 +1,62 @@ +import { test } from '../prepare-test-env-ava.js'; +import { run, setup } from './swingset-setup.js'; + +test.before(async (t) => { + await setup(); + + const [build] = await run('buildV1'); + t.is(build, "fulfilled") + + const [setupInventoryTests] = await run('setupInventoryTests'); + t.is(setupInventoryTests, 'fulfilled'); +}); + +test.serial('| INVENTORY - Unequip Item', async (t) => { + const [result] = await run('unequipItem'); + t.is(result, 'fulfilled'); +}); + +test.serial('| INVENTORY - Unequip already unequipped item', async (t) => { + const [result] = await run('unequipAlreadyUnequippedItem'); + t.is(result, 'rejected'); +}); + +test.serial('| INVENTORY - Unequip - wrong character', async (t) => { + const [result] = await run('unequipWithWrongCharacter'); + t.is(result, 'rejected'); +}); + +test.serial('| INVENTORY - Equip Item', async (t) => { + const [result] = await run('equipItem'); + t.is(result, 'fulfilled'); +}); + +test.serial('| INVENTORY - Equip Item duplicate category', async (t) => { + const [result] = await run('equipItemDuplicateCategory'); + t.is(result, 'rejected'); +}); + +test.serial('| INVENTORY - Swap Items', async (t) => { + const [result] = await run('swapItems'); + t.is(result, 'fulfilled'); +}); + +test.serial('| INVENTORY - Swap Items - Initially empty', async (t) => { + const [result] = await run('swapItemsInitiallyEmpty'); + t.is(result, 'fulfilled'); +}); + +test.serial('| INVENTORY - Swap Items - Different categories', async (t) => { + const [result] = await run('swapItemsDifferentCategories'); + t.is(result, 'rejected'); +}); + +test.serial('| INVENTORY - Unequip all', async (t) => { + const [result] = await run('unequipAll'); + t.is(result, 'fulfilled'); +}); + +test.serial('| INVENTORY - UnequipAll empty inventory', async (t) => { + const [result] = await run('unequipAllEmptyInventory'); + t.is(result, 'fulfilled'); +}); diff --git a/agoric/contract/test/swingsetTests/test-market-metrics.js b/agoric/contract/test/swingsetTests/test-market-metrics.js new file mode 100644 index 000000000..8644e70c4 --- /dev/null +++ b/agoric/contract/test/swingsetTests/test-market-metrics.js @@ -0,0 +1,37 @@ +import { test } from '../prepare-test-env-ava.js'; +import { run, setup } from './swingset-setup.js'; + +test.before(async (t) => { + await setup(); + + const [build] = await run('buildV1'); + t.is(build, "fulfilled") + + const [setupMarketMetricsTests] = await run('setupMarketMetricsTests'); + t.is(setupMarketMetricsTests, 'fulfilled'); +}); + +test.serial('---| METRICS - Initialization', async (t) => { + const [result] = await run('initialization'); + t.is(result, 'fulfilled'); +}); + +test.serial('---| METRICS - Collection size', async (t) => { + const [result] = await run('collectionSize'); + t.is(result, 'fulfilled'); +}); + +test.serial('---| METRICS - Average levels character', async (t) => { + const [result] = await run('averageLevelsCharacter'); + t.is(result, 'fulfilled'); +}); + +test.serial('---| METRICS - Amount sold character', async (t) => { + const [result] = await run('amountSoldCharacter'); + t.is(result, 'fulfilled'); +}); + +test.serial('---| METRICS - Latest sale price character', async (t) => { + const [result] = await run('latestSalePriceCharacter'); + t.is(result, 'fulfilled'); +}); diff --git a/agoric/contract/test/swingsetTests/test-market.js b/agoric/contract/test/swingsetTests/test-market.js new file mode 100644 index 000000000..1001e89f3 --- /dev/null +++ b/agoric/contract/test/swingsetTests/test-market.js @@ -0,0 +1,100 @@ +import { test } from '../prepare-test-env-ava.js'; +import { setup, run } from './swingset-setup.js'; + +test.before(async (t) => { + await setup(); + + const [build] = await run('buildV1'); + t.is(build, "fulfilled") + + const [setupMarketTests] = await run('setupMarketTests'); + t.is(setupMarketTests, 'fulfilled'); +}); + +test.serial('--| MARKET - Sell character', async (t) => { + const [result] = await run('sellCharacter'); + t.is(result, 'fulfilled'); +}); + +test.serial( + '---| MARKET - Buy character; offer less than asking price', + async (t) => { + const [result] = await run('buyCharacterOfferLessThanAskingPrice'); + t.is(result, 'rejected'); + }, +); + +test.serial( + '---| MARKET - Buy character', + async (t) => { + const [result] = await run('buyCharacter'); + t.is(result, 'fulfilled'); + }, +); + +test.serial( + '---| MARKET - Buy character not on market', + async (t) => { + const [result] = await run('buyCharacterNotOnMarket'); + t.is(result, 'rejected'); + }, +); + +test.serial( + '---| MARKET - Sell item', + async (t) => { + const [result] = await run('sellItem'); + t.is(result, 'fulfilled'); + }, +); + +test.serial( + '---| MARKET - buyItemOfferLessThanAskingPrice', + async (t) => { + const [result] = await run('buyItemOfferLessThanAskingPrice'); + t.is(result, 'rejected'); + }, +); + +test.serial( + '---| MARKET - buyItem', + async (t) => { + const [result] = await run('buyItem'); + t.is(result, 'fulfilled'); + }, +); +test.serial( + '---| MARKET - buyItemNotOnMarket', + async (t) => { + const [result] = await run('buyItemNotOnMarket'); + t.is(result, 'rejected'); + }, +); +test.serial( + '---| MARKET - buyCharacterOfferMoreThanAskingPrice', + async (t) => { + const [result] = await run('buyCharacterOfferMoreThanAskingPrice'); + t.is(result, 'fulfilled'); + }, +); +test.serial( + '---| MARKET - buyItemOfferMoreThanAskingPrice', + async (t) => { + const [result] = await run('buyItemOfferMoreThanAskingPrice'); + t.is(result, 'fulfilled'); + }, +); +test.serial( + '---| MARKET - internalSellItemBatch', + async (t) => { + const [result] = await run('internalSellItemBatch'); + t.is(result, 'fulfilled'); + }, +); +test.serial( + '---| MARKET - buyBatchSoldItem', + async (t) => { + const [result] = await run('buyBatchSoldItem'); + t.is(result, 'fulfilled'); + }, +); diff --git a/agoric/contract/test/swingsetTests/test-minting.js b/agoric/contract/test/swingsetTests/test-minting.js new file mode 100644 index 000000000..75dc26743 --- /dev/null +++ b/agoric/contract/test/swingsetTests/test-minting.js @@ -0,0 +1,82 @@ +import { test } from '../prepare-test-env-ava.js'; +import { setup, run } from './swingset-setup.js'; + +test.before(async (t) => { + await setup(); + + const [build] = await run('buildV1'); + t.is(build, "fulfilled") + + const [setupMintTests] = await run('setupMintTests'); + t.is(setupMintTests, 'fulfilled'); +}); + +test.serial('--| MINT - Too Long Name', async (t) => { + const [result] = await run('mintTooLongName'); + t.is(result, 'rejected'); +}); + +test.serial('--| MINT - Invalid Chars in Name', async (t) => { + const [result] = await run('mintInvalidCharsInname'); + t.is(result, 'rejected'); +}); + +test.serial('--| MINT - Forbidden name: "names"', async (t) => { + const [result] = await run('mintForbiddenName'); + t.is(result, 'rejected'); +}); + +test.serial('--| MINT - Expected flow', async (t) => { + const [mintCharacter] = await run('mintExpectedFlow'); + t.is(mintCharacter, 'fulfilled'); +}); + +test.serial('--| MINT - Fee too low', async (t) => { + const [result] = await run('mintFeeTooLow'); + t.is(result, 'rejected'); +}); + +test.serial('--| MINT - No offerArgs', async (t) => { + const [result] = await run('mintNoOfferArgs'); + t.is(result, 'rejected'); +}); + +test.serial('--| MINT - Duplicate Name', async (t) => { + const [result] = await run('mintDuplicateName'); + t.is(result, 'rejected'); +}); + +test.serial('--| MINT - No name', async (t) => { + const [result] = await run('mintNoName'); + t.is(result, 'rejected'); +}); + +test.serial('--| MINT - No characters available', async (t) => { + const [result] = await run('mintNoCharactersAvailable'); + t.is(result, 'rejected'); +}); + +test.serial('--| MINT - Inventory check', async (t) => { + const [result] = await run('mintInventoryCheck'); + t.is(result, 'fulfilled'); +}); + +test.serial('--| MINT - Item - Expected flow', async (t) => { + const [result] = await run('mintItemExpectedFlow'); + t.is(result, 'fulfilled'); +}); + +test.serial('--| MINT - Item - Mint same item (SFT)', async (t) => { + const [result] = await run('mintSameItemSFT'); + t.is(result, 'fulfilled'); +}); + +test.serial('--| MINT - Item - Multiple flow', async (t) => { + const [result] = await run('mintItemMultipleFlow'); + t.is(result, 'fulfilled'); +}); + +test.serial('--| MINT - Item - Multiple different items flow', async (t) => { + const [result] = await run('mintItemMultipleDifferentFlow'); + t.is(result, 'fulfilled'); +}); diff --git a/agoric/contract/test/swingsetTests/test-null-upgrade.js b/agoric/contract/test/swingsetTests/test-null-upgrade.js new file mode 100644 index 000000000..f3250e940 --- /dev/null +++ b/agoric/contract/test/swingsetTests/test-null-upgrade.js @@ -0,0 +1,26 @@ +import { test } from '../prepare-test-env-ava.js'; +import { setup, run } from './swingset-setup.js'; + +test.before(async (t) => { + await setup(); +}); + +test.serial('buildV1', async (t) => { + const [build] = await run('buildV1'); + t.is(build, 'fulfilled'); +}) + +test.serial('testBefore', async (t) => { + const [build] = await run('mint'); + t.is(build, 'fulfilled'); +}) + +test.serial('null upgrade', async (t) => { + const [result] = await run('nullUpgrade'); + t.is(result, 'fulfilled'); +}); + +test.serial('test after', async (t) => { + const [result] = await run('mint'); + t.is(result, 'rejected'); +}); diff --git a/agoric/contract/test/swingsetTests/test-upgrade.js b/agoric/contract/test/swingsetTests/test-upgrade.js new file mode 100644 index 000000000..5948ac6aa --- /dev/null +++ b/agoric/contract/test/swingsetTests/test-upgrade.js @@ -0,0 +1,50 @@ +import { defaultItems } from './items.js'; +import { test } from '../prepare-test-env-ava.js'; +import { setup, run } from './swingset-setup.js'; +import { defaultCharacters } from '../characters.js'; + +test.before(async (t) => { + await setup(); + + const [build] = await run('buildV1', [ + [ + ...defaultCharacters, + [ + 2, + { + title: 'Citizen', + origin: 'Sage', + description: + 'A Tempet Scavenger has Tempet technology, which is, own modification on the standard requirements and regulations on tech that is allowed. Agreed among the cities. Minimal and elegant, showcasing their water technology filtration system that is known throughout that land as having the best mask when it comes to scent tracking technology.', + level: 1, + artistMetadata: '', + characterTraits: '', + image: + 'https://ipfs.io/ipfs/QmSkCL11goTK7qw1qLjbozUJ1M7mJtSyH1PnL1g8AB96Zg', + }, + ], + ], + defaultItems, + ]); + t.is(build, 'fulfilled'); + + const [setupUpgradeTests] = await run('setupUpgradeTests'); + t.is(setupUpgradeTests, 'fulfilled'); +}); + +test.serial('test functionality before upgrade', async (t) => { + const [result] = await run('testFunctionalityBeforeUpgrade'); + t.is(result, 'fulfilled'); +}); + +test.serial('upgrade to V2', async (t) => { + const [result] = await run('upgradeV2'); + t.is(result, 'fulfilled'); +}); + +test.serial('test functionality after upgrade', async (t) => { + const [result] = await run('testFunctionalityAfterUpgrade'); + t.is(result, 'fulfilled'); +}); + +// test.serial() diff --git a/agoric/contract/test/swingsetTests/text.js b/agoric/contract/test/swingsetTests/text.js new file mode 100644 index 000000000..ea6e58af4 --- /dev/null +++ b/agoric/contract/test/swingsetTests/text.js @@ -0,0 +1,6 @@ +export const text = { + characterMintSuccess: 'Character NFT minted successfully!', + itemMintSuccess: 'Item NFT(s) minted successfully!', + unequipSuccess: 'Item(s) were unequipped successfully', + equipSuccess: 'Item(s) were equipped successfully', +}; diff --git a/agoric/contract/test/swingsetTests/types.js b/agoric/contract/test/swingsetTests/types.js new file mode 100644 index 000000000..e7732a3bd --- /dev/null +++ b/agoric/contract/test/swingsetTests/types.js @@ -0,0 +1,88 @@ +/** + * Configuration options for seting up asset kits + * + * @typedef {{ + * fts?: string[] + * nfts?: string[] + * }} AssetConf + */ + +/** + * AssetObject + * + * @typedef {{ + * kit: IssuerKit, + * makeAmount: { + * brand: Brand, + * value: bigint + * }, + * name: string + * }} AssetObject + */ + +/** + * Return value from setup-assets.js + * + * @typedef {{ + * fts: {[key: string]: AssetObject} + * nfts: {[key: string]: AssetObject} + * all: {[key: string]: AssetObject} + * issuerKeywordRecord: IssuerKeywordRecord + * }} Assets + */ + +/** + * Contract assets as returned from getTokenInfo + * + * @typedef {{ + * character: { brand: Brand, issuer: Issuer, name: string } + * item: { brand: Brand, issuer: Issuer, name: string } + * }} ContractAssets + */ + +/** + * @typedef {{ + * character: Purse + * item: Purse + * payment: Purse + * }} Purses + */ + +/** + * @typedef {{ + * name: string + * purses: Purses + * getItems: () => Item[] + * getCharacters: () => any[] + * getPaymentBalance: async () => bigint + * depositItems: (items) => void + * depositCharacters: (characters) => void + * depositPayment: (payment) => void + * withdrawItems: (items) => Payment + * withdrawCharacters: (characters) => Payment + * withdrawPayment: (payment) => Payment + * getSeat: () => any + * setMarketSeat: (seat) => void + * }} KreadUser + */ + +// XXX approximate +/** + * Testing context + * + * @typedef {{ + * assets: Assets; + * creatorFacet: unknown; + * contractAssets: ContractAssets; + * publicFacet: unknown; + * governorFacets: import('@agoric/governance/tools/puppetContractGovernor').PuppetContractGovernorKit; + * zoe: ZoeService; + * purses: { + * character: any + * item: any + * payment: any + * }; + * users: Object.; + * storageNode: StorageNode; + * }} Context + */ diff --git a/agoric/contract/test/test-inventory.js b/agoric/contract/test/test-inventory.js index 694ac24cf..6db4bd063 100644 --- a/agoric/contract/test/test-inventory.js +++ b/agoric/contract/test/test-inventory.js @@ -6,7 +6,7 @@ import { bootstrapContext } from './bootstrap.js'; import { flow } from './flow.js'; import { addCharacterToBootstrap, addItemToBootstrap } from './setup.js'; import { makeCopyBag } from '@agoric/store'; -import { errors } from '../src/errors.js'; +import { errors } from '../src/kreadV2/errors.js'; test.before(async (t) => { const bootstrap = await bootstrapContext(); diff --git a/agoric/contract/test/test-market.js b/agoric/contract/test/test-market.js index 079b84f99..b4a3bd82e 100644 --- a/agoric/contract/test/test-market.js +++ b/agoric/contract/test/test-market.js @@ -7,7 +7,7 @@ import { flow } from './flow.js'; import { makeKreadUser } from './make-user.js'; import { addCharacterToBootstrap, addItemToBootstrap } from './setup.js'; import { makeCopyBag } from '@agoric/store'; -import { errors } from '../src/errors.js'; +import { errors } from '../src/kreadV2/errors.js'; import { defaultItems } from './items.js'; import { multiplyBy } from '@agoric/zoe/src/contractSupport/ratio.js'; diff --git a/agoric/yarn.lock b/agoric/yarn.lock index 8c9b107f7..0971bc1aa 100644 --- a/agoric/yarn.lock +++ b/agoric/yarn.lock @@ -16,6 +16,11 @@ n-readlines "^1.0.0" tmp "^0.2.1" +"@agoric/assert@^0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@agoric/assert/-/assert-0.6.0.tgz#43ede53cf0943f3e9038f597f776e52500446e41" + integrity sha512-bpY9ul5egbVlmdf9RtDfxh1WQaDSOCzqcAxyqE771rbkv+QYs46oZc4oUVHi7wt3g5LVXj/JsKgLkJEKpEl1BA== + "@agoric/assert@^0.6.1-u11wf.0", "@agoric/assert@agoric-upgrade-11": version "0.6.1-u11wf.0" resolved "https://registry.yarnpkg.com/@agoric/assert/-/assert-0.6.1-u11wf.0.tgz#742ae27103547b41cdbb3f17c4f09922a2d639e2" @@ -196,6 +201,20 @@ agoric "^0.21.2-u11wf.0" jessie.js "^0.3.2" +"@agoric/internal@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@agoric/internal/-/internal-0.3.2.tgz#a1242947083ab46cbd34613add8bacbd0c9dc443" + integrity sha512-iCgZE2NabhDKBJ+EfTeVqWUcoNvgxNfnw/dMF57yKnRn3E9iOqt7Iyx+DloTGBu5a1ZyywdQo4G2cFWrMX3Q7g== + dependencies: + "@agoric/zone" "^0.2.2" + "@endo/far" "^0.2.18" + "@endo/marshal" "^0.8.5" + "@endo/patterns" "^0.2.2" + "@endo/promise-kit" "^0.2.56" + "@endo/stream" "^0.3.25" + anylogger "^0.21.0" + jessie.js "^0.3.2" + "@agoric/internal@^0.3.3-u11wf.0", "@agoric/internal@agoric-upgrade-11": version "0.3.3-u11wf.0" resolved "https://registry.yarnpkg.com/@agoric/internal/-/internal-0.3.3-u11wf.0.tgz#06646cfd645282275bc1b2a0f8da73364bdae423" @@ -303,6 +322,22 @@ "@endo/import-bundle" "0.3.4" "@endo/marshal" "0.8.5" +"@agoric/store@^0.9.2": + version "0.9.2" + resolved "https://registry.yarnpkg.com/@agoric/store/-/store-0.9.2.tgz#0973e57b8811a70923c141fccfb002bbad8fed4b" + integrity sha512-9YtBlQG1cO7COfprPqBUYDW1Jg805Ick1RHm8Etj5VyfkhF8emhv/OqJKi4FMlA3XDVL3Yvbptrjvdo1WjCvjg== + dependencies: + "@agoric/assert" "^0.6.0" + "@agoric/internal" "^0.3.2" + "@endo/eventual-send" "^0.17.2" + "@endo/exo" "^0.2.2" + "@endo/far" "^0.2.18" + "@endo/marshal" "^0.8.5" + "@endo/pass-style" "^0.1.3" + "@endo/patterns" "^0.2.2" + "@endo/promise-kit" "^0.2.56" + "@fast-check/ava" "^1.1.3" + "@agoric/store@^0.9.3-u11wf.0", "@agoric/store@agoric-upgrade-11": version "0.9.3-u11wf.0" resolved "https://registry.yarnpkg.com/@agoric/store/-/store-0.9.3-u11wf.0.tgz#1214fa39bdf433d4c9afb8381cbe4e63eae66e82" @@ -314,6 +349,19 @@ "@endo/pass-style" "0.1.3" "@endo/patterns" "0.2.2" +"@agoric/swing-store@^0.9.1": + version "0.9.1" + resolved "https://registry.yarnpkg.com/@agoric/swing-store/-/swing-store-0.9.1.tgz#0ed85beac7a7cd2e8e7507ea58e50eecb08a203e" + integrity sha512-GRgXOJwEnFSX3gY2sOhHs1yO503ltUVjslSeSWFHd8bmrPs11igIYRES7Vx4lOSQuNmGe5mKFhgfDnoHPmKxjQ== + dependencies: + "@agoric/assert" "^0.6.0" + "@agoric/internal" "^0.3.2" + "@endo/base64" "^0.2.31" + "@endo/bundle-source" "^2.5.1" + "@endo/check-bundle" "^0.2.18" + "@endo/nat" "^4.1.27" + better-sqlite3 "^8.2.0" + "@agoric/swing-store@^0.9.2-u11wf.0": version "0.9.2-u11wf.0" resolved "https://registry.yarnpkg.com/@agoric/swing-store/-/swing-store-0.9.2-u11wf.0.tgz#166a135c7700f0e28c13b4ea0fdb766283e3913f" @@ -327,6 +375,24 @@ "@endo/nat" "4.1.27" better-sqlite3 "^8.2.0" +"@agoric/swingset-liveslots@^0.10.2": + version "0.10.2" + resolved "https://registry.yarnpkg.com/@agoric/swingset-liveslots/-/swingset-liveslots-0.10.2.tgz#a8d18f32ff7a611b9945f4ff920b00b9e2801e08" + integrity sha512-jp05WNHEUH5K8MgiIoHhNrWu7ozKqStyIe0Ex6ejSInFFo/NhJLVL7QLyUgRjD74RbUSuvbR8v8PaQ/pslVI0Q== + dependencies: + "@agoric/assert" "^0.6.0" + "@agoric/internal" "^0.3.2" + "@agoric/store" "^0.9.2" + "@agoric/vat-data" "^0.5.2" + "@endo/eventual-send" "^0.17.2" + "@endo/exo" "^0.2.2" + "@endo/init" "^0.5.56" + "@endo/marshal" "^0.8.5" + "@endo/nat" "^4.1.27" + "@endo/pass-style" "^0.1.3" + "@endo/patterns" "^0.2.2" + "@endo/promise-kit" "^0.2.56" + "@agoric/swingset-liveslots@^0.10.3-u11wf.0": version "0.10.3-u11wf.0" resolved "https://registry.yarnpkg.com/@agoric/swingset-liveslots/-/swingset-liveslots-0.10.3-u11wf.0.tgz#5d383c26b70bdd998d23dd557dca0bc8cb1aec56" @@ -346,6 +412,41 @@ "@endo/patterns" "0.2.2" "@endo/promise-kit" "0.2.56" +"@agoric/swingset-vat@^0.32.2": + version "0.32.2" + resolved "https://registry.yarnpkg.com/@agoric/swingset-vat/-/swingset-vat-0.32.2.tgz#5228855132ab2701223316d86eeaef410ec6b4b6" + integrity sha512-aYIhyYCuI7Oi47DBqn8wqMFnQOeOxSB7GU69wOQcQIoJVWRhOECDuFzoEqdV6G8xwGIsGpX/fMtXT9A1YAy6ZA== + dependencies: + "@agoric/assert" "^0.6.0" + "@agoric/internal" "^0.3.2" + "@agoric/store" "^0.9.2" + "@agoric/swing-store" "^0.9.1" + "@agoric/swingset-liveslots" "^0.10.2" + "@agoric/swingset-xsnap-supervisor" "^0.10.2" + "@agoric/time" "^0.3.2" + "@agoric/vat-data" "^0.5.2" + "@agoric/xsnap" "^0.14.2" + "@agoric/xsnap-lockdown" "^0.14.0" + "@endo/base64" "^0.2.31" + "@endo/bundle-source" "^2.5.1" + "@endo/captp" "^3.1.1" + "@endo/check-bundle" "^0.2.18" + "@endo/compartment-mapper" "^0.8.4" + "@endo/eventual-send" "^0.17.2" + "@endo/far" "^0.2.18" + "@endo/import-bundle" "^0.3.4" + "@endo/init" "^0.5.56" + "@endo/marshal" "^0.8.5" + "@endo/nat" "^4.1.27" + "@endo/promise-kit" "^0.2.56" + "@endo/zip" "^0.2.31" + ansi-styles "^6.2.1" + anylogger "^0.21.0" + import-meta-resolve "^2.2.1" + microtime "^3.1.0" + semver "^6.3.0" + tmp "^0.2.1" + "@agoric/swingset-vat@^0.32.3-u11wf.0": version "0.32.3-u11wf.0" resolved "https://registry.yarnpkg.com/@agoric/swingset-vat/-/swingset-vat-0.32.3-u11wf.0.tgz#6ee71b681e6577f207488853df396b9f4565ce2e" @@ -382,6 +483,11 @@ semver "^6.3.0" tmp "^0.2.1" +"@agoric/swingset-xsnap-supervisor@^0.10.2": + version "0.10.2" + resolved "https://registry.yarnpkg.com/@agoric/swingset-xsnap-supervisor/-/swingset-xsnap-supervisor-0.10.2.tgz#09f067695b0ea6ebfeb6ea200cc7f1675f0f8939" + integrity sha512-3PB15aiNHfjTYmtUz9Rxmm6qSHnoO5w5dygRzjx2ytk8yoNn/ZOpxlIOLonhD8kwOaEli5D7btY9OA3jf+Sm6w== + "@agoric/swingset-xsnap-supervisor@^0.10.3-u11wf.0": version "0.10.3-u11wf.0" resolved "https://registry.yarnpkg.com/@agoric/swingset-xsnap-supervisor/-/swingset-xsnap-supervisor-0.10.3-u11wf.0.tgz#0d51d94ffb8cb7e1b305c8c5b1a51d6ffb0a5c1b" @@ -410,6 +516,15 @@ bufferfromfile agoric-labs/BufferFromFile#Agoric-built tmp "^0.2.1" +"@agoric/time@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@agoric/time/-/time-0.3.2.tgz#9231eec197e10b52a9f416ec2afe929b67f7165c" + integrity sha512-qRgvfD/gQJNQaWk0uQqPhq0IGbIABz1z6oFtAhGkylU6zNO/no6VpJG4gw5YwEO8mIAJVOM5HE3qL53AaENYkw== + dependencies: + "@agoric/assert" "^0.6.0" + "@agoric/store" "^0.9.2" + "@endo/nat" "^4.1.27" + "@agoric/time@^0.3.3-u11wf.0", "@agoric/time@agoric-upgrade-11": version "0.3.3-u11wf.0" resolved "https://registry.yarnpkg.com/@agoric/time/-/time-0.3.3-u11wf.0.tgz#36a6bc4ea1bafd135419052191bfc3d882c0fcf2" @@ -419,6 +534,15 @@ "@agoric/store" "^0.9.3-u11wf.0" "@endo/nat" "4.1.27" +"@agoric/vat-data@^0.5.2": + version "0.5.2" + resolved "https://registry.yarnpkg.com/@agoric/vat-data/-/vat-data-0.5.2.tgz#abafab83279552466cf4ca946faa175a0a1423dc" + integrity sha512-j71bSl7oPcWikR4bP15KMu67D3BLGLhEOcqgewC1cArcE99rhxDU19ALN0OITD0F0KkNCahRNifoIr73n/fBng== + dependencies: + "@agoric/assert" "^0.6.0" + "@agoric/internal" "^0.3.2" + "@agoric/store" "^0.9.2" + "@agoric/vat-data@^0.5.3-u11wf.0", "@agoric/vat-data@agoric-upgrade-11": version "0.5.3-u11wf.0" resolved "https://registry.yarnpkg.com/@agoric/vat-data/-/vat-data-0.5.3-u11wf.0.tgz#ff64080bae8fdfc4f1634559433b27aaa23a7089" @@ -470,11 +594,34 @@ eslint-plugin-eslint-comments "^3.1.2" import-meta-resolve "^2.2.1" +"@agoric/xsnap-lockdown@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@agoric/xsnap-lockdown/-/xsnap-lockdown-0.14.0.tgz#0c605bbd08e6ccf1954a615dbce7d4c0fe578a32" + integrity sha512-T8kYrW1baTDQTkQJ9mDp1ME2Ive3RNNRFU7PXuu60Pu9A/tWliYKiJWwqcGhYAQOkHxxFz0BVwk9Jf8HErzgRA== + "@agoric/xsnap-lockdown@^0.14.1-u11wf.0": version "0.14.1-u11wf.0" resolved "https://registry.yarnpkg.com/@agoric/xsnap-lockdown/-/xsnap-lockdown-0.14.1-u11wf.0.tgz#9701e93edc5e17dcaf76ff35b920db16ef607fe7" integrity sha512-UWXoAvq8NzF8L9Mjg8SclvdAiZkc2skP/Q4CR4MOKatu1cO8KdES7VDXNuetnejlSJV7ybXdPyvGIUrpvo9SBg== +"@agoric/xsnap@^0.14.2": + version "0.14.2" + resolved "https://registry.yarnpkg.com/@agoric/xsnap/-/xsnap-0.14.2.tgz#0685b1c85af986edc3e5f226fd4e96c44df32bf0" + integrity sha512-bA4IZJixw8uCcDBqA9KUKEnxjB65pdkWb5xL0a4XM//QvIzYmB6EGJ80U+pbMOrjGtomUqo0Oxdc5X/PhAniSg== + dependencies: + "@agoric/assert" "^0.6.0" + "@agoric/internal" "^0.3.2" + "@agoric/xsnap-lockdown" "^0.14.0" + "@endo/bundle-source" "^2.5.1" + "@endo/eventual-send" "^0.17.2" + "@endo/init" "^0.5.56" + "@endo/netstring" "^0.3.26" + "@endo/promise-kit" "^0.2.56" + "@endo/stream" "^0.3.25" + "@endo/stream-node" "^0.2.26" + glob "^7.1.6" + tmp "^0.2.1" + "@agoric/xsnap@^0.14.3-u11wf.0": version "0.14.3-u11wf.0" resolved "https://registry.yarnpkg.com/@agoric/xsnap/-/xsnap-0.14.3-u11wf.0.tgz#fffdb78b76ca321f07993f1e054e00e8dab93b6f" @@ -516,6 +663,15 @@ "@endo/patterns" "0.2.2" "@endo/promise-kit" "0.2.56" +"@agoric/zone@^0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@agoric/zone/-/zone-0.2.2.tgz#df5cc091d4a83842b87888e74159a723a424a82e" + integrity sha512-joVRnwH55xOeaoO2xYd1TWXXLPQ9pAeNsaiwTjO3FufYb/q55rv9mpYdUdhEy+zYQuFpPH87+6w/o3f/0AXrDQ== + dependencies: + "@agoric/store" "^0.9.2" + "@agoric/vat-data" "^0.5.2" + "@endo/far" "^0.2.18" + "@agoric/zone@^0.2.3-u11wf.0", "@agoric/zone@agoric-upgrade-11": version "0.2.3-u11wf.0" resolved "https://registry.yarnpkg.com/@agoric/zone/-/zone-0.2.3-u11wf.0.tgz#329fb94835f930799e8f2607ea3b92be54bb6b5c" @@ -878,6 +1034,11 @@ resolved "https://registry.yarnpkg.com/@endo/base64/-/base64-0.2.31.tgz#92378462cd791e0258a2291d44d2cfd15415cf32" integrity sha512-7IndkaZ7buIuFw8oBovNZV7epuyFWs0gdusSJ/zrx6fMXRqX0ycSTtxr6M5xADQGss1I9fqP3vteVLiNFlyIbw== +"@endo/base64@^0.2.32", "@endo/base64@^0.2.35": + version "0.2.35" + resolved "https://registry.yarnpkg.com/@endo/base64/-/base64-0.2.35.tgz#7d18203d5807748388c935df7eb79c7672a0b64e" + integrity sha512-rsAicKvgNq/ar+9b3ElXRXglMiJcg1IErz3lx1HFYZUzfWp8r/Dibi3TEjYpSBmtOeYN9CeWH8CBluN0uFqdag== + "@endo/bundle-source@2.5.2-upstream-rollup", "@endo/bundle-source@^2.5.2-upstream-rollup": version "2.5.2-upstream-rollup" resolved "https://registry.yarnpkg.com/@endo/bundle-source/-/bundle-source-2.5.2-upstream-rollup.tgz#89fdc6b1b6625ca8c484c12e7762f04cd711ca9f" @@ -897,6 +1058,26 @@ rollup "^2.79.1" source-map "^0.7.3" +"@endo/bundle-source@^2.5.1": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@endo/bundle-source/-/bundle-source-2.8.0.tgz#56f25b3d9c74d3d0bede5c526647aaf02c0a8f94" + integrity sha512-nDiM3u/LKWq5xAnJ+zm35HC6kMKF3IG6Y5V0385slFHZVT8mXzRJ5ztEqRsVzvVeITfz3ZRFOaFer6v4V8Lkjg== + dependencies: + "@agoric/babel-generator" "^7.17.4" + "@babel/parser" "^7.17.3" + "@babel/traverse" "^7.17.3" + "@endo/base64" "^0.2.35" + "@endo/compartment-mapper" "^0.9.2" + "@endo/init" "^0.5.60" + "@endo/promise-kit" "^0.2.60" + "@endo/where" "^0.3.5" + "@rollup/plugin-commonjs" "^19.0.0" + "@rollup/plugin-node-resolve" "^13.0.0" + acorn "^8.2.4" + jessie.js "^0.3.2" + rollup "^2.79.1" + source-map "^0.7.3" + "@endo/captp@3.1.1": version "3.1.1" resolved "https://registry.yarnpkg.com/@endo/captp/-/captp-3.1.1.tgz#538cdb7deec694cfce1015e1ccb387270172642d" @@ -907,6 +1088,16 @@ "@endo/nat" "^4.1.27" "@endo/promise-kit" "^0.2.56" +"@endo/captp@^3.1.1": + version "3.1.5" + resolved "https://registry.yarnpkg.com/@endo/captp/-/captp-3.1.5.tgz#4cf0eeedc4728e856bd3e71cfc42cba3ab02449c" + integrity sha512-uyhECyTQqZcxt31YzCQ+n2nKu1+YE1qCuH00FFmK2qKGdF92gkluTvmDHcgxJ6lsKl/QBkQcuch51GZqXDs+xQ== + dependencies: + "@endo/eventual-send" "^0.17.6" + "@endo/marshal" "^0.8.9" + "@endo/nat" "^4.1.31" + "@endo/promise-kit" "^0.2.60" + "@endo/check-bundle@0.2.18": version "0.2.18" resolved "https://registry.yarnpkg.com/@endo/check-bundle/-/check-bundle-0.2.18.tgz#0880f4237dbc1c72c292aab3eccd7b1c20506a97" @@ -915,11 +1106,24 @@ "@endo/base64" "^0.2.31" "@endo/compartment-mapper" "^0.8.4" +"@endo/check-bundle@^0.2.18": + version "0.2.22" + resolved "https://registry.yarnpkg.com/@endo/check-bundle/-/check-bundle-0.2.22.tgz#1a978e71401b61ce9e091ac6c6bfd037140263b8" + integrity sha512-xAIcx8PCnvpSRmaSqo0iA7AeIhHrx9er5fEoz/lnXxHNngYcGaPnzux5B57kLdcJs3lBNCIzaUuh4HRhCNpIJA== + dependencies: + "@endo/base64" "^0.2.35" + "@endo/compartment-mapper" "^0.9.2" + "@endo/cjs-module-analyzer@^0.2.31": version "0.2.31" resolved "https://registry.yarnpkg.com/@endo/cjs-module-analyzer/-/cjs-module-analyzer-0.2.31.tgz#baf37a8f7eb6781a0c5780da5d1375e0fe6ad3f1" integrity sha512-0/BHR1UWN0FpKDUnmuCBd6UQV8QkQ97809iZQ4VIs1faxtAx/z2iZCNnkC3qFOPrurYSp31YbmHDfWsTDYrQ3A== +"@endo/cjs-module-analyzer@^0.2.32", "@endo/cjs-module-analyzer@^0.2.35": + version "0.2.35" + resolved "https://registry.yarnpkg.com/@endo/cjs-module-analyzer/-/cjs-module-analyzer-0.2.35.tgz#0de39d2306bba5671e121efa091bf6cb9990f11e" + integrity sha512-Ldr1auybH9AzrR/WV6bzP4aLRpv8CCl98mv0IAui4uQmmFOPOGchshyBfpiDF5XMKM6wh7z0VgmvmydQ5/7AHQ== + "@endo/compartment-mapper@0.8.4", "@endo/compartment-mapper@^0.8.4": version "0.8.4" resolved "https://registry.yarnpkg.com/@endo/compartment-mapper/-/compartment-mapper-0.8.4.tgz#afae6a4dfc64dff7082e90d7f215a072fb0a9b85" @@ -930,6 +1134,26 @@ "@endo/zip" "^0.2.31" ses "^0.18.4" +"@endo/compartment-mapper@^0.8.5": + version "0.8.5" + resolved "https://registry.yarnpkg.com/@endo/compartment-mapper/-/compartment-mapper-0.8.5.tgz#6910d2be41754fde90190671d2fc5dc48d6fb787" + integrity sha512-PKJ1WgYRBkSEJTYOXZTOf9tYQLEkuGTfhAPoKm22loAuaXWI1ortJ7UdRAPLWt95Cd71KGrmfd1FpemGvmr3lQ== + dependencies: + "@endo/cjs-module-analyzer" "^0.2.32" + "@endo/static-module-record" "^0.7.20" + "@endo/zip" "^0.2.32" + ses "^0.18.5" + +"@endo/compartment-mapper@^0.9.2": + version "0.9.2" + resolved "https://registry.yarnpkg.com/@endo/compartment-mapper/-/compartment-mapper-0.9.2.tgz#48bfa610179cc5521c745c7b2d1eb5fab52ed29a" + integrity sha512-zsAyTf87zBsE1yZ2CBzEGhcGZGGv5m93/CXZHQhut53o4DWwhuS/WTQ4cBoVFSGKWz63JbbA/7qa4fcOnv5dDw== + dependencies: + "@endo/cjs-module-analyzer" "^0.2.35" + "@endo/static-module-record" "^0.8.2" + "@endo/zip" "^0.2.35" + ses "^0.18.8" + "@endo/env-options@^0.1.4": version "0.1.4" resolved "https://registry.yarnpkg.com/@endo/env-options/-/env-options-0.1.4.tgz#e516bc3864f00b154944e444fb8996a9a0c23a45" @@ -950,6 +1174,13 @@ resolved "https://registry.yarnpkg.com/@endo/eventual-send/-/eventual-send-0.17.2.tgz#c8710d557c2f57723be05fe99e941cd893acc5d2" integrity sha512-nux02l2yYXXUeUA2PigOO1K0gbVVMYx3prfYrW/G7Ny6PiDLtOyaeMWwKQwFTgJV2yAkOfvycr4LC1+tm7hu/Q== +"@endo/eventual-send@^0.17.6": + version "0.17.6" + resolved "https://registry.yarnpkg.com/@endo/eventual-send/-/eventual-send-0.17.6.tgz#86719e4e3ff76991c49f6680309dc77dff65fe55" + integrity sha512-73cKY2uiWdzMJn7i284NJyD3K0UKjpksBg/EA2GT8YJa0TgeBczFQIm81vC08itK5gHuDDH2vC5COSGR6hxKIg== + dependencies: + "@endo/env-options" "^0.1.4" + "@endo/exo@0.2.2": version "0.2.2" resolved "https://registry.yarnpkg.com/@endo/exo/-/exo-0.2.2.tgz#eeebe3eeb40dcf9b409fddf8d5ff73821b470515" @@ -958,6 +1189,16 @@ "@endo/far" "^0.2.18" "@endo/patterns" "^0.2.2" +"@endo/exo@^0.2.2": + version "0.2.6" + resolved "https://registry.yarnpkg.com/@endo/exo/-/exo-0.2.6.tgz#09721063377981d4376b3cf8aa534dd0d49939dc" + integrity sha512-fk4EYdHRZectyLt0cn0aT8PIlb8BgE5ji6DD4AHJ9Q6TFrGr6RRV0aXs8xW9LAs7MIduz+j7vtpeURxugN8KvQ== + dependencies: + "@endo/env-options" "^0.1.4" + "@endo/far" "^0.2.22" + "@endo/pass-style" "^0.1.7" + "@endo/patterns" "^0.2.6" + "@endo/far@0.2.18", "@endo/far@^0.2.18", "@endo/far@^0.2.3": version "0.2.18" resolved "https://registry.yarnpkg.com/@endo/far/-/far-0.2.18.tgz#8d8ca8ac1f7c4b57871e55c2c2f06c8e4fcf3839" @@ -966,6 +1207,14 @@ "@endo/eventual-send" "^0.17.2" "@endo/pass-style" "^0.1.3" +"@endo/far@^0.2.22": + version "0.2.22" + resolved "https://registry.yarnpkg.com/@endo/far/-/far-0.2.22.tgz#fda187289a903ee3f9d6dcc5664ee7fef1994b1f" + integrity sha512-LFOicqyHslKOSk/H5EfGOcw347ftDSwYHARPasnrG4UJOEkcU1ZG5bN/BmfONtcidB776gWZKrV/tNl4WLIlyw== + dependencies: + "@endo/eventual-send" "^0.17.6" + "@endo/pass-style" "^0.1.7" + "@endo/import-bundle@0.3.4": version "0.3.4" resolved "https://registry.yarnpkg.com/@endo/import-bundle/-/import-bundle-0.3.4.tgz#dd93dca2aa595f669365f05d03affd4465837919" @@ -974,6 +1223,14 @@ "@endo/base64" "^0.2.31" "@endo/compartment-mapper" "^0.8.4" +"@endo/import-bundle@^0.3.4": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@endo/import-bundle/-/import-bundle-0.3.5.tgz#b2b21f58c9fd077857754ccb7e9d0a91868de88d" + integrity sha512-jYBXGnvWhw4w/N8ZPGar+sftNg/wgI5mCCE0ooUYMUBvM5ulPHI+/KYW3FD9pSwU8h7d5Nvs3bvA4w6dN7b67A== + dependencies: + "@endo/base64" "^0.2.32" + "@endo/compartment-mapper" "^0.8.5" + "@endo/init@0.5.56", "@endo/init@^0.5.56": version "0.5.56" resolved "https://registry.yarnpkg.com/@endo/init/-/init-0.5.56.tgz#c241de519434309f362dc676e76ee36c93240151" @@ -984,6 +1241,16 @@ "@endo/lockdown" "^0.1.28" "@endo/promise-kit" "^0.2.56" +"@endo/init@^0.5.60": + version "0.5.60" + resolved "https://registry.yarnpkg.com/@endo/init/-/init-0.5.60.tgz#e78051b13cd4a04c72d5ec1d2a6011b7f987f7ff" + integrity sha512-AbAvs6Nk01fyJ+PaW0RzwemIWyomjzDf8ZEhVa3jCOhr8kBBsTnJdX0v7XkbZ/Y8NQxlrFaW0fPqlJK6aMWTlQ== + dependencies: + "@endo/base64" "^0.2.35" + "@endo/eventual-send" "^0.17.6" + "@endo/lockdown" "^0.1.32" + "@endo/promise-kit" "^0.2.60" + "@endo/lockdown@0.1.28", "@endo/lockdown@^0.1.28": version "0.1.28" resolved "https://registry.yarnpkg.com/@endo/lockdown/-/lockdown-0.1.28.tgz#43f23dcbb12b6ebd3ad2a3dc8c6bb3609dd9e95f" @@ -991,6 +1258,13 @@ dependencies: ses "^0.18.4" +"@endo/lockdown@^0.1.32": + version "0.1.32" + resolved "https://registry.yarnpkg.com/@endo/lockdown/-/lockdown-0.1.32.tgz#2d13a9ca336d5dce243a3cf919c543b55973153c" + integrity sha512-AN696XS3robsopxVg7gc/6c9TXPGosGmKfcM0g9SNnD1rqgo1EakS4wf7f3AbICU9iJdo0e4V5JjzWPnjqoR0g== + dependencies: + ses "^0.18.8" + "@endo/marshal@0.8.5", "@endo/marshal@^0.8.5": version "0.8.5" resolved "https://registry.yarnpkg.com/@endo/marshal/-/marshal-0.8.5.tgz#c1a10ed4d9b37ee7444d314d8dec9a9a96728d64" @@ -1001,11 +1275,26 @@ "@endo/pass-style" "^0.1.3" "@endo/promise-kit" "^0.2.56" +"@endo/marshal@^0.8.9": + version "0.8.9" + resolved "https://registry.yarnpkg.com/@endo/marshal/-/marshal-0.8.9.tgz#f6fcaf23ecad828f6d086657f1d1590ea8ef3840" + integrity sha512-wzYlY5/JFzY/wAVxZ6h0BxlRaAS/9KKnhircKO/tGw5bZYHFvLeSeMCBZ4VCSZg5aNgDlhuvB0S6iCwS5MYqcg== + dependencies: + "@endo/eventual-send" "^0.17.6" + "@endo/nat" "^4.1.31" + "@endo/pass-style" "^0.1.7" + "@endo/promise-kit" "^0.2.60" + "@endo/nat@4.1.27", "@endo/nat@^4.1.27": version "4.1.27" resolved "https://registry.yarnpkg.com/@endo/nat/-/nat-4.1.27.tgz#8f1a398b39f994b0769070a3fb36d3397bf86794" integrity sha512-mKRdIc4NvrxZ1qPBcYZH6zaj0RsRwADaCcfPNRnGWcHC9dY8DmZDDcgqNdSBFLiEto1RnXeoKAEGxk6hn253Ow== +"@endo/nat@^4.1.31": + version "4.1.31" + resolved "https://registry.yarnpkg.com/@endo/nat/-/nat-4.1.31.tgz#ca738f472481a572f47749b41529b3261ebb4c1e" + integrity sha512-tz0PnEmzX9BAtKEawYndsx+XC6f+2CKErtrpbpOuX3uct5VNLdw6q6cArSYtnHbxRHR0YaHUdeG0W6okmup4bg== + "@endo/netstring@0.3.26": version "0.3.26" resolved "https://registry.yarnpkg.com/@endo/netstring/-/netstring-0.3.26.tgz#7da8338cb372772894e1ebcc0728b23666fa2c89" @@ -1015,6 +1304,15 @@ "@endo/stream" "^0.3.25" ses "^0.18.4" +"@endo/netstring@^0.3.26": + version "0.3.30" + resolved "https://registry.yarnpkg.com/@endo/netstring/-/netstring-0.3.30.tgz#ee0f29c4fc33674733833610129136435b56b044" + integrity sha512-Z3e2duj7Qumt+xm1RVQq/O74ORfM87WBXgBQyxIgTAxBT1o0qjR+BnPBWSyzWg4+JBtax0qgge8KiKpfoECa4g== + dependencies: + "@endo/init" "^0.5.60" + "@endo/stream" "^0.3.29" + ses "^0.18.8" + "@endo/pass-style@0.1.3", "@endo/pass-style@^0.1.3": version "0.1.3" resolved "https://registry.yarnpkg.com/@endo/pass-style/-/pass-style-0.1.3.tgz#951056a2869b04f2aab0928b61a91ae7252ddbe4" @@ -1023,6 +1321,14 @@ "@endo/promise-kit" "^0.2.56" "@fast-check/ava" "^1.1.3" +"@endo/pass-style@^0.1.7": + version "0.1.7" + resolved "https://registry.yarnpkg.com/@endo/pass-style/-/pass-style-0.1.7.tgz#ea22568e8b86fb2d1a14a5fc042374cc0d8e310b" + integrity sha512-dlB62Ptjcy/+iachy7qzAdgIwaU60rE+XLummLRpE2tDSJF2jSFJlVwa/QuGw1KKO7Rt4vog/51sKev3EbJZQg== + dependencies: + "@endo/promise-kit" "^0.2.60" + "@fast-check/ava" "^1.1.5" + "@endo/patterns@0.2.2", "@endo/patterns@^0.2.2": version "0.2.2" resolved "https://registry.yarnpkg.com/@endo/patterns/-/patterns-0.2.2.tgz#d4c4d63bf450477ed9a9cf194b4a8daa56fcb4f4" @@ -1032,6 +1338,15 @@ "@endo/marshal" "^0.8.5" "@endo/promise-kit" "^0.2.56" +"@endo/patterns@^0.2.6": + version "0.2.6" + resolved "https://registry.yarnpkg.com/@endo/patterns/-/patterns-0.2.6.tgz#abbbc3743ee313ffc6167d783d5fc78de74125fe" + integrity sha512-FbayXMv9sY4qP5vSaPhq9RSJmsTykImbCy0FN1YmZzaChGwOfSPOJw4898xVLDK5Xi6f+6zV02uXjuMTuZt6UA== + dependencies: + "@endo/eventual-send" "^0.17.6" + "@endo/marshal" "^0.8.9" + "@endo/promise-kit" "^0.2.60" + "@endo/promise-kit@0.2.56", "@endo/promise-kit@^0.2.56": version "0.2.56" resolved "https://registry.yarnpkg.com/@endo/promise-kit/-/promise-kit-0.2.56.tgz#24ed3cf87af1eec65f4635643b7e67617b909e71" @@ -1039,6 +1354,13 @@ dependencies: ses "^0.18.4" +"@endo/promise-kit@^0.2.59", "@endo/promise-kit@^0.2.60": + version "0.2.60" + resolved "https://registry.yarnpkg.com/@endo/promise-kit/-/promise-kit-0.2.60.tgz#8012ada06970c7eaf965cd856563b34a1790e163" + integrity sha512-6Zp9BqBbc3ywaG+iLRrQRmO/VLKrMnvsbgOKKPMpjEC3sUlksYA09uaH3GrKZgoGChF8m9bXK8eFW39z7wJNUw== + dependencies: + ses "^0.18.8" + "@endo/ses-ava@0.2.40", "@endo/ses-ava@^0.2.40": version "0.2.40" resolved "https://registry.yarnpkg.com/@endo/ses-ava/-/ses-ava-0.2.40.tgz#8a6c1f668131ecbe4d06339cac2a8346253089b8" @@ -1057,6 +1379,28 @@ "@babel/types" "^7.17.0" ses "^0.18.4" +"@endo/static-module-record@^0.7.20": + version "0.7.20" + resolved "https://registry.yarnpkg.com/@endo/static-module-record/-/static-module-record-0.7.20.tgz#5d9583aaa8042b8a6de58c72f765e5a28e880489" + integrity sha512-qpow712L7Bh7F3olFW9e15PcDWnC2eSY4xPdhpZoYTzedsyjCETRgxFWY6+DdT193lNlyKIn0On1O1Go+5WmBA== + dependencies: + "@agoric/babel-generator" "^7.17.6" + "@babel/parser" "^7.17.3" + "@babel/traverse" "^7.17.3" + "@babel/types" "^7.17.0" + ses "^0.18.5" + +"@endo/static-module-record@^0.8.2": + version "0.8.2" + resolved "https://registry.yarnpkg.com/@endo/static-module-record/-/static-module-record-0.8.2.tgz#25f66d555d1a075e5258520405410fd01fc2d1f7" + integrity sha512-wHJLX/hU/MoSFvnFN9sZ/49DYPlbASHlVQrJszeKH3xIpBtl3SG4JdRswO6RQgLREQJD/HV/ZN5V8x2bCpMu4Q== + dependencies: + "@agoric/babel-generator" "^7.17.6" + "@babel/parser" "^7.17.3" + "@babel/traverse" "^7.17.3" + "@babel/types" "^7.17.0" + ses "^0.18.8" + "@endo/stream-node@0.2.26": version "0.2.26" resolved "https://registry.yarnpkg.com/@endo/stream-node/-/stream-node-0.2.26.tgz#bf3c6ce6c506cde4468a64d220b8df4224638e16" @@ -1066,6 +1410,15 @@ "@endo/stream" "^0.3.25" ses "^0.18.4" +"@endo/stream-node@^0.2.26": + version "0.2.30" + resolved "https://registry.yarnpkg.com/@endo/stream-node/-/stream-node-0.2.30.tgz#4af1989976eaad385663cd2a3342072cf9dbea7c" + integrity sha512-KZZJ6MWeTxFYScuuIj5BwGVX6Y5F9+RzW8RhVZy7Najr/irgdGnF/oGk8QeUIHuVzTgL4HLJP+XATnHaLKOcGw== + dependencies: + "@endo/init" "^0.5.60" + "@endo/stream" "^0.3.29" + ses "^0.18.8" + "@endo/stream@0.3.25", "@endo/stream@^0.3.25": version "0.3.25" resolved "https://registry.yarnpkg.com/@endo/stream/-/stream-0.3.25.tgz#a49b012b62f345e3de6b360dc30ec27cc32a455f" @@ -1075,11 +1428,30 @@ "@endo/promise-kit" "^0.2.56" ses "^0.18.4" +"@endo/stream@^0.3.29": + version "0.3.29" + resolved "https://registry.yarnpkg.com/@endo/stream/-/stream-0.3.29.tgz#f49c24629429a3650ddd0e5e9fb90e36ef44ed0a" + integrity sha512-C850JqDGYsObE0fAC2uUw/IrN3kUpECddiARIGDpe/y3wnWu5fsau52FkGOKY4lno5kyAhfyvZ9MxhigYnXxEg== + dependencies: + "@endo/eventual-send" "^0.17.6" + "@endo/promise-kit" "^0.2.60" + ses "^0.18.8" + +"@endo/where@^0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@endo/where/-/where-0.3.5.tgz#df7661ec38ab6a327ef050aa88b50555876c39ef" + integrity sha512-y9agS7UWpSY9YSAAYwtn6sAE7zfU2BmYGOUJpw859WcmRt5ufCRi2XAXDcvIugAUPTsSVPqJj6FO3uZNVRmXPw== + "@endo/zip@0.2.31", "@endo/zip@^0.2.31": version "0.2.31" resolved "https://registry.yarnpkg.com/@endo/zip/-/zip-0.2.31.tgz#371b1a9ca8b3216ad8a3564e97e3d747be42a657" integrity sha512-rNCZtQzPm6Q8kW69gyeU0hUwKZtwuR8cX1+URgpDuUuaMUbKWBaqURKOmrqKVtE5fkqCE7pSrHvGH02DMDbDHQ== +"@endo/zip@^0.2.32", "@endo/zip@^0.2.35": + version "0.2.35" + resolved "https://registry.yarnpkg.com/@endo/zip/-/zip-0.2.35.tgz#37a7f9266ca9c9167de5e42b55b0d9c979598d87" + integrity sha512-UM+mMZjBtJf33lXj38xXIEIe1B5wrgg/nT9CHrC8s+Pj/h63eMpQmcJzjL2vMKrvq3Tsj+TDzmQhtYcbrFACqQ== + "@es-joy/jsdoccomment@~0.40.1": version "0.40.1" resolved "https://registry.yarnpkg.com/@es-joy/jsdoccomment/-/jsdoccomment-0.40.1.tgz#13acd77fb372ed1c83b7355edd865a3b370c9ec4" @@ -1121,7 +1493,7 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.48.0.tgz#642633964e217905436033a2bd08bf322849b7fb" integrity sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw== -"@fast-check/ava@^1.1.3": +"@fast-check/ava@^1.1.3", "@fast-check/ava@^1.1.5": version "1.1.6" resolved "https://registry.yarnpkg.com/@fast-check/ava/-/ava-1.1.6.tgz#fb7d1b7f84c26f892c09937e62200ea2f3ccd9eb" integrity sha512-xshsWNumcefyXFyfxREFcz/mrOay3ITRJh8My7bpEPK3YEJ7Rg/QHExVCUMRqeMjxj5ChQz6ngZxNCnxkOWq+w== @@ -5681,7 +6053,7 @@ serve-static@1.15.0: parseurl "~1.3.3" send "0.18.0" -ses@^0.18.4: +ses@^0.18.4, ses@^0.18.5, ses@^0.18.8: version "0.18.8" resolved "https://registry.yarnpkg.com/ses/-/ses-0.18.8.tgz#88036511ac3b3c07e4d82dd8cfc6e5f3788205b6" integrity sha512-kOH1AhJc6gWDXKURKeU1w7iFUdImAegAljVvBg5EUBgNqjH4bxcEsGVUadVEPtA2PVRMyQp1fiSMDwEZkQNj1g==