diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
new file mode 100644
index 00000000..3235ae98
--- /dev/null
+++ b/.github/workflows/pr.yml
@@ -0,0 +1,24 @@
+name: Lint and Test
+
+on:
+ pull_request:
+
+jobs:
+ all:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout Repo
+ uses: actions/checkout@v3
+ - name: Use Node.js 18.8.x
+ uses: actions/setup-node@v3
+ with:
+ # use node 18.8.x until Agoric/agoric-sdk#8636
+ node-version: '18.8.x'
+ - name: yarn install
+ run: yarn
+ - name: yarn lint
+ run: yarn lint
+ - name: yarn build
+ run: yarn build
+ - name: yarn test
+ run: yarn test
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..a547bf36
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..cbcff8a0
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,111 @@
+## Testing the contract (WIP)
+
+```sh
+cd contract
+yarn # may take a while
+yarn test
+```
+
+```
+yarn run v1.22.21
+$ ava --verbose
+
+start proposal module evaluating
+bundles/ add: assetContract from /home/connolly/projects/dapp-offer-up/contract/src/offer-up.contract.js
+ ✔ bundle-source › bundleSource() bundles the contract for use with zoe (1s)
+ ℹ 7fffb45de65f0c887401d4a5c5185ad87d41e3842d6eb2e10559a06c747358fe0dc5ef41fd4c04457c5e9bb27ed85e48ea1ff8bdeac524063b7743205f4817e6
+ ℹ Object @Alleged: BundleInstallation {}
+bundles/ bundled 85 files in bundle-assetContract.js at 2024-01-23T02:30:57.437Z
+startOfferUpContract()...
+ ✔ contract › Install the contract
+ ℹ Object @Alleged: BundleInstallation {}
+ ✔ contract › Start the contract (901ms)
+ ℹ terms: {
+ tradePrice: {
+ brand: Object @Alleged: PlayMoney brand {},
+ value: 5n,
+ },
+ }
+ ℹ Object @Alleged: InstanceHandle {}
+CoreEval script: started contract Object [Alleged: InstanceHandle] {}
+ ✔ contract › Alice trades: give some play money, want items (939ms)
+ ℹ Object @Alleged: InstanceHandle {}
+ ℹ Alice gives {
+ Price: {
+ brand: Object @Alleged: PlayMoney brand {},
+ value: 5n,
+ },
+ }
+ ℹ Alice payout brand Object @Alleged: Item brand {}
+ ℹ Alice payout value Object @copyBag {
+ payload: [
+ [
+ 'scroll',
+ 1n,
+ ],
+ [
+ 'map',
+ 1n,
+ ],
+ ],
+ }
+CoreEval script: share via agoricNames: Object [Alleged: Item brand] {}
+offerUp (re)started
+----- OfferUp.2 2 trade give { Price: { brand: Object [Alleged: PlayMoney brand] {}, value: 5n } } want Object [copyBag] { payload: [ [ 'scroll', 1n ], [ 'map', 1n ] ] }
+bundles/ add: centralSupply from /home/connolly/projects/dapp-offer-up/node_modules/@agoric/vats/src/centralSupply.js
+bundles/ bundled 132 files in bundle-centralSupply.js at 2024-01-23T02:30:59.505Z
+----- OfferUp.2 2 trade give {
+ Price: { brand: Object [Alleged: ZDEFAULT brand] {}, value: 250000n }
+} want Object [copyBag] { payload: [ [ 'scroll', 1n ], [ 'map', 1n ] ] }
+ ✔ contract › Trade in IST rather than play money (2.5s)
+ ℹ Alice gives {
+ Price: {
+ brand: Object @Alleged: ZDEFAULT brand {},
+ value: 250000n,
+ },
+ }
+ ℹ Alice payout brand Object @Alleged: Item brand {}
+ ℹ Alice payout value Object @copyBag {
+ payload: [
+ [
+ 'scroll',
+ 1n,
+ ],
+ [
+ 'map',
+ 1n,
+ ],
+ ],
+ }
+ ✔ contract › use the code that will go on chain to start the contract (2.5s)
+ ℹ Alice gives {
+ Price: {
+ brand: Object @Alleged: ZDEFAULT brand {},
+ value: 250000n,
+ },
+ }
+ ℹ Alice payout brand Object @Alleged: Item brand {}
+ ℹ Alice payout value Object @copyBag {
+ payload: [
+ [
+ 'scroll',
+ 1n,
+ ],
+ [
+ 'map',
+ 1n,
+ ],
+ ],
+ }
+----- OfferUp.2 2 trade give {
+ Price: { brand: Object [Alleged: ZDEFAULT brand] {}, value: 250000n }
+} want Object [copyBag] { payload: [ [ 'scroll', 1n ], [ 'map', 1n ] ] }
+ ─
+
+ 6 tests passed
+Done in 4.74s.
+```
+
+Any `Error#1: changed ...` diagnostics are benign reports of updated files
+outdating the contract bundle. It's benign: the test will re-build the bundle
+as necessary.
diff --git a/README-local-chain.md b/README-local-chain.md
new file mode 100644
index 00000000..2cdc2a62
--- /dev/null
+++ b/README-local-chain.md
@@ -0,0 +1,21 @@
+# Agoric Local Chain with docker-compose
+
+To start a local agoric blockchain:
+
+```sh
+docker compose up -d
+```
+
+**NOTE**: The image, which is several Gb, has to be pulled
+and extracted the first time you start the docker application
+services.
+
+Then use `docker-compose logs` etc. as usual.
+
+Some useful recipies are included in `Makefile`.
+Use `yarn make:help` to list them.
+For example: `yarn docker:make mint4k`.
+
+See also https://github.com/Agoric/documentation/pull/881
+
+See also the [Agoric Gov Proposal Builder](https://cosgov.org/).
diff --git a/README.md b/README.md
index a67589bd..0ac69f9e 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,24 @@
-# dapp-agoric-basics
\ No newline at end of file
+# dapp-agoric-basics
+
+This is a simple app for the [Agoric smart contract platform](https://docs.agoric.com/).
+
+
+
+The contract lets you make an offer to give a small amount of [IST](https://inter.trade/) in exchange for
+a few NFTs.
+
+## 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.
diff --git a/_agstate/.keep b/_agstate/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/_agstate/agoric-servers/package.json b/_agstate/agoric-servers/package.json
new file mode 100644
index 00000000..72c4137c
--- /dev/null
+++ b/_agstate/agoric-servers/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "offer-up-agservers",
+ "version": "0.0.1",
+ "description": "Agoric server instances for dapp-offer-up",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "build": "exit 0",
+ "test": "exit 0",
+ "lint": "exit 0",
+ "lint-check": "exit 0",
+ "lint-fix": "exit 0"
+ },
+ "author": "Agoric",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@agoric/cosmic-swingset": "^0.41.3"
+ }
+}
diff --git a/api/README-no-api.md b/api/README-no-api.md
new file mode 100644
index 00000000..0d51a366
--- /dev/null
+++ b/api/README-no-api.md
@@ -0,0 +1 @@
+This dapp has no API.
diff --git a/api/package.json b/api/package.json
new file mode 100644
index 00000000..d7719757
--- /dev/null
+++ b/api/package.json
@@ -0,0 +1,5 @@
+{
+ "$note": "@agoric/create-dapp@0.1.0 expects an api/package.json",
+ "name": "offer-up-api",
+ "version": "0.1.0"
+}
diff --git a/contract/.gitignore b/contract/.gitignore
new file mode 100644
index 00000000..eb994f6c
--- /dev/null
+++ b/contract/.gitignore
@@ -0,0 +1,3 @@
+start-offer-up-permit.json
+start-offer-up.js
+bundles/
diff --git a/contract/Makefile b/contract/Makefile
new file mode 100644
index 00000000..f2b85069
--- /dev/null
+++ b/contract/Makefile
@@ -0,0 +1,121 @@
+CHAINID=agoriclocal
+USER1ADDR=$(shell agd keys show user1 -a --keyring-backend="test")
+ACCT_ADDR=$(USER1ADDR)
+BLD=000000ubld
+
+ATOM_DENOM=ibc/BA313C4A19DFBF943586C0387E6B11286F9E416B4DD27574E6909CABE0E342FA
+ATOM=000000$(ATOM_DENOM)
+
+.PHONY: list
+# https://stackoverflow.com/a/73159833/7963
+list:
+ @make -npq : 2> /dev/null | grep -v PHONY |\
+ awk -v RS= -F: '$$1 ~ /^[^#%]+$$/ { print $$1 }'
+
+balance-q:
+ agd keys show user1 -a --keyring-backend="test"
+ agd query bank balances $(ACCT_ADDR)
+
+GAS_ADJUSTMENT=1.2
+SIGN_BROADCAST_OPTS=--keyring-backend=test --chain-id=$(CHAINID) \
+ --gas=auto --gas-adjustment=$(GAS_ADJUSTMENT) \
+ --yes -b block
+
+mint100:
+ make FUNDS=1000$(ATOM) fund-acct
+ cd /usr/src/agoric-sdk && \
+ yarn --silent agops vaults open --wantMinted 100 --giveCollateral 100 >/tmp/want-ist.json && \
+ yarn --silent agops perf satisfaction --executeOffer /tmp/want-ist.json --from user1 --keyring-backend=test
+
+# https://agoric.explorers.guru/proposal/61
+lower-bundle-cost: bundles/lower-bundle-cost.json ./scripts/voteLatestProposalAndWait.sh
+ agd tx gov submit-proposal param-change bundles/lower-bundle-cost.json \
+ $(SIGN_BROADCAST_OPTS) \
+ --from user1
+ ./scripts/voteLatestProposalAndWait.sh
+ # agd query swingset params
+
+
+bundles/swingset-params.json:
+ mkdir -p bundles/
+ agd query swingset params -o json >$@
+
+.ONESHELL:
+bundles/lower-bundle-cost.json: bundles/swingset-params.json
+ @read PARAMS < bundles/swingset-params.json; export PARAMS
+ node - <<- EOF >$@
+ const storageByte = '20000000';
+ const paramChange = {
+ title: 'Lower Bundle Cost to 0.02 IST/Kb (a la mainnet 61)',
+ description: '0.02 IST/Kb',
+ deposit: '10000000ubld',
+ changes: [{
+ subspace: 'swingset',
+ key: 'beans_per_unit',
+ value: '...',
+ }],
+ };
+ const params = JSON.parse(process.env.PARAMS);
+ const ix = params.beans_per_unit.findIndex(({key}) => key === 'storageByte');
+ params.beans_per_unit[ix].beans = storageByte;
+ paramChange.changes[0].value = params.beans_per_unit;
+ console.log(JSON.stringify(paramChange, null, 2));
+ EOF
+
+# Keep mint4k around a while for compatibility
+mint4k:
+ make FUNDS=1000$(ATOM) fund-acct
+ cd /usr/src/agoric-sdk && \
+ yarn --silent agops vaults open --wantMinted 4000 --giveCollateral 1000 >/tmp/want4k.json && \
+ yarn --silent agops perf satisfaction --executeOffer /tmp/want4k.json --from user1 --keyring-backend=test
+
+FUNDS=321$(BLD)
+fund-acct:
+ agd tx bank send validator $(ACCT_ADDR) $(FUNDS) \
+ $(SIGN_BROADCAST_OPTS) \
+ -o json >,tx.json
+ jq '{code: .code, height: .height}' ,tx.json
+
+gov-q:
+ agd query gov proposals --output json | \
+ jq -c '.proposals[] | [.proposal_id,.voting_end_time,.status]'
+
+gov-voting-q:
+ agd query gov proposals --status=voting_period --output json | \
+ jq -c '.proposals[].proposal_id'
+
+PROPOSAL=1
+VOTE_OPTION=yes
+vote:
+ agd tx gov vote $(PROPOSAL) $(VOTE_OPTION) --from=validator \
+ $(SIGN_BROADCAST_OPTS) \
+ -o json >,tx.json
+ jq '{code: .code, height: .height}' ,tx.json
+
+instance-q:
+ agd query vstorage data published.agoricNames.instance -o json
+
+print-key: /root/.agoric/user1.key
+ @echo Import the following mnemonic into Keplr:
+ @cat $<
+ @echo
+ @echo -n 'The resulting address should be: '
+ @agd keys show user1 -a --keyring-backend="test"
+ @echo
+
+SCRIPT=start-offer-up.js
+PERMIT=start-offer-up-permit.json
+start-contract: $(SCRIPT) $(PERMIT) install-bundles
+ scripts/propose-start-contract.sh
+
+install-bundles: bundles/bundle-list
+ ./scripts/install-bundles.sh
+
+build-proposal: bundles/bundle-list
+
+bundles/bundle-list $(SCRIPT) $(PERMIT):
+ ./scripts/build-proposal.sh
+
+
+clean:
+ @rm -rf $(SCRIPT) $(PERMIT) bundles/
diff --git a/contract/jsconfig.json b/contract/jsconfig.json
new file mode 100644
index 00000000..e7dca004
--- /dev/null
+++ b/contract/jsconfig.json
@@ -0,0 +1,19 @@
+// This file can contain .js-specific Typescript compiler config.
+{
+ "compilerOptions": {
+ "target": "esnext",
+ "module": "ES2022",
+
+ "noEmit": true,
+ /*
+ // The following flags are for creating .d.ts files:
+ "noEmit": false,
+ "declaration": true,
+ "emitDeclarationOnly": true,
+*/
+ "downlevelIteration": true,
+ "strictNullChecks": true,
+ "moduleResolution": "node"
+ },
+ "include": ["src/**/*.js", "test/**/*.js", "exported.js", "globals.d.ts"]
+}
diff --git a/contract/package.json b/contract/package.json
new file mode 100644
index 00000000..1b473e83
--- /dev/null
+++ b/contract/package.json
@@ -0,0 +1,85 @@
+{
+ "name": "offer-up-contract",
+ "version": "0.1.0",
+ "private": true,
+ "description": "Offer Up Contract",
+ "type": "module",
+ "scripts": {
+ "start:docker": "docker compose up -d",
+ "docker:logs": "docker compose logs --tail 200 -f",
+ "docker:bash": "docker compose exec agd bash",
+ "docker:make": "docker compose exec agd make -C /workspace/contract",
+ "make:help": "make list",
+ "start": "yarn docker:make clean start-contract print-key",
+ "build": "agoric run scripts/build-contract-deployer.js",
+ "test": "ava --verbose",
+ "lint": "eslint '**/*.js'",
+ "lint:fix": "eslint --fix '**/*.js'"
+ },
+ "devDependencies": {
+ "agoric": "^0.21.2-u12.0",
+ "@agoric/deploy-script-support": "^0.10.4-u12.0",
+ "@endo/bundle-source": "^2.8.0",
+ "@agoric/eslint-config": "dev",
+ "@endo/eslint-plugin": "^0.5.2",
+ "@endo/init": "^0.5.60",
+ "@endo/promise-kit": "0.2.56",
+ "@endo/ses-ava": "^0.2.44",
+ "@jessie.js/eslint-plugin": "^0.4.0",
+ "@typescript-eslint/eslint-plugin": "^6.7.0",
+ "@typescript-eslint/parser": "^6.7.0",
+ "ava": "^5.3.0",
+ "eslint": "^8.47.0",
+ "eslint-config-airbnb-base": "^15.0.0",
+ "eslint-config-jessie": "^0.0.6",
+ "eslint-config-prettier": "^9.0.0",
+ "eslint-plugin-ava": "^14.0.0",
+ "eslint-plugin-github": "^4.10.0",
+ "eslint-plugin-import": "^2.25.3",
+ "eslint-plugin-jsdoc": "^46.4.3",
+ "eslint-plugin-prettier": "^5.0.0",
+ "import-meta-resolve": "^2.2.1",
+ "prettier": "^3.0.3",
+ "prettier-plugin-jsdoc": "^1.0.0",
+ "type-coverage": "^2.26.3",
+ "typescript": "~5.2.2"
+ },
+ "dependencies": {
+ "@agoric/zoe": "^0.26.3-u12.0",
+ "@agoric/ertp": "^0.16.3-u12.0",
+ "@endo/far": "^0.2.22",
+ "@endo/marshal": "^0.8.9",
+ "@endo/patterns": "^0.2.5"
+ },
+ "ava": {
+ "files": [
+ "test/**/test-*.js"
+ ],
+ "timeout": "10m"
+ },
+ "keywords": [],
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/Agoric/dapp-offer-up"
+ },
+ "author": "Agoric",
+ "license": "Apache-2.0",
+ "bugs": {
+ "url": "https://github.com/Agoric/dapp-offer-up/issues"
+ },
+ "homepage": "https://github.com/Agoric/dapp-offer-up#readme",
+ "eslintConfig": {
+ "parserOptions": {
+ "sourceType": "module",
+ "ecmaVersion": 2021
+ },
+ "extends": [
+ "@agoric"
+ ]
+ },
+ "prettier": {
+ "trailingComma": "all",
+ "arrowParens": "avoid",
+ "singleQuote": true
+ }
+}
diff --git a/contract/scripts/build-contract-deployer.js b/contract/scripts/build-contract-deployer.js
new file mode 100644
index 00000000..1986482a
--- /dev/null
+++ b/contract/scripts/build-contract-deployer.js
@@ -0,0 +1,36 @@
+/**
+ * @file Permission Contract Deployment builder
+ *
+ * Usage:
+ * agoric run build-contract-deployer.js
+ */
+
+import { makeHelpers } from '@agoric/deploy-script-support';
+import { getManifestForOfferUp } from '../src/offer-up-proposal.js';
+
+/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').ProposalBuilder} */
+export const offerUpProposalBuilder = async ({ publishRef, install }) => {
+ return harden({
+ sourceSpec: '../src/offer-up-proposal.js',
+ getManifestCall: [
+ getManifestForOfferUp.name,
+ {
+ offerUpRef: publishRef(
+ install(
+ '../src/offer-up.contract.js',
+ '../bundles/bundle-offer-up.js',
+ {
+ persist: true,
+ },
+ ),
+ ),
+ },
+ ],
+ });
+};
+
+/** @type {DeployScriptFunction} */
+export default async (homeP, endowments) => {
+ const { writeCoreProposal } = await makeHelpers(homeP, endowments);
+ await writeCoreProposal('start-offer-up', offerUpProposalBuilder);
+};
diff --git a/contract/scripts/build-proposal.sh b/contract/scripts/build-proposal.sh
new file mode 100755
index 00000000..68bdcadb
--- /dev/null
+++ b/contract/scripts/build-proposal.sh
@@ -0,0 +1,11 @@
+#!/bin/sh
+# NOTE: intended to run _inside_ the agd container
+
+cd /workspace/contract
+
+mkdir -p bundles
+(agoric run ./scripts/build-contract-deployer.js )>/tmp/,run.log
+./scripts/parseProposals.mjs bundles/bundle-list
+
+
diff --git a/contract/scripts/install-bundles.sh b/contract/scripts/install-bundles.sh
new file mode 100755
index 00000000..717257bb
--- /dev/null
+++ b/contract/scripts/install-bundles.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+# NOTE: intended to run _inside_ the agd container
+
+set -xueo pipefail
+
+cd /workspace/contract
+
+# TODO: try `agoric publish` to better track outcome
+install_bundle() {
+ ls -sh "$1"
+ agd tx swingset install-bundle --compress "@$1" \
+ --from user1 --keyring-backend=test --gas=auto --gas-adjustment=1.2 \
+ --chain-id=agoriclocal -bblock --yes -o json
+}
+
+# exit fail if bundle-list is emtpy
+[ -s bundles/bundle-list ] || exit 1
+
+make balance-q # do we have enough IST?
+
+for b in $(cat bundles/bundle-list); do
+ echo installing $b
+ install_bundle $b
+done
diff --git a/contract/scripts/parseProposals.mjs b/contract/scripts/parseProposals.mjs
new file mode 100755
index 00000000..daab1be3
--- /dev/null
+++ b/contract/scripts/parseProposals.mjs
@@ -0,0 +1,41 @@
+#!/usr/bin/env node
+
+import fs from 'fs';
+
+const Fail = (template, ...args) => {
+ throw Error(String.raw(template, ...args.map(val => String(val))));
+};
+
+/**
+ * Parse output of `agoric run proposal-builder.js`
+ *
+ * @param {string} txt
+ *
+ * adapted from packages/boot/test/bootstrapTests/supports.js
+ */
+const parseProposalParts = txt => {
+ const evals = [
+ ...txt.matchAll(/swingset-core-eval (?\S+) (?
+