From d8b9cb8b5d9be7c93770344e8b00e35edc09fdf7 Mon Sep 17 00:00:00 2001 From: Pandelis Symeonidis Date: Mon, 30 Oct 2023 17:01:16 +0100 Subject: [PATCH 01/17] set up test environment with null upgrade --- agoric/.yarnrc | 5 - agoric/contract/package.json | 2 + agoric/contract/src/index.js | 4 +- .../src/proposal/start-kread-proposal.js | 2 +- agoric/contract/test/bootstrap.js | 2 +- .../swingsetTests/bootstrap-upgradable.js | 290 ++++++++++++++ .../test/swingsetTests/test-kread-upgrade.js | 92 +++++ agoric/yarn.lock | 376 +++++++++++++++++- 8 files changed, 762 insertions(+), 11 deletions(-) delete mode 100644 agoric/.yarnrc create mode 100644 agoric/contract/test/swingsetTests/bootstrap-upgradable.js create mode 100644 agoric/contract/test/swingsetTests/test-kread-upgrade.js diff --git a/agoric/.yarnrc b/agoric/.yarnrc deleted file mode 100644 index 54b284145..000000000 --- a/agoric/.yarnrc +++ /dev/null @@ -1,5 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -yarn-path ".yarn/releases/yarn-1.22.5.js" 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/index.js b/agoric/contract/src/index.js index 3202eab66..1bcb3e26b 100644 --- a/agoric/contract/src/index.js +++ b/agoric/contract/src/index.js @@ -74,7 +74,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 @@ -178,4 +178,4 @@ export const start = async (zcf, privateArgs, baggage) => { }); }; -harden(start); +harden(prepare); diff --git a/agoric/contract/src/proposal/start-kread-proposal.js b/agoric/contract/src/proposal/start-kread-proposal.js index d7c4e3c27..e775bd2d2 100644 --- a/agoric/contract/src/proposal/start-kread-proposal.js +++ b/agoric/contract/src/proposal/start-kread-proposal.js @@ -328,7 +328,7 @@ 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/test/bootstrap.js b/agoric/contract/test/bootstrap.js index 6607e2650..5b404a2ce 100644 --- a/agoric/contract/test/bootstrap.js +++ b/agoric/contract/test/bootstrap.js @@ -103,7 +103,7 @@ export const bootstrapContext = async (conf = undefined) => { character: { issuer: characterIssuer, brand: characterBrand }, item: { issuer: itemIssuer, brand: itemBrand }, }; - + console.log("TERMS: ", terms) const purses = { character: characterIssuer.makeEmptyPurse(), item: itemIssuer.makeEmptyPurse(), diff --git a/agoric/contract/test/swingsetTests/bootstrap-upgradable.js b/agoric/contract/test/swingsetTests/bootstrap-upgradable.js new file mode 100644 index 000000000..39a2bb257 --- /dev/null +++ b/agoric/contract/test/swingsetTests/bootstrap-upgradable.js @@ -0,0 +1,290 @@ +import { Far, deeplyFulfilled } from '@endo/marshal'; +import { E } from '@endo/eventual-send'; +import { makeFakeStorageKit } from '@agoric/internal/src/storage-test-utils'; +import { makeNameHubKit } from '@agoric/vats'; +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 { AmountMath, 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 { AdminFacetI } from '@agoric/zoe/src/typeGuards.js'; + +const trace = makeTracer('kreadBootUpgrade'); + +const kreadV1BundleName = 'kreadV1'; + +export const buildRootObject = async () => { + let vatAdmin; + let initialPoserInvitation; + let electorateInvitationAmount; + let zoe; + let governedInstance; + let creatorFacet; + let publicFacet; + let contractAssets; + let purses; + let governorFacets; + + const storageKit = makeFakeStorageKit('kread'); + const { nameAdmin: namesByAddressAdmin } = makeNameHubKit(); + const timer = buildManualTimer(); + const clock = await E(timer).getClock(); + const marshaller = makeFakeBoard().getReadonlyMarshaller(); + const installations = {}; + 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; + }; + + 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', + ); + zoe = 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 () => { + 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(); + + publicFacet = await E(governorFacets.creatorFacet).getPublicFacet(); + creatorFacet = await E(governorFacets.creatorFacet).getCreatorFacet(); + + await E(creatorFacet).initializeBaseAssets( + defaultCharacters, + defaultItems, + ); + 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; + + 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(), + }; + }, + mintCharacter: async (name) => { + const purse = issuerMockIST.makeEmptyPurse(); + const payout = mintMockIST.mintPayment( + AmountMath.make(brandMockIST, harden(100000000000n)), + ); + purse.deposit(payout); + + const mintCharacterInvitation = await E( + publicFacet, + ).makeMintCharacterInvitation(); + + const priceAmount = AmountMath.make(brandMockIST, 30000000n); + + const proposal = harden({ + give: { Price: priceAmount }, + }); + + const payment = { Price: purse.withdraw(priceAmount) }; + const offerArgs = harden({ name }); + + const userSeat = await E(zoe).offer( + mintCharacterInvitation, + proposal, + payment, + offerArgs, + ); + + const result = await E(userSeat).getOfferResult(); + assert(result, 'Character NFT minted successfully!'); + + const characters = await E(publicFacet).getCharacters(); + assert(characters[0].name, offerArgs.name); + + const payout2 = await E(userSeat).getPayout('Asset'); + + await E(purses.character).deposit(payout2); + assert( + offerArgs.name, + (await E(purses.character).getCurrentAmount()).value.payload[0][0].name, + ); + }, + 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") + } + }); +}; diff --git a/agoric/contract/test/swingsetTests/test-kread-upgrade.js b/agoric/contract/test/swingsetTests/test-kread-upgrade.js new file mode 100644 index 000000000..589d38df9 --- /dev/null +++ b/agoric/contract/test/swingsetTests/test-kread-upgrade.js @@ -0,0 +1,92 @@ +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 modulePath = async (sourceRoot) => { + const url = await importMetaResolve(sourceRoot, import.meta.url); + return new URL(url).pathname; +}; + +test('kread upgrade', async (t) => { + /** @type {SwingSetConfig} */ + const config = { + includeDevDependencies: true, + defaultManagerType: 'local', + bootstrap: 'bootstrap', + vats: { + bootstrap: { + sourceSpec: bfile('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/index.js', + import.meta.url, + ).then((href) => new URL(href).pathname), + }, + }, + }; + + t.log('buildVatController'); + const c = await buildVatController(config); + t.log(c.vatNameToID('bootstrap')); + + c.pinVatRoot('bootstrap'); + t.log('run-controller'); + await c.run(); + + 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]; + }; + + t.log('create initial version'); + const [buildV1] = await run('buildV1', []); + t.is(buildV1, 'fulfilled'); + + t.log('test functionality'); + const [mintCharacter] = await run('mintCharacter', ["example"]); + t.is(mintCharacter, 'fulfilled'); + + t.log('test null upgrade'); + const [nullUpgrade] = await run('nullUpgrade', []); + t.is(nullUpgrade, 'fulfilled'); + + t.log('test functionality after upgrade'); + const [mintCharacterAfterUpgrade] = await run('mintCharacter', ["example"]); + t.is(mintCharacterAfterUpgrade, 'rejected'); // name has already been minted +}); 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== From 15c0af175a2afeaf92d79f6ffc17df5e7f2e870a Mon Sep 17 00:00:00 2001 From: Pandelis Symeonidis Date: Wed, 1 Nov 2023 09:01:44 +0100 Subject: [PATCH 02/17] setup tests --- agoric/contract/src/utils.js | 2 +- .../bootstrap/bootstrap-inventory.js | 0 .../bootstrap/bootstrap-market.js | 181 +++++++++++++++++ .../swingsetTests/bootstrap/bootstrap-mint.js | 88 ++++++++ .../{ => bootstrap}/bootstrap-upgradable.js | 87 ++++---- .../bootstrap/make-bootstrap-users.js | 79 +++++++ .../test/swingsetTests/bootstrap/utils.js | 149 ++++++++++++++ agoric/contract/test/swingsetTests/data.js | 12 ++ agoric/contract/test/swingsetTests/flow.js | 112 ++++++++++ agoric/contract/test/swingsetTests/items.js | 192 ++++++++++++++++++ ...est-kread-upgrade.js => swingset-setup.js} | 50 ++--- .../test/swingsetTests/test-inventory.js | 8 + .../test/swingsetTests/test-market.js | 21 ++ .../test/swingsetTests/test-minting.js | 16 ++ agoric/contract/test/swingsetTests/text.js | 6 + agoric/contract/test/swingsetTests/types.js | 59 ++++++ 16 files changed, 975 insertions(+), 87 deletions(-) create mode 100644 agoric/contract/test/swingsetTests/bootstrap/bootstrap-inventory.js create mode 100644 agoric/contract/test/swingsetTests/bootstrap/bootstrap-market.js create mode 100644 agoric/contract/test/swingsetTests/bootstrap/bootstrap-mint.js rename agoric/contract/test/swingsetTests/{ => bootstrap}/bootstrap-upgradable.js (82%) create mode 100644 agoric/contract/test/swingsetTests/bootstrap/make-bootstrap-users.js create mode 100644 agoric/contract/test/swingsetTests/bootstrap/utils.js create mode 100644 agoric/contract/test/swingsetTests/data.js create mode 100644 agoric/contract/test/swingsetTests/flow.js create mode 100644 agoric/contract/test/swingsetTests/items.js rename agoric/contract/test/swingsetTests/{test-kread-upgrade.js => swingset-setup.js} (56%) create mode 100644 agoric/contract/test/swingsetTests/test-inventory.js create mode 100644 agoric/contract/test/swingsetTests/test-market.js create mode 100644 agoric/contract/test/swingsetTests/test-minting.js create mode 100644 agoric/contract/test/swingsetTests/text.js create mode 100644 agoric/contract/test/swingsetTests/types.js diff --git a/agoric/contract/src/utils.js b/agoric/contract/src/utils.js index 7767a179a..39fe4f110 100644 --- a/agoric/contract/src/utils.js +++ b/agoric/contract/src/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/test/swingsetTests/bootstrap/bootstrap-inventory.js b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-inventory.js new file mode 100644 index 000000000..e69de29bb 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..a365b8d3e --- /dev/null +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market.js @@ -0,0 +1,181 @@ +import { AmountMath } from '@agoric/ertp'; +import { E } from '@endo/eventual-send'; +import { flow } from '../flow.js'; +import { makeCopyBag } from '@agoric/store'; +import { addCharacterToContext } from './utils.js'; +import { makeKreadUser } from './make-bootstrap-users.js'; +import { errors } from '../../../src/errors.js'; + +export async function setupMarketTests(context) { + await addCharacterToContext(context); + // await addItemToBootstrap(bootstrap, { + // 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 admin = makeKreadUser('admin', { + // character: await E(contractAssets.character.issuer).makeEmptyPurse(), + // item: await E(contractAssets.item.issuer).makeEmptyPurse(), + // payment: await E(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 {Bootstrap} */ + const { + publicFacet, + contractAssets, + zoe, + users: { bob }, + paymentAsset, + } = 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 is successfully added to market', + ); + + assert.equal( + (await bob.getCharacters()).length, + 0, + "Character is no longer in bob's wallet", + ); +} + +export async function buyCharacterOfferLessThanAskingPrice(context) { + /** @type {Bootstrap} */ + 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; + } +} 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..a988a65d7 --- /dev/null +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-mint.js @@ -0,0 +1,88 @@ +import { AmountMath } from '@agoric/ertp'; +import { E } from '@endo/eventual-send'; +import { flow } from '../flow.js'; + +export async function mintCharacterNameTooLong(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: alice.withdrawPayment(priceAmount) }; + + const proposal = harden({ + give: { Price: priceAmount }, + }); + + const userSeat = await E(zoe).offer( + mintCharacterInvitation, + proposal, + payment, + offerArgs, + ); + + // await t.throwsAsync(E(userSeat).getOfferResult(), 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', + ); + + alice.depositPayment(await E(userSeat).getPayout('Price')); +} + +export async function mintCharacterExpectedFlow(context, name) { + /** @type {Context} */ + const { publicFacet, purses, paymentAsset, zoe } = context; + + const purse = paymentAsset.issuerMockIST.makeEmptyPurse(); + const payout = paymentAsset.mintMockIST.mintPayment( + AmountMath.make(paymentAsset.brandMockIST, harden(100000000000n)), + ); + purse.deposit(payout); + + const mintCharacterInvitation = await E( + publicFacet, + ).makeMintCharacterInvitation(); + + const priceAmount = AmountMath.make(paymentAsset.brandMockIST, 30000000n); + + const proposal = harden({ + give: { Price: priceAmount }, + }); + + const payment = { Price: purse.withdraw(priceAmount) }; + const offerArgs = harden({ name }); + + const userSeat = await E(zoe).offer( + mintCharacterInvitation, + proposal, + payment, + offerArgs, + ); + + const result = await E(userSeat).getOfferResult(); + assert.equal(result, 'Character NFT minted successfully!'); + + const characters = await E(publicFacet).getCharacters(); + assert.equal(characters[0].name, offerArgs.name); + + const payout2 = await E(userSeat).getPayout('Asset'); + + await E(purses.character).deposit(payout2); + assert( + offerArgs.name, + (await E(purses.character).getCurrentAmount()).value.payload[0][0].name, + ); +} diff --git a/agoric/contract/test/swingsetTests/bootstrap-upgradable.js b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js similarity index 82% rename from agoric/contract/test/swingsetTests/bootstrap-upgradable.js rename to agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js index 39a2bb257..794eb88fc 100644 --- a/agoric/contract/test/swingsetTests/bootstrap-upgradable.js +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js @@ -6,12 +6,13 @@ 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 { AmountMath, makeIssuerKit } from '@agoric/ertp'; +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 { AdminFacetI } from '@agoric/zoe/src/typeGuards.js'; +import { defaultCharacters } from '../../characters.js'; +import { defaultItems } from '../../items.js'; +import { mintCharacterExpectedFlow } from './bootstrap-mint.js'; +import { setupMarketTests, sellCharacter, buyCharacterOfferLessThanAskingPrice } from './bootstrap-market.js'; const trace = makeTracer('kreadBootUpgrade'); @@ -29,6 +30,8 @@ export const buildRootObject = async () => { let purses; let governorFacets; + let context; + const storageKit = makeFakeStorageKit('kread'); const { nameAdmin: namesByAddressAdmin } = makeNameHubKit(); const timer = buildManualTimer(); @@ -232,59 +235,43 @@ export const buildRootObject = async () => { item: await E(itemIssuer).makeEmptyPurse(), payment: issuerMockIST.makeEmptyPurse(), }; + context = { + contractAssets, + purses, + paymentAsset: { + mintMockIST, + issuerMockIST, + brandMockIST, + }, + publicFacet, + zoe, + }; }, mintCharacter: async (name) => { - const purse = issuerMockIST.makeEmptyPurse(); - const payout = mintMockIST.mintPayment( - AmountMath.make(brandMockIST, harden(100000000000n)), - ); - purse.deposit(payout); - - const mintCharacterInvitation = await E( - publicFacet, - ).makeMintCharacterInvitation(); - - const priceAmount = AmountMath.make(brandMockIST, 30000000n); - - const proposal = harden({ - give: { Price: priceAmount }, - }); - - const payment = { Price: purse.withdraw(priceAmount) }; - const offerArgs = harden({ name }); - - const userSeat = await E(zoe).offer( - mintCharacterInvitation, - proposal, - payment, - offerArgs, - ); - - const result = await E(userSeat).getOfferResult(); - assert(result, 'Character NFT minted successfully!'); - - const characters = await E(publicFacet).getCharacters(); - assert(characters[0].name, offerArgs.name); - - const payout2 = await E(userSeat).getPayout('Asset'); - - await E(purses.character).deposit(payout2); - assert( - offerArgs.name, - (await E(purses.character).getCurrentAmount()).value.payload[0][0].name, - ); + await mintCharacterExpectedFlow(context, name); + }, + setupMarketTests: async () => { + context = await setupMarketTests(context) + }, + sellCharacter: async () => { + await sellCharacter(context); + }, + buyCharacterOfferLessThanAskingPrice: async () => { + await buyCharacterOfferLessThanAskingPrice(context); }, nullUpgrade: async () => { - trace("start null upgrade") + trace('start null upgrade'); const bundleId = await E(vatAdmin).getBundleIDByName(kreadV1BundleName); - const kreadAdminFacet = await E(governorFacets.creatorFacet).getAdminFacet(); + 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") - } + initialPoserInvitation, + }); + assert.equal(upgradeResult.incarnationNumber, 1); + trace('null upgrade completed'); + }, }); }; 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..73330236d --- /dev/null +++ b/agoric/contract/test/swingsetTests/bootstrap/make-bootstrap-users.js @@ -0,0 +1,79 @@ +import { E } from '@endo/eventual-send'; + +/** + * @typedef {{ + * character: Purse + * item: Purse + * payment: Purse + * }} Purses + */ + +/** + * 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 {{ + * name: string + * purses: Purses + * getItems: () => Item[] + * getCharacters: () => any[] + * getPaymentBalance: () => bigint + * depositItems: (items) => void + * depositCharacters: (characters) => void + * depositPayment: (payment) => void + * withdrawItems: (items) => Payment + * withdrawCharacters: (characters) => Payment + * withdrawPayment: (payment) => Payment + * getSeat: () => any + * setMarketSeat: (seat) => void + * }} + */ +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 = (characters) => + 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..3999eb2db --- /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 {Bootstrap} bootstrap + */ +export const addItemToBootstrap = async (bootstrap, item) => { + /** @type {Bootstrap} */ + const { creatorFacet, contractAssets, purses, zoe } = bootstrap; + + 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'); + purses.item.deposit(payout); +}; +harden(addItemToBootstrap); + +/** + * @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..e65a79049 --- /dev/null +++ b/agoric/contract/test/swingsetTests/flow.js @@ -0,0 +1,112 @@ +import { errors } from '../../src/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/test-kread-upgrade.js b/agoric/contract/test/swingsetTests/swingset-setup.js similarity index 56% rename from agoric/contract/test/swingsetTests/test-kread-upgrade.js rename to agoric/contract/test/swingsetTests/swingset-setup.js index 589d38df9..223a9dcb4 100644 --- a/agoric/contract/test/swingsetTests/test-kread-upgrade.js +++ b/agoric/contract/test/swingsetTests/swingset-setup.js @@ -7,20 +7,25 @@ import { assert } from '@agoric/assert'; const bfile = (name) => new URL(name, import.meta.url).pathname; const kreadV1BundleName = 'kreadV1'; -const modulePath = async (sourceRoot) => { - const url = await importMetaResolve(sourceRoot, import.meta.url); - return new URL(url).pathname; +let c; + +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]; }; -test('kread upgrade', async (t) => { - /** @type {SwingSetConfig} */ +export async function setup() { const config = { includeDevDependencies: true, defaultManagerType: 'local', bootstrap: 'bootstrap', vats: { bootstrap: { - sourceSpec: bfile('bootstrap-upgradable.js'), + sourceSpec: bfile('bootstrap/bootstrap-upgradable.js'), }, zoe: { sourceSpec: await importMetaResolve( @@ -57,36 +62,9 @@ test('kread upgrade', async (t) => { }, }; - t.log('buildVatController'); - const c = await buildVatController(config); - t.log(c.vatNameToID('bootstrap')); - + c = await buildVatController(config); c.pinVatRoot('bootstrap'); - t.log('run-controller'); await c.run(); - 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]; - }; - - t.log('create initial version'); - const [buildV1] = await run('buildV1', []); - t.is(buildV1, 'fulfilled'); - - t.log('test functionality'); - const [mintCharacter] = await run('mintCharacter', ["example"]); - t.is(mintCharacter, 'fulfilled'); - - t.log('test null upgrade'); - const [nullUpgrade] = await run('nullUpgrade', []); - t.is(nullUpgrade, 'fulfilled'); - - t.log('test functionality after upgrade'); - const [mintCharacterAfterUpgrade] = await run('mintCharacter', ["example"]); - t.is(mintCharacterAfterUpgrade, 'rejected'); // name has already been minted -}); + await run('buildV1', []); +} diff --git a/agoric/contract/test/swingsetTests/test-inventory.js b/agoric/contract/test/swingsetTests/test-inventory.js new file mode 100644 index 000000000..383900711 --- /dev/null +++ b/agoric/contract/test/swingsetTests/test-inventory.js @@ -0,0 +1,8 @@ +import { test } from '../prepare-test-env-ava.js'; +import { setup } from './swingset-setup.js'; + +test.before(async (t) => { + await setup(); +}); + +// test("") diff --git a/agoric/contract/test/swingsetTests/test-market.js b/agoric/contract/test/swingsetTests/test-market.js new file mode 100644 index 000000000..de3b6b1d3 --- /dev/null +++ b/agoric/contract/test/swingsetTests/test-market.js @@ -0,0 +1,21 @@ +import { test } from '../prepare-test-env-ava.js'; +import { setup, run } from './swingset-setup.js'; + +test.before(async (t) => { + await setup(); + 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'); + }, +); diff --git a/agoric/contract/test/swingsetTests/test-minting.js b/agoric/contract/test/swingsetTests/test-minting.js new file mode 100644 index 000000000..d85450dbb --- /dev/null +++ b/agoric/contract/test/swingsetTests/test-minting.js @@ -0,0 +1,16 @@ +import { test } from '../prepare-test-env-ava.js'; +import { setup, run } from './swingset-setup.js'; + +test.before(async (t) => { + await setup(); +}); + +test.serial('--| MINT - Too Long Name', async (t) => { + const [mintCharacter] = await run('mintCharacter', ['example']); + t.is(mintCharacter, 'fulfilled'); +}); + +test.serial('--| MINT - Expected flow', async (t) => { + const [mintCharacter] = await run('mintCharacter', ['example']); + t.is(mintCharacter, 'rejected'); +}); 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..be83ea151 --- /dev/null +++ b/agoric/contract/test/swingsetTests/types.js @@ -0,0 +1,59 @@ +/** + * 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 + */ + +// XXX approximate +/** + * Testing context + * + * @typedef {{ + * assets: Assets; + * creatorFacet: unknown; + * contractAssets: ContractAssets; + * publicFacet: unknown; + * zoe: ZoeService; + * purses: { + * character: any + * item: any + * payment: any + * }; + * }} Context + */ From f904f1fdf5101a93dd926188054572abc6f3dcaf Mon Sep 17 00:00:00 2001 From: Pandelis Symeonidis Date: Wed, 1 Nov 2023 15:35:51 +0100 Subject: [PATCH 03/17] add market tests --- .../bootstrap/bootstrap-market.js | 685 +++++++++++++++++- .../bootstrap/bootstrap-upgradable.js | 66 +- .../bootstrap/make-bootstrap-users.js | 28 +- .../test/swingsetTests/bootstrap/utils.js | 12 +- .../test/swingsetTests/test-market.js | 75 ++ agoric/contract/test/swingsetTests/types.js | 27 + 6 files changed, 826 insertions(+), 67 deletions(-) diff --git a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market.js b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market.js index a365b8d3e..1cda95148 100644 --- a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market.js +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market.js @@ -2,32 +2,34 @@ import { AmountMath } from '@agoric/ertp'; import { E } from '@endo/eventual-send'; import { flow } from '../flow.js'; import { makeCopyBag } from '@agoric/store'; -import { addCharacterToContext } from './utils.js'; +import { addCharacterToContext, addItemToContext } from './utils.js'; import { makeKreadUser } from './make-bootstrap-users.js'; import { errors } from '../../../src/errors.js'; +import { multiplyBy } from '@agoric/zoe/src/contractSupport/ratio.js'; +import { defaultItems } from '../items.js'; export async function setupMarketTests(context) { await addCharacterToContext(context); - // await addItemToBootstrap(bootstrap, { - // 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: '', - // }); + 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; @@ -37,11 +39,6 @@ export async function setupMarketTests(context) { item: await E(contractAssets.item.issuer).makeEmptyPurse(), payment: paymentAsset.issuerMockIST.makeEmptyPurse(), }); - // const admin = makeKreadUser('admin', { - // character: await E(contractAssets.character.issuer).makeEmptyPurse(), - // item: await E(contractAssets.item.issuer).makeEmptyPurse(), - // payment: await E(paymentAsset.issuerMockIST).makeEmptyPurse(), - // }); const payout = await E(paymentAsset.mintMockIST).mintPayment( AmountMath.make(paymentAsset.brandMockIST, harden(100n)), @@ -54,7 +51,7 @@ export async function setupMarketTests(context) { } export async function sellCharacter(context) { - /** @type {Bootstrap} */ + /** @type {Context} */ const { publicFacet, contractAssets, @@ -110,18 +107,18 @@ export async function sellCharacter(context) { assert.equal( charactersForSale.length, 1, - 'Character is successfully added to market', + 'Character was not added to market', ); assert.equal( (await bob.getCharacters()).length, 0, - "Character is no longer in bob's wallet", + "Character is still in bob's wallet", ); } export async function buyCharacterOfferLessThanAskingPrice(context) { - /** @type {Bootstrap} */ + /** @type {Context} */ const { publicFacet, contractAssets, @@ -168,14 +165,638 @@ export async function buyCharacterOfferLessThanAskingPrice(context) { proposal, payment, ); + try { await E(userSeat).getOfferResult(); } catch (error) { - assert.equal(error.message, errors.insufficientFunds) + 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, + } = 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', + ); + + console.log(characterToBuy.askingPrice); + assert.equal( + royaltyPurse.getCurrentAmount().value, + royaltyPursePre + multiplyBy(characterToBuy.askingPrice, royaltyRate).value, + ); + assert.equal( + platformFeePurse.getCurrentAmount().value, + platformFeePursePre + + multiplyBy(characterToBuy.askingPrice, platformFeeRate).value, + ); +} + +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, + } = 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", + ); +} + +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 }, + } = 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', + ); +} + +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 } = 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'); + + // assert.equal((await bob.getItems()).length, 0, "Item is no longer in bob's wallet"); +} + +export async function buyBatchSoldItem(context) { + /** @type {Context} */ + const { + publicFacet, + contractAssets, + zoe, + users: { bob }, + paymentAsset, + } = 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', + ); +} diff --git a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js index 794eb88fc..81081693b 100644 --- a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js @@ -1,7 +1,6 @@ import { Far, deeplyFulfilled } from '@endo/marshal'; import { E } from '@endo/eventual-send'; import { makeFakeStorageKit } from '@agoric/internal/src/storage-test-utils'; -import { makeNameHubKit } from '@agoric/vats'; import { buildManualTimer } from '@agoric/swingset-vat/tools/manual-timer'; import { makeFakeBoard } from '@agoric/vats/tools/board-utils'; import { makeTracer } from '@agoric/internal'; @@ -12,7 +11,22 @@ import { makePromiseKit } from '@endo/promise-kit'; import { defaultCharacters } from '../../characters.js'; import { defaultItems } from '../../items.js'; import { mintCharacterExpectedFlow } from './bootstrap-mint.js'; -import { setupMarketTests, sellCharacter, buyCharacterOfferLessThanAskingPrice } from './bootstrap-market.js'; +import { + setupMarketTests, + sellCharacter, + buyCharacterOfferLessThanAskingPrice, + buyCharacter, + buyCharacterNotOnMarket, + sellItem, + buyItemOfferLessThanAskingPrice, + buyItem, + buyItemNotOnMarket, + buyCharacterOfferMoreThanAskingPrice, + buyItemOfferMoreThanAskingPrice, + internalSellItemBatch, + buyBatchSoldItem, +} from './bootstrap-market.js'; +import { makeRatio } from '@agoric/zoe/src/contractSupport/index.js'; const trace = makeTracer('kreadBootUpgrade'); @@ -33,7 +47,6 @@ export const buildRootObject = async () => { let context; const storageKit = makeFakeStorageKit('kread'); - const { nameAdmin: namesByAddressAdmin } = makeNameHubKit(); const timer = buildManualTimer(); const clock = await E(timer).getClock(); const marshaller = makeFakeBoard().getReadonlyMarshaller(); @@ -244,14 +257,29 @@ export const buildRootObject = async () => { brandMockIST, }, publicFacet, + creatorFacet, zoe, + royaltyPurse, + platformFeePurse, + royaltyRate: makeRatio( + royaltyRate.numerator, + brandMockIST, + royaltyRate.denominator, + brandMockIST, + ), + platformFeeRate: makeRatio( + platformFeeRate.numerator, + brandMockIST, + platformFeeRate.denominator, + brandMockIST, + ), }; }, mintCharacter: async (name) => { await mintCharacterExpectedFlow(context, name); }, setupMarketTests: async () => { - context = await setupMarketTests(context) + context = await setupMarketTests(context); }, sellCharacter: async () => { await sellCharacter(context); @@ -259,6 +287,36 @@ export const buildRootObject = async () => { 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) + }, nullUpgrade: async () => { trace('start null upgrade'); const bundleId = await E(vatAdmin).getBundleIDByName(kreadV1BundleName); diff --git a/agoric/contract/test/swingsetTests/bootstrap/make-bootstrap-users.js b/agoric/contract/test/swingsetTests/bootstrap/make-bootstrap-users.js index 73330236d..c6c4782e5 100644 --- a/agoric/contract/test/swingsetTests/bootstrap/make-bootstrap-users.js +++ b/agoric/contract/test/swingsetTests/bootstrap/make-bootstrap-users.js @@ -1,13 +1,5 @@ import { E } from '@endo/eventual-send'; -/** - * @typedef {{ - * character: Purse - * item: Purse - * payment: Purse - * }} Purses - */ - /** * Creates a user with its own purses and seats * includes methods for checking balances and @@ -19,21 +11,7 @@ import { E } from '@endo/eventual-send'; * * @param {string} name * @param {Purses} purses - * @returns {{ - * name: string - * purses: Purses - * getItems: () => Item[] - * getCharacters: () => any[] - * getPaymentBalance: () => bigint - * depositItems: (items) => void - * depositCharacters: (characters) => void - * depositPayment: (payment) => void - * withdrawItems: (items) => Payment - * withdrawCharacters: (characters) => Payment - * withdrawPayment: (payment) => Payment - * getSeat: () => any - * setMarketSeat: (seat) => void - * }} + * @returns {KreadUser} */ export const makeKreadUser = (name, purses) => { const seat = { @@ -51,8 +29,8 @@ export const makeKreadUser = (name, purses) => { (await E(purses.payment).getCurrentAmount()).value; const depositItems = async (items) => E(purses.item).deposit(items); - const depositCharacters = (characters) => - purses.character.deposit(characters); + 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); diff --git a/agoric/contract/test/swingsetTests/bootstrap/utils.js b/agoric/contract/test/swingsetTests/bootstrap/utils.js index 3999eb2db..00a419157 100644 --- a/agoric/contract/test/swingsetTests/bootstrap/utils.js +++ b/agoric/contract/test/swingsetTests/bootstrap/utils.js @@ -44,11 +44,11 @@ export const addCharacterToContext = async (context) => { /** * Mint item and deposit on Bootstrap item purse * - * @param {Bootstrap} bootstrap + * @param {Context} context */ -export const addItemToBootstrap = async (bootstrap, item) => { - /** @type {Bootstrap} */ - const { creatorFacet, contractAssets, purses, zoe } = bootstrap; +export const addItemToContext = async (context, item) => { + /** @type {Context} */ + const { creatorFacet, contractAssets, purses, zoe } = context; const mintItemInvitation = await E(creatorFacet).makeMintItemInvitation(); const itemAmount = AmountMath.make( @@ -62,9 +62,9 @@ export const addItemToBootstrap = async (bootstrap, item) => { const userSeat = await E(zoe).offer(mintItemInvitation, proposal); const payout = await E(userSeat).getPayout('Asset'); - purses.item.deposit(payout); + await E(purses.item).deposit(payout); }; -harden(addItemToBootstrap); +harden(addItemToContext); /** * @param {AssetConf} [conf] diff --git a/agoric/contract/test/swingsetTests/test-market.js b/agoric/contract/test/swingsetTests/test-market.js index de3b6b1d3..4b4a06d3b 100644 --- a/agoric/contract/test/swingsetTests/test-market.js +++ b/agoric/contract/test/swingsetTests/test-market.js @@ -19,3 +19,78 @@ test.serial( 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/types.js b/agoric/contract/test/swingsetTests/types.js index be83ea151..15f03bddb 100644 --- a/agoric/contract/test/swingsetTests/types.js +++ b/agoric/contract/test/swingsetTests/types.js @@ -40,6 +40,32 @@ * }} 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 @@ -55,5 +81,6 @@ * item: any * payment: any * }; + * users: Object. * }} Context */ From 3a0ea52d54ba30017bc4ebc8f5448741d07f164f Mon Sep 17 00:00:00 2001 From: Pandelis Symeonidis Date: Wed, 1 Nov 2023 15:41:44 +0100 Subject: [PATCH 04/17] clean up --- .../test/swingsetTests/swingset-setup.js | 10 ++++++++++ .../test/swingsetTests/test-kread-upgrade.js | 18 ++++++++++++++++++ .../contract/test/swingsetTests/test-market.js | 4 +++- 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 agoric/contract/test/swingsetTests/test-kread-upgrade.js diff --git a/agoric/contract/test/swingsetTests/swingset-setup.js b/agoric/contract/test/swingsetTests/swingset-setup.js index 223a9dcb4..ed06d452d 100644 --- a/agoric/contract/test/swingsetTests/swingset-setup.js +++ b/agoric/contract/test/swingsetTests/swingset-setup.js @@ -9,6 +9,13 @@ const kreadV1BundleName = 'kreadV1'; 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); @@ -18,6 +25,9 @@ export const run = async (name, args = []) => { return [status, capdata]; }; +/** + * Sets up swingset and starts v1 of kread contract + */ export async function setup() { const config = { includeDevDependencies: true, diff --git a/agoric/contract/test/swingsetTests/test-kread-upgrade.js b/agoric/contract/test/swingsetTests/test-kread-upgrade.js new file mode 100644 index 000000000..51875549c --- /dev/null +++ b/agoric/contract/test/swingsetTests/test-kread-upgrade.js @@ -0,0 +1,18 @@ +import { test } from '../prepare-test-env-ava.js'; +import { setup, run } from './swingset-setup.js'; + +test.before(async (t) => { + const [result] = await setup(); + t.is(result, 'fulfilled'); + +}); + +test.serial('--| MINT - Too Long Name', async (t) => { + const [mintCharacter] = await run('mintCharacter', ['example']); + t.is(mintCharacter, 'fulfilled'); +}); + +test.serial('--| MINT - Expected flow', async (t) => { + const [mintCharacter] = await run('mintCharacter', ['example']); + t.is(mintCharacter, 'rejected'); +}); diff --git a/agoric/contract/test/swingsetTests/test-market.js b/agoric/contract/test/swingsetTests/test-market.js index 4b4a06d3b..b8cb691f9 100644 --- a/agoric/contract/test/swingsetTests/test-market.js +++ b/agoric/contract/test/swingsetTests/test-market.js @@ -2,7 +2,9 @@ import { test } from '../prepare-test-env-ava.js'; import { setup, run } from './swingset-setup.js'; test.before(async (t) => { - await setup(); + const [result] = await setup(); + t.is(result, 'fulfilled'); + const [setupMarketTests] = await run('setupMarketTests'); t.is(setupMarketTests, 'fulfilled'); }); From 15b483e7fa10fc9f61fab3986f5a313ad05c5eed Mon Sep 17 00:00:00 2001 From: Pandelis Symeonidis Date: Wed, 1 Nov 2023 16:05:56 +0100 Subject: [PATCH 05/17] add null upgrade and setup minting tests --- .../swingsetTests/bootstrap/bootstrap-mint.js | 88 ++++++++----------- .../bootstrap/bootstrap-upgradable.js | 25 +++--- .../test/swingsetTests/swingset-setup.js | 2 +- .../test/swingsetTests/test-kread-upgrade.js | 18 ---- .../test/swingsetTests/test-minting.js | 13 ++- .../test/swingsetTests/test-null-upgrade.js | 14 +++ 6 files changed, 70 insertions(+), 90 deletions(-) delete mode 100644 agoric/contract/test/swingsetTests/test-kread-upgrade.js create mode 100644 agoric/contract/test/swingsetTests/test-null-upgrade.js diff --git a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-mint.js b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-mint.js index a988a65d7..86790179a 100644 --- a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-mint.js +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-mint.js @@ -1,69 +1,47 @@ import { AmountMath } from '@agoric/ertp'; import { E } from '@endo/eventual-send'; import { flow } from '../flow.js'; +import { makeKreadUser } from './make-bootstrap-users.js'; -export async function mintCharacterNameTooLong(context) { +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 mintCharacterExpectedFlow(context) { /** @type {Context} */ const { publicFacet, + purses, paymentAsset, users: { alice }, zoe, } = context; - const { message, give, offerArgs } = flow.mintCharacter.invalidName1; - + const { message, give, offerArgs } = flow.mintCharacter.expected; const mintCharacterInvitation = await E( publicFacet, ).makeMintCharacterInvitation(); const priceAmount = AmountMath.make(paymentAsset.brandMockIST, give.Price); - const payment = { Price: alice.withdrawPayment(priceAmount) }; - const proposal = harden({ give: { Price: priceAmount }, }); - const userSeat = await E(zoe).offer( - mintCharacterInvitation, - proposal, - payment, - offerArgs, - ); - - // await t.throwsAsync(E(userSeat).getOfferResult(), 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', - ); - - alice.depositPayment(await E(userSeat).getPayout('Price')); -} - -export async function mintCharacterExpectedFlow(context, name) { - /** @type {Context} */ - const { publicFacet, purses, paymentAsset, zoe } = context; - - const purse = paymentAsset.issuerMockIST.makeEmptyPurse(); - const payout = paymentAsset.mintMockIST.mintPayment( - AmountMath.make(paymentAsset.brandMockIST, harden(100000000000n)), - ); - purse.deposit(payout); - - const mintCharacterInvitation = await E( - publicFacet, - ).makeMintCharacterInvitation(); - - const priceAmount = AmountMath.make(paymentAsset.brandMockIST, 30000000n); - - const proposal = harden({ - give: { Price: priceAmount }, - }); - - const payment = { Price: purse.withdraw(priceAmount) }; - const offerArgs = harden({ name }); + const payment = { Price: await alice.withdrawPayment(priceAmount) }; const userSeat = await E(zoe).offer( mintCharacterInvitation, @@ -73,16 +51,20 @@ export async function mintCharacterExpectedFlow(context, name) { ); const result = await E(userSeat).getOfferResult(); - assert.equal(result, 'Character NFT minted successfully!'); + assert.equal(result, message, 'Offer does not return success message'); const characters = await E(publicFacet).getCharacters(); - assert.equal(characters[0].name, offerArgs.name); - - const payout2 = await E(userSeat).getPayout('Asset'); - - await E(purses.character).deposit(payout2); - assert( + assert.equal( + characters[0].name, offerArgs.name, + 'New character is not added to contract registry', + ); + + 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 not added to character purse', ); } diff --git a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js index 81081693b..6a7c297b4 100644 --- a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js @@ -10,7 +10,7 @@ import { CONTRACT_ELECTORATE, ParamTypes } from '@agoric/governance'; import { makePromiseKit } from '@endo/promise-kit'; import { defaultCharacters } from '../../characters.js'; import { defaultItems } from '../../items.js'; -import { mintCharacterExpectedFlow } from './bootstrap-mint.js'; +import { mintCharacterExpectedFlow, setupMintTests } from './bootstrap-mint.js'; import { setupMarketTests, sellCharacter, @@ -275,8 +275,11 @@ export const buildRootObject = async () => { ), }; }, - mintCharacter: async (name) => { - await mintCharacterExpectedFlow(context, name); + setupMintTests: async () => { + context = await setupMintTests(context); + }, + mintCharacter: async () => { + await mintCharacterExpectedFlow(context); }, setupMarketTests: async () => { context = await setupMarketTests(context); @@ -291,31 +294,31 @@ export const buildRootObject = async () => { await buyCharacter(context); }, buyCharacterNotOnMarket: async () => { - await buyCharacterNotOnMarket(context) + await buyCharacterNotOnMarket(context); }, sellItem: async () => { await sellItem(context); }, buyItemOfferLessThanAskingPrice: async () => { - await buyItemOfferLessThanAskingPrice(context) + await buyItemOfferLessThanAskingPrice(context); }, buyItem: async () => { - await buyItem(context) + await buyItem(context); }, buyItemNotOnMarket: async () => { - await buyItemNotOnMarket(context) + await buyItemNotOnMarket(context); }, buyCharacterOfferMoreThanAskingPrice: async () => { - await buyCharacterOfferMoreThanAskingPrice(context) + await buyCharacterOfferMoreThanAskingPrice(context); }, buyItemOfferMoreThanAskingPrice: async () => { - await buyItemOfferMoreThanAskingPrice(context) + await buyItemOfferMoreThanAskingPrice(context); }, internalSellItemBatch: async () => { - await internalSellItemBatch(context) + await internalSellItemBatch(context); }, buyBatchSoldItem: async () => { - await buyBatchSoldItem(context) + await buyBatchSoldItem(context); }, nullUpgrade: async () => { trace('start null upgrade'); diff --git a/agoric/contract/test/swingsetTests/swingset-setup.js b/agoric/contract/test/swingsetTests/swingset-setup.js index ed06d452d..9987d617e 100644 --- a/agoric/contract/test/swingsetTests/swingset-setup.js +++ b/agoric/contract/test/swingsetTests/swingset-setup.js @@ -76,5 +76,5 @@ export async function setup() { c.pinVatRoot('bootstrap'); await c.run(); - await run('buildV1', []); + return run('buildV1', []); } diff --git a/agoric/contract/test/swingsetTests/test-kread-upgrade.js b/agoric/contract/test/swingsetTests/test-kread-upgrade.js deleted file mode 100644 index 51875549c..000000000 --- a/agoric/contract/test/swingsetTests/test-kread-upgrade.js +++ /dev/null @@ -1,18 +0,0 @@ -import { test } from '../prepare-test-env-ava.js'; -import { setup, run } from './swingset-setup.js'; - -test.before(async (t) => { - const [result] = await setup(); - t.is(result, 'fulfilled'); - -}); - -test.serial('--| MINT - Too Long Name', async (t) => { - const [mintCharacter] = await run('mintCharacter', ['example']); - t.is(mintCharacter, 'fulfilled'); -}); - -test.serial('--| MINT - Expected flow', async (t) => { - const [mintCharacter] = await run('mintCharacter', ['example']); - t.is(mintCharacter, 'rejected'); -}); diff --git a/agoric/contract/test/swingsetTests/test-minting.js b/agoric/contract/test/swingsetTests/test-minting.js index d85450dbb..349ebd338 100644 --- a/agoric/contract/test/swingsetTests/test-minting.js +++ b/agoric/contract/test/swingsetTests/test-minting.js @@ -2,15 +2,14 @@ import { test } from '../prepare-test-env-ava.js'; import { setup, run } from './swingset-setup.js'; test.before(async (t) => { - await setup(); -}); + const [result] = await setup(); + t.is(result, 'fulfilled'); -test.serial('--| MINT - Too Long Name', async (t) => { - const [mintCharacter] = await run('mintCharacter', ['example']); - t.is(mintCharacter, 'fulfilled'); + const [setupMintTests] = await run('setupMintTests'); + t.is(setupMintTests, 'fulfilled'); }); test.serial('--| MINT - Expected flow', async (t) => { - const [mintCharacter] = await run('mintCharacter', ['example']); - t.is(mintCharacter, 'rejected'); + const [mintCharacter] = await run('mintCharacter'); + t.is(mintCharacter, '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..ab943d9b1 --- /dev/null +++ b/agoric/contract/test/swingsetTests/test-null-upgrade.js @@ -0,0 +1,14 @@ +import { test } from '../prepare-test-env-ava.js'; +import { setup, run } from './swingset-setup.js'; + +test.before(async (t) => { + const [result] = await setup(); + t.is(result, 'fulfilled'); + +}); + +test.serial('null upgrade', async (t) => { + const [result] = await run('nullUpgrade'); + t.is(result, 'fulfilled'); +}); + From 8045f77d37c60f4d51b03e8ebac45fc45625b46e Mon Sep 17 00:00:00 2001 From: Pandelis Symeonidis Date: Fri, 3 Nov 2023 09:02:59 +0100 Subject: [PATCH 06/17] mint and inventory tests --- .../bootstrap/bootstrap-inventory.js | 876 ++++++++++++++++++ .../swingsetTests/bootstrap/bootstrap-mint.js | 521 ++++++++++- .../bootstrap/bootstrap-upgradable.js | 107 ++- .../test/swingsetTests/test-inventory.js | 58 +- .../test/swingsetTests/test-minting.js | 67 +- agoric/contract/test/test-inventory.js | 2 + 6 files changed, 1618 insertions(+), 13 deletions(-) diff --git a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-inventory.js b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-inventory.js index e69de29bb..2ac507b93 100644 --- a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-inventory.js +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-inventory.js @@ -0,0 +1,876 @@ +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/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 } = 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', + ); +} + +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 } = 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', + ); + + // 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 } = 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); +} + +export async function swapItemsInitiallyEmpty(context) { + /** @type {Context} */ + const { publicFacet, contractAssets, purses, zoe } = 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', + ); +} + +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 } = 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); +} + +export async function unequipAllEmptyInventory(context) { + const { publicFacet, contractAssets, purses, zoe } = 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', + ); +} diff --git a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-mint.js b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-mint.js index 86790179a..6543a7545 100644 --- a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-mint.js +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-mint.js @@ -1,7 +1,8 @@ -import { AmountMath } from '@agoric/ertp'; 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 } from '@agoric/store'; export async function setupMintTests(context) { const { contractAssets, paymentAsset } = context; @@ -21,8 +22,140 @@ export async function setupMintTests(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 mintCharacterExpectedFlow(context) { +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, @@ -32,6 +165,7 @@ export async function mintCharacterExpectedFlow(context) { zoe, } = context; const { message, give, offerArgs } = flow.mintCharacter.expected; + const mintCharacterInvitation = await E( publicFacet, ).makeMintCharacterInvitation(); @@ -51,20 +185,395 @@ export async function mintCharacterExpectedFlow(context) { ); const result = await E(userSeat).getOfferResult(); - assert.equal(result, message, 'Offer does not return success message'); + assert.equal(result, message, 'Offer returns success message'); const characters = await E(publicFacet).getCharacters(); assert.equal( characters[0].name, offerArgs.name, - 'New character is not added to contract registry', + 'New character is added to contract registry', ); const payout = await E(userSeat).getPayout('Asset'); - await E(purses.character).deposit(payout); + await E(purses.character).deposit(payout); assert.equal( (await E(purses.character).getCurrentAmount()).value.payload[0][0].name, offerArgs.name, - 'New Character was not added to character purse', + '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 } = 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', + ); + + assert.equal( + mappedInventory.filter((i) => i.rarity < 20).length, + 2, + 'No two common items', + ); + + assert.equal( + mappedInventory.filter((i) => i.rarity > 19).length, + 1, + 'No uncommon to legendary item found.', + ); +} + +export async function mintItemExpectedFlow(context) { + /** @type {Context} */ + const { creatorFacet, contractAssets, purses, zoe } = 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', + ); +} + +export async function mintSameItemSFT(context) { + /** @type {Context} */ + const { creatorFacet, contractAssets, purses, zoe } = 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); +} + +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-upgradable.js b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js index 6a7c297b4..c10531f2a 100644 --- a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js @@ -10,7 +10,23 @@ import { CONTRACT_ELECTORATE, ParamTypes } from '@agoric/governance'; import { makePromiseKit } from '@endo/promise-kit'; import { defaultCharacters } from '../../characters.js'; import { defaultItems } from '../../items.js'; -import { mintCharacterExpectedFlow, setupMintTests } from './bootstrap-mint.js'; +import { + setupMintTests, + mintTooLongName, + mintInvalidCharsInname, + mintDuplicateName, + mintExpectedFlow, + mintFeeTooLow, + mintForbiddenName, + mintInventoryCheck, + mintItemExpectedFlow, + mintItemMultipleDifferentFlow, + mintNoName, + mintNoCharactersAvailable, + mintSameItemSFT, + mintItemMultipleFlow, + mintNoOfferArgs, +} from './bootstrap-mint.js'; import { setupMarketTests, sellCharacter, @@ -26,6 +42,19 @@ import { internalSellItemBatch, buyBatchSoldItem, } from './bootstrap-market.js'; +import { + unequipAll, + unequipAllEmptyInventory, + unequipAlreadyUnequippedItem, + unequipWithWrongCharacter, + unequipItem, + swapItems, + swapItemsDifferentCategories, + swapItemsInitiallyEmpty, + setupInventoryTests, + equipItemDuplicateCategory, + equipItem, +} from './bootstrap-inventory.js'; import { makeRatio } from '@agoric/zoe/src/contractSupport/index.js'; const trace = makeTracer('kreadBootUpgrade'); @@ -278,8 +307,47 @@ export const buildRootObject = async () => { setupMintTests: async () => { context = await setupMintTests(context); }, - mintCharacter: async () => { - await mintCharacterExpectedFlow(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); @@ -320,6 +388,39 @@ export const buildRootObject = async () => { 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); + }, nullUpgrade: async () => { trace('start null upgrade'); const bundleId = await E(vatAdmin).getBundleIDByName(kreadV1BundleName); diff --git a/agoric/contract/test/swingsetTests/test-inventory.js b/agoric/contract/test/swingsetTests/test-inventory.js index 383900711..8fbea22f6 100644 --- a/agoric/contract/test/swingsetTests/test-inventory.js +++ b/agoric/contract/test/swingsetTests/test-inventory.js @@ -1,8 +1,60 @@ import { test } from '../prepare-test-env-ava.js'; -import { setup } from './swingset-setup.js'; +import { run, setup } from './swingset-setup.js'; test.before(async (t) => { - await setup(); + const [result] = await setup(); + t.is(result, '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("") +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-minting.js b/agoric/contract/test/swingsetTests/test-minting.js index 349ebd338..f79adcfee 100644 --- a/agoric/contract/test/swingsetTests/test-minting.js +++ b/agoric/contract/test/swingsetTests/test-minting.js @@ -9,7 +9,72 @@ test.before(async (t) => { 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('mintCharacter'); + 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/test-inventory.js b/agoric/contract/test/test-inventory.js index 694ac24cf..2701e0287 100644 --- a/agoric/contract/test/test-inventory.js +++ b/agoric/contract/test/test-inventory.js @@ -14,6 +14,8 @@ test.before(async (t) => { t.context = bootstrap; }); + + const unequipOffer = async (t) => { /** @type {Bootstrap} */ const { publicFacet, contractAssets, purses, zoe } = t.context; From d8ce568537c71c3512c31bf47d067803960db4a9 Mon Sep 17 00:00:00 2001 From: Pandelis Symeonidis Date: Fri, 3 Nov 2023 11:31:12 +0100 Subject: [PATCH 07/17] market metrics and governance tests --- agoric/contract/test/bootstrap.js | 1 - .../bootstrap/bootstrap-governance.js | 38 +++ .../bootstrap/bootstrap-market-metrics.js | 250 ++++++++++++++++++ .../bootstrap/bootstrap-market.js | 1 - .../bootstrap/bootstrap-upgradable.js | 70 +++-- .../test/swingsetTests/test-governance.js | 12 + .../test/swingsetTests/test-market-metrics.js | 35 +++ agoric/contract/test/swingsetTests/types.js | 1 + 8 files changed, 385 insertions(+), 23 deletions(-) create mode 100644 agoric/contract/test/swingsetTests/bootstrap/bootstrap-governance.js create mode 100644 agoric/contract/test/swingsetTests/bootstrap/bootstrap-market-metrics.js create mode 100644 agoric/contract/test/swingsetTests/test-governance.js create mode 100644 agoric/contract/test/swingsetTests/test-market-metrics.js diff --git a/agoric/contract/test/bootstrap.js b/agoric/contract/test/bootstrap.js index 5b404a2ce..b48c74db9 100644 --- a/agoric/contract/test/bootstrap.js +++ b/agoric/contract/test/bootstrap.js @@ -103,7 +103,6 @@ export const bootstrapContext = async (conf = undefined) => { character: { issuer: characterIssuer, brand: characterBrand }, item: { issuer: itemIssuer, brand: itemBrand }, }; - console.log("TERMS: ", terms) const purses = { character: characterIssuer.makeEmptyPurse(), item: itemIssuer.makeEmptyPurse(), 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-market-metrics.js b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market-metrics.js new file mode 100644 index 000000000..346c16b0e --- /dev/null +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market-metrics.js @@ -0,0 +1,250 @@ +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 } = 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); + } + } +} + +export async function collectionSize(context) { + /** @type {Context} */ + const { + publicFacet, + paymentAsset, + zoe, + users: { bob }, + } = 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); +} + +export async function averageLevelsCharacter(context) { + /** @type {Context} */ + const { + publicFacet, + 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); +} + +export async function amountSoldCharacter(context) { + /** @type {Context} */ + const { + publicFacet, + users: { bob }, + } = 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); +} + +export async function latestSalePriceCharacter(context) { + /** @type {Context} */ + const { + publicFacet, + users: { bob }, + } = 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); +} diff --git a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market.js b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market.js index 1cda95148..9cf7aafff 100644 --- a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market.js +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market.js @@ -256,7 +256,6 @@ export async function buyCharacter(context) { 'Character was not removed from market', ); - console.log(characterToBuy.askingPrice); assert.equal( royaltyPurse.getCurrentAmount().value, royaltyPursePre + multiplyBy(characterToBuy.askingPrice, royaltyRate).value, diff --git a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js index c10531f2a..737fcadc6 100644 --- a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js @@ -55,6 +55,15 @@ import { 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'; const trace = makeTracer('kreadBootUpgrade'); @@ -65,21 +74,20 @@ export const buildRootObject = async () => { let vatAdmin; let initialPoserInvitation; let electorateInvitationAmount; - let zoe; let governedInstance; - let creatorFacet; - let publicFacet; - let contractAssets; - let purses; - let governorFacets; - + /** @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 { @@ -166,7 +174,7 @@ export const buildRootObject = async () => { undefined, 'zcf', ); - zoe = zoeService; + zoePK.resolve(zoeService); trace('Starting!'); const v1BundleId = await E(vatAdmin).getBundleIDByName(kreadV1BundleName); @@ -252,8 +260,8 @@ export const buildRootObject = async () => { trace('BOOT buildV1 started instance'); governedInstance = E(governorFacets.creatorFacet).getInstance(); - publicFacet = await E(governorFacets.creatorFacet).getPublicFacet(); - creatorFacet = await E(governorFacets.creatorFacet).getCreatorFacet(); + const publicFacet = await E(governorFacets.creatorFacet).getPublicFacet(); + const creatorFacet = await E(governorFacets.creatorFacet).getCreatorFacet(); await E(creatorFacet).initializeBaseAssets( defaultCharacters, @@ -268,18 +276,16 @@ export const buildRootObject = async () => { brands: { KREAdCHARACTER: characterBrand, KREAdITEM: itemBrand }, } = terms; - 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(), - }; context = { - contractAssets, - purses, + 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, @@ -287,6 +293,7 @@ export const buildRootObject = async () => { }, publicFacet, creatorFacet, + governorFacets, zoe, royaltyPurse, platformFeePurse, @@ -421,6 +428,27 @@ export const buildRootObject = async () => { 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); diff --git a/agoric/contract/test/swingsetTests/test-governance.js b/agoric/contract/test/swingsetTests/test-governance.js new file mode 100644 index 000000000..a70479e02 --- /dev/null +++ b/agoric/contract/test/swingsetTests/test-governance.js @@ -0,0 +1,12 @@ +import { test } from '../prepare-test-env-ava.js'; +import { run, setup } from './swingset-setup.js'; + +test.before(async (t) => { + const [result] = await setup(); + t.is(result, '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-market-metrics.js b/agoric/contract/test/swingsetTests/test-market-metrics.js new file mode 100644 index 000000000..074816c59 --- /dev/null +++ b/agoric/contract/test/swingsetTests/test-market-metrics.js @@ -0,0 +1,35 @@ +import { test } from '../prepare-test-env-ava.js'; +import { run, setup } from './swingset-setup.js'; + +test.before(async (t) => { + const [result] = await setup(); + t.is(result, 'fulfilled'); + + const [setupMarketMetricsTests] = await run('setupMarketMetricsTests'); + t.is(setupMarketMetricsTests, 'fulfilled'); +}); + +test.serial('---| METRICS - Initialization', async (t) => { + const [result] = await run('setupMarketMetricsTests'); + 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/types.js b/agoric/contract/test/swingsetTests/types.js index 15f03bddb..cf6c3a8bf 100644 --- a/agoric/contract/test/swingsetTests/types.js +++ b/agoric/contract/test/swingsetTests/types.js @@ -75,6 +75,7 @@ * creatorFacet: unknown; * contractAssets: ContractAssets; * publicFacet: unknown; + * governorFacets: import('@agoric/governance/tools/puppetContractGovernor').PuppetContractGovernorKit; * zoe: ZoeService; * purses: { * character: any From 7dd4fab1c460475bb08a99fc0817805c358984d1 Mon Sep 17 00:00:00 2001 From: Pandelis Symeonidis Date: Fri, 3 Nov 2023 11:37:00 +0100 Subject: [PATCH 08/17] cleanup --- agoric/contract/src/proposal/start-kread-proposal.js | 1 - agoric/contract/test/bootstrap.js | 1 + agoric/contract/test/test-inventory.js | 2 -- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/agoric/contract/src/proposal/start-kread-proposal.js b/agoric/contract/src/proposal/start-kread-proposal.js index e775bd2d2..7470e6dda 100644 --- a/agoric/contract/src/proposal/start-kread-proposal.js +++ b/agoric/contract/src/proposal/start-kread-proposal.js @@ -328,7 +328,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/test/bootstrap.js b/agoric/contract/test/bootstrap.js index b48c74db9..d48d1408b 100644 --- a/agoric/contract/test/bootstrap.js +++ b/agoric/contract/test/bootstrap.js @@ -103,6 +103,7 @@ export const bootstrapContext = async (conf = undefined) => { character: { issuer: characterIssuer, brand: characterBrand }, item: { issuer: itemIssuer, brand: itemBrand }, }; + const purses = { character: characterIssuer.makeEmptyPurse(), item: itemIssuer.makeEmptyPurse(), diff --git a/agoric/contract/test/test-inventory.js b/agoric/contract/test/test-inventory.js index 2701e0287..694ac24cf 100644 --- a/agoric/contract/test/test-inventory.js +++ b/agoric/contract/test/test-inventory.js @@ -14,8 +14,6 @@ test.before(async (t) => { t.context = bootstrap; }); - - const unequipOffer = async (t) => { /** @type {Bootstrap} */ const { publicFacet, contractAssets, purses, zoe } = t.context; From e7a2c205bf7ee873fc8ea364a8ffce5729055dba Mon Sep 17 00:00:00 2001 From: Pandelis Symeonidis Date: Fri, 3 Nov 2023 15:10:15 +0100 Subject: [PATCH 09/17] contract upgrade --- agoric/contract/src/kreadV1/README.md | 164 ++ agoric/contract/src/{ => kreadV1}/errors.js | 0 agoric/contract/src/{ => kreadV1}/index.js | 0 .../{ => kreadV1}/kreadCommitteeCharter.js | 0 agoric/contract/src/{ => kreadV1}/kreadKit.js | 0 .../src/{ => kreadV1}/market-metrics.js | 0 agoric/contract/src/{ => kreadV1}/text.js | 0 .../contract/src/{ => kreadV1}/type-guards.js | 0 agoric/contract/src/{ => kreadV1}/types.js | 0 agoric/contract/src/{ => kreadV1}/utils.js | 0 agoric/contract/src/kreadV2/errors.js | 34 + agoric/contract/src/kreadV2/index.js | 181 ++ .../src/kreadV2/kreadCommitteeCharter.js | 145 ++ agoric/contract/src/kreadV2/kreadKit.js | 1832 +++++++++++++++++ agoric/contract/src/kreadV2/market-metrics.js | 77 + agoric/contract/src/kreadV2/text.js | 7 + agoric/contract/src/kreadV2/type-guards.js | 248 +++ agoric/contract/src/kreadV2/types.js | 295 +++ agoric/contract/src/kreadV2/utils.js | 109 + .../src/proposal/start-kread-script.js | 2 +- agoric/contract/test/bootstrap.js | 4 +- agoric/contract/test/flow.js | 2 +- .../bootstrap/bootstrap-inventory.js | 12 +- .../bootstrap/bootstrap-market.js | 26 +- .../swingsetTests/bootstrap/bootstrap-mint.js | 4 +- .../bootstrap/bootstrap-upgradable.js | 41 +- .../bootstrap/bootstrap-upgrade-v2.js | 77 + agoric/contract/test/swingsetTests/flow.js | 2 +- .../test/swingsetTests/swingset-setup.js | 11 +- .../test/swingsetTests/test-governance.js | 6 +- .../test/swingsetTests/test-inventory.js | 6 +- .../test/swingsetTests/test-market-metrics.js | 6 +- .../test/swingsetTests/test-market.js | 6 +- .../test/swingsetTests/test-minting.js | 6 +- .../test/swingsetTests/test-null-upgrade.js | 6 +- .../test/swingsetTests/test-upgrade.js | 50 + agoric/contract/test/swingsetTests/types.js | 5 +- agoric/contract/test/test-inventory.js | 2 +- agoric/contract/test/test-market.js | 2 +- 39 files changed, 3317 insertions(+), 51 deletions(-) create mode 100644 agoric/contract/src/kreadV1/README.md rename agoric/contract/src/{ => kreadV1}/errors.js (100%) rename agoric/contract/src/{ => kreadV1}/index.js (100%) rename agoric/contract/src/{ => kreadV1}/kreadCommitteeCharter.js (100%) rename agoric/contract/src/{ => kreadV1}/kreadKit.js (100%) rename agoric/contract/src/{ => kreadV1}/market-metrics.js (100%) rename agoric/contract/src/{ => kreadV1}/text.js (100%) rename agoric/contract/src/{ => kreadV1}/type-guards.js (100%) rename agoric/contract/src/{ => kreadV1}/types.js (100%) rename agoric/contract/src/{ => kreadV1}/utils.js (100%) create mode 100644 agoric/contract/src/kreadV2/errors.js create mode 100644 agoric/contract/src/kreadV2/index.js create mode 100644 agoric/contract/src/kreadV2/kreadCommitteeCharter.js create mode 100644 agoric/contract/src/kreadV2/kreadKit.js create mode 100644 agoric/contract/src/kreadV2/market-metrics.js create mode 100644 agoric/contract/src/kreadV2/text.js create mode 100644 agoric/contract/src/kreadV2/type-guards.js create mode 100644 agoric/contract/src/kreadV2/types.js create mode 100644 agoric/contract/src/kreadV2/utils.js create mode 100644 agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgrade-v2.js create mode 100644 agoric/contract/test/swingsetTests/test-upgrade.js 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 100% rename from agoric/contract/src/index.js rename to agoric/contract/src/kreadV1/index.js 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 100% rename from agoric/contract/src/utils.js rename to agoric/contract/src/kreadV1/utils.js 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/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 d48d1408b..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: { @@ -103,7 +103,7 @@ export const bootstrapContext = async (conf = undefined) => { character: { issuer: characterIssuer, brand: characterBrand }, item: { issuer: itemIssuer, brand: itemBrand }, }; - + const purses = { character: characterIssuer.makeEmptyPurse(), item: itemIssuer.makeEmptyPurse(), 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-inventory.js b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-inventory.js index 2ac507b93..fc3caad86 100644 --- a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-inventory.js +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-inventory.js @@ -3,7 +3,7 @@ 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/errors.js'; +import { errors } from '../../../src/kreadV2/errors.js'; export async function setupInventoryTests(context) { await addCharacterToContext(context); @@ -257,7 +257,7 @@ export async function equipItem(context) { 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( @@ -338,8 +338,8 @@ 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; + const inventory = await E(publicFacet).getCharacterInventory(characterName); + const existingCategory = inventory.items[0][0].category; await addItemToContext(context, { name: 'New item', category: existingCategory, @@ -473,9 +473,7 @@ export async function swapItems(context) { const itemWantValue = characterInventory.items .map((i) => i[0]) .find(({ rarity }) => rarity > 19); - const itemWantCopyBagAmount = makeCopyBag( - harden([[itemWantValue, 1n]]), - ); + const itemWantCopyBagAmount = makeCopyBag(harden([[itemWantValue, 1n]])); const itemWant = AmountMath.make( contractAssets.item.brand, itemWantCopyBagAmount, diff --git a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market.js b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market.js index 9cf7aafff..204103a46 100644 --- a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market.js +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market.js @@ -4,7 +4,7 @@ import { flow } from '../flow.js'; import { makeCopyBag } from '@agoric/store'; import { addCharacterToContext, addItemToContext } from './utils.js'; import { makeKreadUser } from './make-bootstrap-users.js'; -import { errors } from '../../../src/errors.js'; +import { errors } from '../../../src/kreadV2/errors.js'; import { multiplyBy } from '@agoric/zoe/src/contractSupport/ratio.js'; import { defaultItems } from '../items.js'; @@ -457,18 +457,22 @@ export async function buyItem(context) { 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"); + 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', + 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'); } export async function buyItemNotOnMarket(context) { @@ -711,11 +715,7 @@ export async function buyItemOfferMoreThanAskingPrice(context) { ); itemsForSale = await E(publicFacet).getItemsForSale(); - assert.equal( - itemsForSale.length, - 0, - 'Item was not removed to market', - ); + assert.equal(itemsForSale.length, 0, 'Item was not removed to market'); } export async function internalSellItemBatch(context) { diff --git a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-mint.js b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-mint.js index 6543a7545..91cb27432 100644 --- a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-mint.js +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-mint.js @@ -193,6 +193,8 @@ export async function mintExpectedFlow(context) { offerArgs.name, 'New character is added to contract registry', ); + // console.log("STORAGE NODE: ") + // console.log(storageNode.getPath("kread.character")) const payout = await E(userSeat).getPayout('Asset'); await E(purses.character).deposit(payout); @@ -384,7 +386,7 @@ export async function mintNoCharactersAvailable(context) { zoe, } = context; const { offerArgs, message, give } = flow.mintCharacter.noAvailability; - + const mintCharacterInvitation = await E( publicFacet, ).makeMintCharacterInvitation(); diff --git a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js index 737fcadc6..71c97799d 100644 --- a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js @@ -65,10 +65,16 @@ import { } 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'; const trace = makeTracer('kreadBootUpgrade'); const kreadV1BundleName = 'kreadV1'; +const kreadV2BundleName = 'kreadV2'; export const buildRootObject = async () => { let vatAdmin; @@ -77,7 +83,7 @@ export const buildRootObject = async () => { let governedInstance; /** @type {Context} */ let context; - /** @type {import('@agoric/governance/tools/puppetContractGovernor').PuppetContractGovernorKit} */ + /** @type {import('@agoric/governance/tools/puppetContractGovernor').PuppetContractGovernorKit} */ let governorFacets; const storageKit = makeFakeStorageKit('kread'); @@ -219,7 +225,10 @@ export const buildRootObject = async () => { E(E(zoe).getInvitationIssuer()).getAmountOf(poserInvitationP), ]); }, - buildV1: async () => { + buildV1: async ( + baseCharacters = defaultCharacters, + baseItems = defaultItems, + ) => { trace(`BOOT buildV1 start`); const governorTerms = await deeplyFulfilled( @@ -261,11 +270,13 @@ export const buildRootObject = async () => { governedInstance = E(governorFacets.creatorFacet).getInstance(); const publicFacet = await E(governorFacets.creatorFacet).getPublicFacet(); - const creatorFacet = await E(governorFacets.creatorFacet).getCreatorFacet(); + const creatorFacet = await E( + governorFacets.creatorFacet, + ).getCreatorFacet(); await E(creatorFacet).initializeBaseAssets( - defaultCharacters, - defaultItems, + baseCharacters, + baseItems, ); await E(creatorFacet).initializeCharacterNamesEntries(); await E(creatorFacet).initializeMetrics(); @@ -277,6 +288,7 @@ export const buildRootObject = async () => { } = terms; context = { + storageNode: storageKit.rootNode, contractAssets: { character: { issuer: characterIssuer, brand: characterBrand }, item: { issuer: itemIssuer, brand: itemBrand }, @@ -463,5 +475,24 @@ export const buildRootObject = async () => { 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); + }, }); }; 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..8369d2b53 --- /dev/null +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgrade-v2.js @@ -0,0 +1,77 @@ +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 } = 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); +} + +export async function testFunctionalityAfterUpgrade(context) { + /** @type {Context} */ + const { publicFacet, zoe, purses, paymentAsset } = 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); +} diff --git a/agoric/contract/test/swingsetTests/flow.js b/agoric/contract/test/swingsetTests/flow.js index e65a79049..a304f8912 100644 --- a/agoric/contract/test/swingsetTests/flow.js +++ b/agoric/contract/test/swingsetTests/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/swingset-setup.js b/agoric/contract/test/swingsetTests/swingset-setup.js index 9987d617e..1ab48e3ad 100644 --- a/agoric/contract/test/swingsetTests/swingset-setup.js +++ b/agoric/contract/test/swingsetTests/swingset-setup.js @@ -6,6 +6,7 @@ import { assert } from '@agoric/assert'; const bfile = (name) => new URL(name, import.meta.url).pathname; const kreadV1BundleName = 'kreadV1'; +const kreadV2BundleName = 'kreadV2'; let c; @@ -65,7 +66,13 @@ export async function setup() { }, [kreadV1BundleName]: { sourceSpec: await importMetaResolve( - '../../src/index.js', + '../../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), }, @@ -75,6 +82,4 @@ export async function setup() { c = await buildVatController(config); c.pinVatRoot('bootstrap'); await c.run(); - - return run('buildV1', []); } diff --git a/agoric/contract/test/swingsetTests/test-governance.js b/agoric/contract/test/swingsetTests/test-governance.js index a70479e02..0e51325b5 100644 --- a/agoric/contract/test/swingsetTests/test-governance.js +++ b/agoric/contract/test/swingsetTests/test-governance.js @@ -2,8 +2,10 @@ import { test } from '../prepare-test-env-ava.js'; import { run, setup } from './swingset-setup.js'; test.before(async (t) => { - const [result] = await setup(); - t.is(result, 'fulfilled'); + await setup(); + + const [build] = await run('buildV1'); + t.is(build, "fulfilled") }); test.serial('| GOVERNANCE - Block Methods', async (t) => { diff --git a/agoric/contract/test/swingsetTests/test-inventory.js b/agoric/contract/test/swingsetTests/test-inventory.js index 8fbea22f6..29897e00d 100644 --- a/agoric/contract/test/swingsetTests/test-inventory.js +++ b/agoric/contract/test/swingsetTests/test-inventory.js @@ -2,8 +2,10 @@ import { test } from '../prepare-test-env-ava.js'; import { run, setup } from './swingset-setup.js'; test.before(async (t) => { - const [result] = await setup(); - t.is(result, 'fulfilled'); + await setup(); + + const [build] = await run('buildV1'); + t.is(build, "fulfilled") const [setupInventoryTests] = await run('setupInventoryTests'); t.is(setupInventoryTests, 'fulfilled'); diff --git a/agoric/contract/test/swingsetTests/test-market-metrics.js b/agoric/contract/test/swingsetTests/test-market-metrics.js index 074816c59..2070471db 100644 --- a/agoric/contract/test/swingsetTests/test-market-metrics.js +++ b/agoric/contract/test/swingsetTests/test-market-metrics.js @@ -2,8 +2,10 @@ import { test } from '../prepare-test-env-ava.js'; import { run, setup } from './swingset-setup.js'; test.before(async (t) => { - const [result] = await setup(); - t.is(result, 'fulfilled'); + await setup(); + + const [build] = await run('buildV1'); + t.is(build, "fulfilled") const [setupMarketMetricsTests] = await run('setupMarketMetricsTests'); t.is(setupMarketMetricsTests, 'fulfilled'); diff --git a/agoric/contract/test/swingsetTests/test-market.js b/agoric/contract/test/swingsetTests/test-market.js index b8cb691f9..1001e89f3 100644 --- a/agoric/contract/test/swingsetTests/test-market.js +++ b/agoric/contract/test/swingsetTests/test-market.js @@ -2,8 +2,10 @@ import { test } from '../prepare-test-env-ava.js'; import { setup, run } from './swingset-setup.js'; test.before(async (t) => { - const [result] = await setup(); - t.is(result, 'fulfilled'); + await setup(); + + const [build] = await run('buildV1'); + t.is(build, "fulfilled") const [setupMarketTests] = await run('setupMarketTests'); t.is(setupMarketTests, 'fulfilled'); diff --git a/agoric/contract/test/swingsetTests/test-minting.js b/agoric/contract/test/swingsetTests/test-minting.js index f79adcfee..75dc26743 100644 --- a/agoric/contract/test/swingsetTests/test-minting.js +++ b/agoric/contract/test/swingsetTests/test-minting.js @@ -2,8 +2,10 @@ import { test } from '../prepare-test-env-ava.js'; import { setup, run } from './swingset-setup.js'; test.before(async (t) => { - const [result] = await setup(); - t.is(result, 'fulfilled'); + await setup(); + + const [build] = await run('buildV1'); + t.is(build, "fulfilled") const [setupMintTests] = await run('setupMintTests'); t.is(setupMintTests, 'fulfilled'); diff --git a/agoric/contract/test/swingsetTests/test-null-upgrade.js b/agoric/contract/test/swingsetTests/test-null-upgrade.js index ab943d9b1..9b7bf6dac 100644 --- a/agoric/contract/test/swingsetTests/test-null-upgrade.js +++ b/agoric/contract/test/swingsetTests/test-null-upgrade.js @@ -2,13 +2,13 @@ import { test } from '../prepare-test-env-ava.js'; import { setup, run } from './swingset-setup.js'; test.before(async (t) => { - const [result] = await setup(); - t.is(result, 'fulfilled'); + await setup(); + const [build] = await run('buildV1'); + t.is(build, 'fulfilled'); }); test.serial('null upgrade', async (t) => { const [result] = await run('nullUpgrade'); t.is(result, 'fulfilled'); }); - 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/types.js b/agoric/contract/test/swingsetTests/types.js index cf6c3a8bf..e7732a3bd 100644 --- a/agoric/contract/test/swingsetTests/types.js +++ b/agoric/contract/test/swingsetTests/types.js @@ -75,13 +75,14 @@ * creatorFacet: unknown; * contractAssets: ContractAssets; * publicFacet: unknown; - * governorFacets: import('@agoric/governance/tools/puppetContractGovernor').PuppetContractGovernorKit; + * governorFacets: import('@agoric/governance/tools/puppetContractGovernor').PuppetContractGovernorKit; * zoe: ZoeService; * purses: { * character: any * item: any * payment: any * }; - * users: Object. + * 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'; From 2edfba0206ee8a65310daf494905eb30270d7243 Mon Sep 17 00:00:00 2001 From: Pandelis Symeonidis Date: Wed, 8 Nov 2023 09:10:18 +0100 Subject: [PATCH 10/17] storage node tests --- .../swingsetTests/bootstrap/bootstrap-mint.js | 84 +++++++++++++------ .../bootstrap/bootstrap-upgradable.js | 11 ++- 2 files changed, 69 insertions(+), 26 deletions(-) diff --git a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-mint.js b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-mint.js index 91cb27432..6e7e8d5de 100644 --- a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-mint.js +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-mint.js @@ -2,7 +2,8 @@ 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 } from '@agoric/store'; +import { makeCopyBag, mustMatch } from '@agoric/store'; +import { TimestampRecordShape } from '@agoric/time'; export async function setupMintTests(context) { const { contractAssets, paymentAsset } = context; @@ -163,6 +164,7 @@ export async function mintExpectedFlow(context) { paymentAsset, users: { alice }, zoe, + getFromVStorage, } = context; const { message, give, offerArgs } = flow.mintCharacter.expected; @@ -193,8 +195,13 @@ export async function mintExpectedFlow(context) { offerArgs.name, 'New character is added to contract registry', ); - // console.log("STORAGE NODE: ") - // console.log(storageNode.getPath("kread.character")) + 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); @@ -386,7 +393,7 @@ export async function mintNoCharactersAvailable(context) { zoe, } = context; const { offerArgs, message, give } = flow.mintCharacter.noAvailability; - + const mintCharacterInvitation = await E( publicFacet, ).makeMintCharacterInvitation(); @@ -422,7 +429,7 @@ export async function mintNoCharactersAvailable(context) { export async function mintInventoryCheck(context) { /** @type {Context} */ - const { publicFacet } = context; + const { publicFacet, getFromVStorage } = context; const { offerArgs } = flow.mintCharacter.expected; const characterInventory = await E(publicFacet).getCharacterInventory( @@ -443,22 +450,16 @@ export async function mintInventoryCheck(context) { 'Two or more items have the same category', ); - assert.equal( - mappedInventory.filter((i) => i.rarity < 20).length, - 2, - 'No two common items', - ); - - assert.equal( - mappedInventory.filter((i) => i.rarity > 19).length, - 1, - 'No uncommon to legendary item found.', + const vStorageInventoryItems = getFromVStorage( + `kread.character.inventory-${offerArgs.name}`, ); + mustMatch(vStorageInventoryItems, characterInventory.items); } export async function mintItemExpectedFlow(context) { /** @type {Context} */ - const { creatorFacet, contractAssets, purses, zoe } = context; + const { creatorFacet, contractAssets, purses, zoe, getFromVStorage } = + context; const { want, message } = flow.mintItem.expected; const mintItemInvitation = await E(creatorFacet).makeMintItemInvitation(); @@ -483,11 +484,30 @@ export async function mintItemExpectedFlow(context) { 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 } = context; + const { creatorFacet, contractAssets, purses, zoe, getFromVStorage } = context; const { want, message } = flow.mintItem.expected; const mintItemInvitation = await E(creatorFacet).makeMintItemInvitation(); @@ -518,7 +538,15 @@ export async function mintSameItemSFT(context) { 2n, 'Supply of item not increased to 2', ); - assert.equal((await E(purses.item).getCurrentAmount()).value.payload.length, 1); + 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) { @@ -545,13 +573,16 @@ export async function mintItemMultipleFlow(context) { await E(purses.item).deposit(payout); - const totalItems = (await E(purses.item) - .getCurrentAmount()) - .value.payload.reduce((acc, [_item, supply]) => { - return acc + supply; - }, 0n); + 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); + assert.equal( + (await E(purses.item).getCurrentAmount()).value.payload.length, + 2, + ); } export async function mintItemMultipleDifferentFlow(context) { @@ -577,5 +608,8 @@ export async function mintItemMultipleDifferentFlow(context) { const payout = await E(userSeat).getPayout('Asset'); await E(purses.item).deposit(payout); - assert.equal((await E(purses.item).getCurrentAmount()).value.payload.length, 4); + assert.equal( + (await E(purses.item).getCurrentAmount()).value.payload.length, + 4, + ); } diff --git a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js index 71c97799d..e9eec517a 100644 --- a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js @@ -70,6 +70,7 @@ import { testFunctionalityAfterUpgrade, testFunctionalityBeforeUpgrade, } from './bootstrap-upgrade-v2.js'; +import { unmarshalFromVstorage } from '@agoric/internal/src/lib-chainStorage.js'; const trace = makeTracer('kreadBootUpgrade'); @@ -161,6 +162,14 @@ export const buildRootObject = async () => { 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 + */ + const getFromVStorage = (path) => { + return unmarshalFromVstorage(storageKit.data, path, marshaller.fromCapData.bind(marshaller)) + } const committeeName = 'KREAd Committee'; @@ -288,7 +297,7 @@ export const buildRootObject = async () => { } = terms; context = { - storageNode: storageKit.rootNode, + getFromVStorage, contractAssets: { character: { issuer: characterIssuer, brand: characterBrand }, item: { issuer: itemIssuer, brand: itemBrand }, From 8d17e4d6329f7eaafc57c075af480d96ddc3f3dc Mon Sep 17 00:00:00 2001 From: Pandelis Symeonidis Date: Wed, 8 Nov 2023 09:27:50 +0100 Subject: [PATCH 11/17] chain storage market metric tests --- .../bootstrap/bootstrap-market-metrics.js | 54 +++++++++++++++++-- .../test/swingsetTests/test-market-metrics.js | 2 +- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market-metrics.js b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market-metrics.js index 346c16b0e..0d9dcadd7 100644 --- a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market-metrics.js +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market-metrics.js @@ -34,7 +34,7 @@ async function sellCharacter(context, user, characterName, askingPrice) { proposal, payment, ); - await E(userSeat).getOfferResult() + await E(userSeat).getOfferResult(); user.setMarketSeat(userSeat); @@ -100,7 +100,7 @@ export async function setupMarketMetricsTests(context) { export async function initialization(context) { /** @type {Context} */ - const { publicFacet } = context; + const { publicFacet, getFromVStorage } = context; const metrics = await E(publicFacet).getMarketMetrics(); @@ -109,6 +109,15 @@ export async function initialization(context) { 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) { @@ -118,6 +127,7 @@ export async function collectionSize(context) { paymentAsset, zoe, users: { bob }, + getFromVStorage } = context; const { give, offerArgs } = flow.mintCharacter.expected; @@ -149,12 +159,19 @@ export async function collectionSize(context) { 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; @@ -171,7 +188,7 @@ export async function averageLevelsCharacter(context) { ); 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); @@ -181,6 +198,14 @@ export async function averageLevelsCharacter(context) { 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) { @@ -188,6 +213,7 @@ export async function amountSoldCharacter(context) { const { publicFacet, users: { bob }, + getFromVStorage } = context; const { @@ -212,6 +238,16 @@ export async function amountSoldCharacter(context) { 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) { @@ -219,6 +255,7 @@ export async function latestSalePriceCharacter(context) { const { publicFacet, users: { bob }, + getFromVStorage } = context; const { @@ -247,4 +284,15 @@ export async function latestSalePriceCharacter(context) { 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/test-market-metrics.js b/agoric/contract/test/swingsetTests/test-market-metrics.js index 2070471db..8644e70c4 100644 --- a/agoric/contract/test/swingsetTests/test-market-metrics.js +++ b/agoric/contract/test/swingsetTests/test-market-metrics.js @@ -12,7 +12,7 @@ test.before(async (t) => { }); test.serial('---| METRICS - Initialization', async (t) => { - const [result] = await run('setupMarketMetricsTests'); + const [result] = await run('initialization'); t.is(result, 'fulfilled'); }); From f7a4fc837ec47ef5b86abdda65f44638e82e3c2f Mon Sep 17 00:00:00 2001 From: Pandelis Symeonidis Date: Wed, 8 Nov 2023 14:15:40 +0100 Subject: [PATCH 12/17] vstorage market tests --- .../bootstrap/bootstrap-market.js | 57 ++++++++++++++++++- .../bootstrap/bootstrap-upgradable.js | 28 ++++++--- 2 files changed, 74 insertions(+), 11 deletions(-) diff --git a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market.js b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market.js index 204103a46..67cd7e317 100644 --- a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market.js +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market.js @@ -1,12 +1,13 @@ import { AmountMath } from '@agoric/ertp'; import { E } from '@endo/eventual-send'; import { flow } from '../flow.js'; -import { makeCopyBag } from '@agoric/store'; +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); @@ -58,6 +59,10 @@ export async function sellCharacter(context) { zoe, users: { bob }, paymentAsset, + getFromVStorage, + royaltyRate, + platformFeeRate, + storageKit, } = context; const { market: { @@ -109,12 +114,27 @@ export async function sellCharacter(context) { 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}`, + ); + // + 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) { @@ -188,6 +208,7 @@ export async function buyCharacter(context) { platformFeePurse, royaltyRate, platformFeeRate, + getFromVStorage } = context; const { @@ -265,6 +286,13 @@ export async function buyCharacter(context) { 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) { @@ -328,6 +356,9 @@ export async function sellItem(context) { zoe, users: { bob }, paymentAsset, + getFromVStorage, + royaltyRate, + platformFeeRate } = context; const itemToSellValue = (await bob.getItems()).find( @@ -361,6 +392,19 @@ export async function sellItem(context) { 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 + 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) { @@ -421,6 +465,7 @@ export async function buyItem(context) { contractAssets, zoe, users: { bob, alice }, + getFromVStorage } = context; let itemsForSale = await E(publicFacet).getItemsForSale(); @@ -473,6 +518,14 @@ export async function buyItem(context) { 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) { diff --git a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js index e9eec517a..9182ad15b 100644 --- a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js @@ -1,8 +1,11 @@ -import { Far, deeplyFulfilled } from '@endo/marshal'; +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 { + makeFakeBoard, + slotToBoardRemote, +} from '@agoric/vats/tools/board-utils'; import { makeTracer } from '@agoric/internal'; import { Fail, NonNullish } from '@agoric/assert'; import { makeIssuerKit } from '@agoric/ertp'; @@ -165,11 +168,20 @@ export const buildRootObject = async () => { /** * Reads the data from the vstorage at the given path * Will throw an error if path doesnt exist - * @param {string} path + * @param {string} path */ const getFromVStorage = (path) => { - return unmarshalFromVstorage(storageKit.data, path, marshaller.fromCapData.bind(marshaller)) - } + const { fromCapData } = makeMarshal( + undefined, + (slot, iface) => { + return Far((iface ?? '').replace(/^Alleged: /, ''), {}); + }, + { + serializeBodyFormat: 'smallcaps', + }, + ); + return unmarshalFromVstorage(storageKit.data, path, fromCapData); + }; const committeeName = 'KREAd Committee'; @@ -283,10 +295,7 @@ export const buildRootObject = async () => { governorFacets.creatorFacet, ).getCreatorFacet(); - await E(creatorFacet).initializeBaseAssets( - baseCharacters, - baseItems, - ); + await E(creatorFacet).initializeBaseAssets(baseCharacters, baseItems); await E(creatorFacet).initializeCharacterNamesEntries(); await E(creatorFacet).initializeMetrics(); @@ -297,6 +306,7 @@ export const buildRootObject = async () => { } = terms; context = { + storageKit, getFromVStorage, contractAssets: { character: { issuer: characterIssuer, brand: characterBrand }, From a14a5a9cc1dbaaa951934e9ad8e0e86292095a7c Mon Sep 17 00:00:00 2001 From: Pandelis Symeonidis Date: Wed, 8 Nov 2023 15:22:33 +0100 Subject: [PATCH 13/17] more market vstorage tests --- .../bootstrap/bootstrap-market.js | 71 +++++++++++++++---- 1 file changed, 58 insertions(+), 13 deletions(-) diff --git a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market.js b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market.js index 67cd7e317..93d44be6b 100644 --- a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market.js +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market.js @@ -122,8 +122,14 @@ export async function sellCharacter(context) { const vStorageCharacterMarket = getFromVStorage( `kread.market-characters.character-${characterToSell.name}`, ); - // - mustMatch(vStorageCharacterMarket.asset, harden({...characterToSell, date: {...characterToSell.date, timerBrand: TimerBrandShape}})); + // + 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( @@ -208,7 +214,7 @@ export async function buyCharacter(context) { platformFeePurse, royaltyRate, platformFeeRate, - getFromVStorage + getFromVStorage, } = context; const { @@ -291,7 +297,10 @@ export async function buyCharacter(context) { `kread.market-characters.character-${characterToBuy.asset.name}`, ); } catch (error) { - assert.equal(error.message, `no data for "kread.market-characters.character-${characterToBuy.asset.name}"`) + assert.equal( + error.message, + `no data for "kread.market-characters.character-${characterToBuy.asset.name}"`, + ); } } @@ -358,7 +367,7 @@ export async function sellItem(context) { paymentAsset, getFromVStorage, royaltyRate, - platformFeeRate + platformFeeRate, } = context; const itemToSellValue = (await bob.getItems()).find( @@ -392,7 +401,7 @@ export async function sellItem(context) { 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 + const vStorageItemMarket = getFromVStorage(`kread.market-items.item-0`); // this is the first item on sale so we know it will be assigned id 0 mustMatch(vStorageItemMarket.asset, itemToSellValue); assert.equal(vStorageItemMarket.id, 0); assert.equal(vStorageItemMarket.askingPrice.value, priceAmount.value); @@ -465,7 +474,7 @@ export async function buyItem(context) { contractAssets, zoe, users: { bob, alice }, - getFromVStorage + getFromVStorage, } = context; let itemsForSale = await E(publicFacet).getItemsForSale(); @@ -520,11 +529,9 @@ export async function buyItem(context) { assert.equal(itemsForSale.length, 0, 'Item was not removed from market'); try { - getFromVStorage( - `kread.market-items.item-0`, - ); + getFromVStorage(`kread.market-items.item-0`); } catch (error) { - assert.equal(error.message, `no data for "kread.market-items.item-0"`) + assert.equal(error.message, `no data for "kread.market-items.item-0"`); } } @@ -773,7 +780,14 @@ export async function buyItemOfferMoreThanAskingPrice(context) { export async function internalSellItemBatch(context) { /** @type {Context} */ - const { publicFacet, creatorFacet, paymentAsset } = context; + const { + publicFacet, + creatorFacet, + paymentAsset, + getFromVStorage, + royaltyRate, + platformFeeRate, + } = context; const itemCollection = Object.values(defaultItems).map((item) => [item, 3n]); const itemsToSell = harden(itemCollection); @@ -784,7 +798,26 @@ export async function internalSellItemBatch(context) { const itemsForSale = await E(publicFacet).getItemsForSale(); assert.equal(itemsForSale.length, 27, 'Item was not added to market'); - // assert.equal((await bob.getItems()).length, 0, "Item is no longer in bob's wallet"); + 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) { @@ -795,6 +828,7 @@ export async function buyBatchSoldItem(context) { zoe, users: { bob }, paymentAsset, + getFromVStorage } = context; const itemsForSale = await E(publicFacet).getItemsForSale(); @@ -851,4 +885,15 @@ export async function buyBatchSoldItem(context) { 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}"`, + ); + } } From 26a4fb30618d9eb71c2af28b0a3c01b68a831a0b Mon Sep 17 00:00:00 2001 From: Pandelis Symeonidis Date: Thu, 9 Nov 2023 09:21:30 +0100 Subject: [PATCH 14/17] inventory and upgrade vstorage tests --- .../bootstrap/bootstrap-inventory.js | 55 ++++++++++++++++--- .../bootstrap/bootstrap-upgradable.js | 5 +- .../bootstrap/bootstrap-upgrade-v2.js | 14 ++++- 3 files changed, 61 insertions(+), 13 deletions(-) diff --git a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-inventory.js b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-inventory.js index fc3caad86..c71a9fa12 100644 --- a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-inventory.js +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-inventory.js @@ -14,7 +14,6 @@ const unequipOffer = async (context) => { /** @type {Context} */ const { publicFacet, contractAssets, purses, zoe } = context; const { characterName } = flow.inventory; - const characterInventory = await E(publicFacet).getCharacterInventory( characterName, ); @@ -73,7 +72,7 @@ const unequipOffer = async (context) => { export async function unequipItem(context) { /** @type {Context} */ - const { publicFacet, purses } = context; + const { publicFacet, purses, getFromVStorage } = context; const { characterName } = flow.inventory; await unequipOffer(context); @@ -96,6 +95,11 @@ export async function unequipItem(context) { 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) { @@ -245,7 +249,7 @@ export async function unequipWithWrongCharacter(context) { export async function equipItem(context) { /** @type {Context} */ - const { publicFacet, contractAssets, purses, zoe } = context; + const { publicFacet, contractAssets, purses, zoe, getFromVStorage } = context; const { characterName, equip: { message }, @@ -331,6 +335,15 @@ export async function equipItem(context) { '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") } @@ -443,7 +456,7 @@ export async function equipItemDuplicateCategory(context) { export async function swapItems(context) { /** @type {Context} */ - const { publicFacet, contractAssets, purses, zoe } = context; + const { publicFacet, contractAssets, purses, zoe, getFromVStorage } = context; const { characterName } = flow.inventory; const invitation = await E(publicFacet).makeItemSwapInvitation(); @@ -536,11 +549,19 @@ export async function swapItems(context) { 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 } = context; + const { publicFacet, contractAssets, purses, zoe, getFromVStorage } = context; const { characterName } = flow.inventory; await unequipOffer(context); @@ -630,6 +651,17 @@ export async function swapItemsInitiallyEmpty(context) { 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) { @@ -742,7 +774,7 @@ export async function swapItemsDifferentCategories(context) { } export async function unequipAll(context) { - const { publicFacet, contractAssets, purses, zoe } = context; + const { publicFacet, contractAssets, purses, zoe, getFromVStorage } = context; const { characterName } = flow.inventory; let characterInventory = await E(publicFacet).getCharacterInventory( @@ -811,10 +843,15 @@ export async function unequipAll(context) { characterName, ); assert.equal(characterInventory.items.length, 0); + + mustMatch( + getFromVStorage(`kread.character.inventory-${characterName}`), + harden([]), + ); } export async function unequipAllEmptyInventory(context) { - const { publicFacet, contractAssets, purses, zoe } = context; + const { publicFacet, contractAssets, purses, zoe, getFromVStorage } = context; const { characterName } = flow.inventory; @@ -871,4 +908,8 @@ export async function unequipAllEmptyInventory(context) { characterName, 'CharacterKey was not returned to character purse', ); + mustMatch( + getFromVStorage(`kread.character.inventory-${characterName}`), + harden([]), + ); } diff --git a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js index 9182ad15b..66bc1a852 100644 --- a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js @@ -2,10 +2,7 @@ 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, - slotToBoardRemote, -} from '@agoric/vats/tools/board-utils'; +import { makeFakeBoard } from '@agoric/vats/tools/board-utils'; import { makeTracer } from '@agoric/internal'; import { Fail, NonNullish } from '@agoric/assert'; import { makeIssuerKit } from '@agoric/ertp'; diff --git a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgrade-v2.js b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgrade-v2.js index 8369d2b53..1c4257231 100644 --- a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgrade-v2.js +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgrade-v2.js @@ -16,7 +16,7 @@ export async function setupUpgradeTests(context) { export async function testFunctionalityBeforeUpgrade(context) { /** @type {Context} */ - const { publicFacet, zoe, purses, paymentAsset } = context; + const { publicFacet, zoe, purses, paymentAsset, getFromVStorage } = context; const mintCharacterInvitation = await E( publicFacet, ).makeMintCharacterInvitation(); @@ -43,11 +43,16 @@ export async function testFunctionalityBeforeUpgrade(context) { '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 } = context; + const { publicFacet, zoe, purses, paymentAsset, getFromVStorage } = context; const mintCharacterInvitation = await E( publicFacet, ).makeMintCharacterInvitation(); @@ -74,4 +79,9 @@ export async function testFunctionalityAfterUpgrade(context) { 'example2', ); assert.equal(characterInventory.items.length, 4); + + const vStorageInventory = getFromVStorage( + `kread.character.inventory-example2`, + ); + assert.equal(vStorageInventory.length, 4); } From a62f55194e6f82a8e117deaa9be71d5ebf8255b6 Mon Sep 17 00:00:00 2001 From: Pandelis Symeonidis Date: Thu, 9 Nov 2023 09:43:58 +0100 Subject: [PATCH 15/17] add comments --- .../swingsetTests/bootstrap/bootstrap-market.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market.js b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market.js index 93d44be6b..5be921f8a 100644 --- a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market.js +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-market.js @@ -122,7 +122,9 @@ export async function sellCharacter(context) { 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({ @@ -402,6 +404,9 @@ export async function sellItem(context) { "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); @@ -828,7 +833,7 @@ export async function buyBatchSoldItem(context) { zoe, users: { bob }, paymentAsset, - getFromVStorage + getFromVStorage, } = context; const itemsForSale = await E(publicFacet).getItemsForSale(); @@ -887,9 +892,7 @@ export async function buyBatchSoldItem(context) { ); try { - getFromVStorage( - `kread.market-items.item-${itemToBuy.id}`, - ); + getFromVStorage(`kread.market-items.item-${itemToBuy.id}`); } catch (error) { assert.equal( error.message, From ecdadce25c23a4d3a63d21f6ee290855f605e809 Mon Sep 17 00:00:00 2001 From: Pandelis Symeonidis Date: Wed, 15 Nov 2023 09:55:59 +0100 Subject: [PATCH 16/17] update unmarshalling --- agoric/contract/src/proposal/kread-committee-script.js | 2 +- .../test/swingsetTests/bootstrap/bootstrap-upgradable.js | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) 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/test/swingsetTests/bootstrap/bootstrap-upgradable.js b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js index 66bc1a852..eca748846 100644 --- a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js @@ -70,7 +70,7 @@ import { testFunctionalityAfterUpgrade, testFunctionalityBeforeUpgrade, } from './bootstrap-upgrade-v2.js'; -import { unmarshalFromVstorage } from '@agoric/internal/src/lib-chainStorage.js'; +import { unmarshalFromVstorage } from '@agoric/vats/tools/board-utils'; const trace = makeTracer('kreadBootUpgrade'); @@ -166,8 +166,9 @@ export const buildRootObject = async () => { * 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) => { + const getFromVStorage = (path, index = -1) => { const { fromCapData } = makeMarshal( undefined, (slot, iface) => { @@ -177,7 +178,7 @@ export const buildRootObject = async () => { serializeBodyFormat: 'smallcaps', }, ); - return unmarshalFromVstorage(storageKit.data, path, fromCapData); + return unmarshalFromVstorage(storageKit.data, path, fromCapData, index); }; const committeeName = 'KREAd Committee'; From a3f571e55a30a38c21d16a55ce1f797a411d84c1 Mon Sep 17 00:00:00 2001 From: Pandelis Symeonidis Date: Wed, 15 Nov 2023 10:58:35 +0100 Subject: [PATCH 17/17] add checks to null upgrade test --- .../bootstrap/bootstrap-null-upgrade.js | 35 +++++++++++++++++++ .../bootstrap/bootstrap-upgradable.js | 4 +++ .../test/swingsetTests/test-null-upgrade.js | 14 +++++++- 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 agoric/contract/test/swingsetTests/bootstrap/bootstrap-null-upgrade.js 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 index eca748846..77a8bc197 100644 --- a/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js +++ b/agoric/contract/test/swingsetTests/bootstrap/bootstrap-upgradable.js @@ -71,6 +71,7 @@ import { 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'); @@ -511,5 +512,8 @@ export const buildRootObject = async () => { testFunctionalityAfterUpgrade: async () => { await testFunctionalityAfterUpgrade(context); }, + mint: async () => { + await mint(context) + } }); }; diff --git a/agoric/contract/test/swingsetTests/test-null-upgrade.js b/agoric/contract/test/swingsetTests/test-null-upgrade.js index 9b7bf6dac..f3250e940 100644 --- a/agoric/contract/test/swingsetTests/test-null-upgrade.js +++ b/agoric/contract/test/swingsetTests/test-null-upgrade.js @@ -3,12 +3,24 @@ 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'); +});