diff --git a/README.md b/README.md index 777c5841..4db5ef53 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,33 @@ # Agoric Dapp Starter: Agoric Basics -This is a simple app for the [Agoric smart contract platform](https://docs.agoric.com/). - -Vite + React + Agoric page with Connect Wallet button - -The contract lets you make an offer to give a small amount of [IST](https://inter.trade/) in exchange for -a few NFTs. +This is a basic Agoric Dapp that contains three smart contracts `postal-service`, `sell-concert-tickets`, and `swaparoo` demonstrating different scenarios which can be implemented easily using Agoric SDK. There is also a UI for `sell-concert-tickets` contract that a user can use to buy three different types of concert tickets and pay through a wallet extension in the browser. ## Getting started -See [Your First Agoric Dapp](https://docs.agoric.com/guides/getting-started/) tutorial. - -## Contributing: Development, Testing - -The UI is a React app started with the [vite](https://vitejs.dev/) `react-ts` template. -On top of that, we add - -- Watching [blockchain state queries](https://docs.agoric.com/guides/getting-started/contract-rpc.html#querying-vstorage) -- [Signing and sending offers](https://docs.agoric.com/guides/getting-started/contract-rpc.html#signing-and-broadcasting-offers) - -See [CONTRIBUTING](./CONTRIBUTING.md) for more on testing. +Make sure all the required dependecies are already installed (including node, nvm, docker, Keplr, and that your node version is set to `18.x.x` by running `nvm use 18.20.2`. See [a tutorial here](https://docs.agoric.com/guides/getting-started/) on how to install these dependecies.). Here are the steps to run `dapp-agoric-basics`: +- run `yarn install` in the `agoric-basics` directory, to install dependencies of the Dapp. +- run `yarn start:docker` to start Agoric blockchain from the container. +- run `yarn docker:logs` to to make sure blocks are being produced by viewing the Docker logs; once your logs resemble the following, stop the logs by pressing `ctrl+c`. +``` +demo-agd-1 | 2023-12-27T04:08:06.384Z block-manager: block 1003 begin +demo-agd-1 | 2023-12-27T04:08:06.386Z block-manager: block 1003 commit +demo-agd-1 | 2023-12-27T04:08:07.396Z block-manager: block 1004 begin +demo-agd-1 | 2023-12-27T04:08:07.398Z block-manager: block 1004 commit +demo-agd-1 | 2023-12-27T04:08:08.405Z block-manager: block 1005 begin +demo-agd-1 | 2023-12-27T04:08:08.407Z block-manager: block 1005 commit +``` +- run `yarn start:contract` to start the contracts. +- run `yarn start:ui` to start `sell-concert-tickets` contract UI. +- open a browser and navigate to `localhost:5173` to interact with the contract via UI. + +To follow more detailed tutorial, go [here](https://docs.agoric.com/guides/getting-started/tutorial-dapp-agoric-basics.html). + +## Testing + +To perform unit tests: +-run the command `yarn test` in the root directory. +To perform end to end test +-run the command `yarn test:e2e` in the root directory. + +## Contributing +See [CONTRIBUTING](./CONTRIBUTING.md) for more on contributions. diff --git a/contract/Makefile b/contract/Makefile index 3bd65b81..fe726b6f 100644 --- a/contract/Makefile +++ b/contract/Makefile @@ -60,7 +60,12 @@ vote: instance-q: agd query vstorage data published.agoricNames.instance -o json -start-contract: start-contract-mint start-contract-swap start-contract-pay +start-contract: start-contract-mint start-contract-swap start-contract-pay start-contract-cate + +start-contract-cate: + yarn node scripts/deploy-contract.js \ + --install src/cateCoin.contract.js \ + --eval src/cateCoin.proposal.js start-contract-mint: yarn node scripts/deploy-contract.js \ diff --git a/contract/rollup.config.mjs b/contract/rollup.config.mjs index b5617069..53d42bee 100644 --- a/contract/rollup.config.mjs +++ b/contract/rollup.config.mjs @@ -22,7 +22,7 @@ import { permit as postalServicePermit } from './src/postal-service.proposal.js' import { permit as swapPermit } from './src/swaparoo.proposal.js'; import { permit as sellPermit } from './src/sell-concert-tickets.proposal.js'; import { permit as boardAuxPermit } from './src/platform-goals/board-aux.core.js'; - +import { permit as catePermit} from './src/cateCoin.proposal.js' /** * @param {*} opts * @returns {import('rollup').RollupOptions} @@ -89,5 +89,9 @@ const config = [ name: 'postal-service', permit: postalServicePermit, }), + config1({ + name: 'cateCoin', + permit: catePermit, + }), ]; export default config; diff --git a/contract/src/cateCoin.contract.js b/contract/src/cateCoin.contract.js new file mode 100644 index 00000000..b92ab7d8 --- /dev/null +++ b/contract/src/cateCoin.contract.js @@ -0,0 +1,100 @@ +import { AmountMath, makeIssuerKit } from '@agoric/ertp'; +import { Far } from '@endo/far'; + +import '@agoric/zoe/exported.js'; + +export const start = async zcf => { + // Step 0: get an issuer kit + const { + issuer: cateIssuer, + mint: cateMint, + brand: cateBrand, + } = makeIssuerKit('cateCoin'); + let currSupply = AmountMath.make(cateBrand, 0n); + const maxSupply = AmountMath.make(cateBrand, 1000_000n); + let creatorPurse; + let isInitialized = 0; + + const getIssuer = () => cateIssuer; + console.log('Name of the contract : ', zcf.name); + + const createInitialCoins = (myPurse, amount) => { + try { + if (isInitialized === 1) { + throw new Error('Fail - already initialized'); + } + + console.log('creating first coins'); + isInitialized = 1; + creatorPurse = myPurse; + + const cateAmount = AmountMath.make(cateBrand, amount); + if (AmountMath.isGTE(cateAmount, maxSupply)) { + throw new Error('Fail - amount exceeds maxSupply'); + } + + const catePayment = cateMint.mintPayment(cateAmount); + creatorPurse.deposit(catePayment); + currSupply = amount; + + const currentAmount = creatorPurse.getCurrentAmount(); + console.log( + 'Current Amount in my purse after deposit is : ', + currentAmount, + ); + return 'success'; + } catch (error) { + console.error('Error in createInitialCoins:', error); + return error.message; + } + }; + + const mintCateCoins = (myPurse, amount) => { + try { + if (creatorPurse !== myPurse) { + throw new Error('Fail - Only creator can mint new tokens'); + } + if (currSupply + amount > maxSupply) { + throw new Error('Fail - reached max supply'); + } + + const cateAmount = AmountMath.make(cateBrand, amount); + const catePayment = cateMint.mintPayment(cateAmount); + creatorPurse.deposit(catePayment); + currSupply += amount; + + return 'success'; + } catch (error) { + console.error('Error in mintCateCoins:', error); + return error.message; + } + }; + + const transferCateCoins = (fromPurse, toPurse, amount) => { + try { + const cateAmount = AmountMath.make(cateBrand, amount); + if (!AmountMath.isGTE(fromPurse.getCurrentAmount(), cateAmount)) { + throw new Error('Fail - not enough funds in sender account'); + } + + const catePayment = fromPurse.withdraw(cateAmount); + toPurse.deposit(catePayment); + return 'success'; + } catch (error) { + console.error('Error in transferCateCoins:', error); + return error.message; + } + }; + + return { + creatorFacet: Far('Creator Facet', { + mintCateCoins, + transferCateCoins, + getIssuer, + createInitialCoins, + }), + publicFacet: Far('Public Facet', { transferCateCoins }), + }; +}; + +harden(start); diff --git a/contract/src/cateCoin.proposal.js b/contract/src/cateCoin.proposal.js new file mode 100644 index 00000000..fea21ca0 --- /dev/null +++ b/contract/src/cateCoin.proposal.js @@ -0,0 +1,80 @@ +import { + installContract, + startContract, +} from './platform-goals/start-contract.js'; +import { allValues } from './objectTools.js'; + +// const { Fail } = assert; + +const contractName = 'cateCoin'; + +/** + * Core eval script to start contract + * + * @param {BootstrapPowers} permittedPowers + * @param {*} config + */ + +export const startCateCoin = async (powers, config) => { + console.log('core eval for', contractName); + // const { + // bundleID = Fail`no bundleID`, + // } = config?.options?.[contractName] ?? {}; + const bundleID = config?.options?.[contractName] ?? {}; + const installation = await installContract(powers, { + name: contractName, + bundleID, + }); + try { + const ist = await allValues({ + brand: powers.brand.consume.IST, + issuer: powers.issuer.consume.IST, + }); + + const terms = undefined; + + await startContract(powers, { + name: contractName, + startArgs: { + installation, + issuerKeywordRecord: { Price: ist.issuer }, + terms, + }, + issuerNames: ['Ticket'], + }); + + console.log(contractName, '(re)started'); + } catch (error) { + console.error(`Error starting ${contractName} contract:`, error); + throw error; // Rethrow the error after logging it + } +}; + +// Define a manifest object describing the contract's capabilities and permissions +export const manifest = /** @type {const} */ ({ + [startCateCoin.name]: { + // Define entry for the postalService contract + consume: { + // Resources consumed by the contract + agoricNames: true, // Needs access to the agoricNames registry + namesByAddress: true, // Needs access to the namesByAddress registry + namesByAddressAdmin: true, // Needs administrative access to the namesByAddress registry + startUpgradable: true, // Allows upgrades to the contract + zoe: true, // Needs access to the Zoe service for contract execution + }, + installation: { + // Capabilities provided by the contract during installation + consume: { [contractName]: true }, + produce: { [contractName]: true }, + }, + instance: { + // Capabilities provided by the contract instance + produce: { [contractName]: true }, // Produces a "postalService" instance + }, + }, +}); + +// Define the permit object based on the manifest +export const permit = Object.values(manifest)[0]; + +export const main = startCateCoin; diff --git a/contract/test/test-cateCoin.js b/contract/test/test-cateCoin.js new file mode 100644 index 00000000..99f764e4 --- /dev/null +++ b/contract/test/test-cateCoin.js @@ -0,0 +1,154 @@ +// @ts-check + +/* eslint-disable import/order -- https://github.com/endojs/endo/issues/1235 */ +import { test as anyTest } from './prepare-test-env-ava.js'; +import { AmountMath } from '@agoric/ertp'; +import { createRequire } from 'module'; +import { E } from '@endo/far'; +import { makeNodeBundleCache } from '@endo/bundle-source/cache.js'; +import { makeZoeKitForTest } from '@agoric/zoe/tools/setup-zoe.js'; + +const myRequire = createRequire(import.meta.url); +const contractPath = myRequire.resolve(`../src/cateCoin.contract.js`); + +const test = anyTest; + +const makeTestContext = async _t => { + const { zoeService: zoe, feeMintAccess } = makeZoeKitForTest(); + const bundleCache = await makeNodeBundleCache( + 'bundles/', + {}, + nodeModuleSpecifier => import(nodeModuleSpecifier), + ); + try { + const bundle = await bundleCache.load(contractPath, 'cateCoinContract'); + + await E(zoe).install(bundle); + + return { zoe, bundle, bundleCache, feeMintAccess }; + } catch (error) { + console.error('Error in makeTestContext:', error); + throw error; + } +}; + +test.before(async t => { + t.context = await makeTestContext(t); +}); + +test('Install CateCoin contract', async t => { + const { zoe, bundle } = t.context; + const installation = await E(zoe).install(bundle); + try { + t.log(installation); + t.is(typeof installation, 'object'); + } catch (error) { + console.error('Error in Install CateCoin contract test:', error); + t.fail(error.message); + } +}); + +test('Start CateCoin contract', async t => { + const { zoe, bundle } = t.context; + const installation = await E(zoe).install(bundle); + try { + const { instance } = await E(zoe).startInstance(installation); + t.log(instance); + t.is(typeof instance, 'object'); + } catch (error) { + console.error('Error in Start CateCoin contract test:', error); + t.fail(error.message); + } +}); + +test('createInitialCoins creates a fixed amount of initial CateCoin', async t => { + const { zoe, bundle } = t.context; + const installation = await E(zoe).install(bundle); + try { + const { creatorFacet } = await E(zoe).startInstance(installation); + const cateIssuer = await E(creatorFacet).getIssuer(); + const cateBrand = await E(cateIssuer).getBrand(); + + const myPurse = await E(cateIssuer).makeEmptyPurse(); + + await E(creatorFacet).createInitialCoins(myPurse, 100n); + + const amnt = await myPurse.getCurrentAmount(); + const amnt2 = AmountMath.make(cateBrand, 100n); + t.true(AmountMath.isEqual(amnt, amnt2)); + } catch (error) { + console.error('Error in createInitialCoins test:', error); + t.fail(error.message); + } +}); + +test('createInitialCoins CateCoin more than maxSupply', async t => { + const { zoe, bundle } = t.context; + const installation = await E(zoe).install(bundle); + try { + const { creatorFacet } = await E(zoe).startInstance(installation); + const cateIssuer = await E(creatorFacet).getIssuer(); + const cateBrand = await E(cateIssuer).getBrand(); + const myPurse = await E(cateIssuer).makeEmptyPurse(); + + await E(creatorFacet).createInitialCoins(myPurse, 1000_001n); + + const amnt = await myPurse.getCurrentAmount(); + const amnt2 = AmountMath.make(cateBrand, 0n); + t.true(AmountMath.isEqual(amnt, amnt2)); + } catch (error) { + console.error( + 'Error in createInitialCoins more than maxSupply test:', + error, + ); + t.fail(error.message); + } +}); + +test('transferCateCoins two purses', async t => { + const { zoe, bundle } = t.context; + const installation = await E(zoe).install(bundle); + try { + const { creatorFacet } = await E(zoe).startInstance(installation); + const cateIssuer = await E(creatorFacet).getIssuer(); + const cateBrand = cateIssuer.getBrand(); + const fromPurse = await E(cateIssuer).makeEmptyPurse(); + await E(creatorFacet).createInitialCoins(fromPurse, 1000n); + const toPurse = await E(cateIssuer).makeEmptyPurse(); + await E(creatorFacet).transferCateCoins(fromPurse, toPurse, 500n); + + const fromAmnt = await fromPurse.getCurrentAmount(); + const toAmnt = await toPurse.getCurrentAmount(); + t.true(AmountMath.isEqual(fromAmnt, AmountMath.make(cateBrand, 500n))); + t.true(AmountMath.isEqual(toAmnt, AmountMath.make(cateBrand, 500n))); + } catch (error) { + console.error('Error in transferCateCoins two purses test:', error); + t.fail(error.message); + } +}); + +test('transferCateCoins more than sender has', async t => { + const { zoe, bundle } = t.context; + const installation = await E(zoe).install(bundle); + try { + const { creatorFacet } = await E(zoe).startInstance(installation); + const cateIssuer = await E(creatorFacet).getIssuer(); + const cateBrand = cateIssuer.getBrand(); + const fromPurse = await E(cateIssuer).makeEmptyPurse(); + await E(creatorFacet).createInitialCoins(fromPurse, 1000n); + const toPurse = await E(cateIssuer).makeEmptyPurse(); + await E(creatorFacet).transferCateCoins(fromPurse, toPurse, 500n); + await E(creatorFacet).transferCateCoins(fromPurse, toPurse, 501n); + + const fromAmnt = await fromPurse.getCurrentAmount(); + const toAmnt = await toPurse.getCurrentAmount(); + t.true(AmountMath.isEqual(fromAmnt, AmountMath.make(cateBrand, 500n))); + t.true(AmountMath.isEqual(toAmnt, AmountMath.make(cateBrand, 500n))); + } catch (error) { + console.error( + 'Error in transferCateCoins more than sender has test:', + error, + ); + t.fail(error.message); + } +});