diff --git a/package.json b/package.json index fbfa4c8b59b..633debfc2f2 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "packages/bundle-source", "packages/import-bundle", "packages/eventual-send", + "packages/governance", "packages/promise-kit", "packages/tame-metering", "packages/transform-metering", diff --git a/packages/governance/CHANGELOG.md b/packages/governance/CHANGELOG.md new file mode 100644 index 00000000000..767840ada8e --- /dev/null +++ b/packages/governance/CHANGELOG.md @@ -0,0 +1,5 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + diff --git a/packages/governance/NEWS.md b/packages/governance/NEWS.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/governance/jsconfig.json b/packages/governance/jsconfig.json new file mode 100644 index 00000000000..195554cb5e5 --- /dev/null +++ b/packages/governance/jsconfig.json @@ -0,0 +1,18 @@ +// This file can contain .js-specific Typescript compiler config. +{ + "compilerOptions": { + "target": "esnext", + + "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/packages/governance/package.json b/packages/governance/package.json new file mode 100644 index 00000000000..bdebba1b597 --- /dev/null +++ b/packages/governance/package.json @@ -0,0 +1,83 @@ +{ + "name": "@agoric/governance", + "version": "0.1.0", + "description": "Core governance support", + "parsers": { + "js": "mjs" + }, + "main": "src/missing.js", + "engines": { + "node": ">=11.0" + }, + "scripts": { + "build": "exit 0", + "test": "ava", + "test:xs": "exit 0", + "lint-fix": "yarn lint:eslint --fix && yarn lint:types", + "lint-check": "yarn lint", + "lint": "yarn lint:types && yarn lint:eslint", + "lint:eslint": "eslint '**/*.js'", + "lint:types": "tsc -p jsconfig.json" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Agoric/agoric-sdk.git" + }, + "author": "Agoric", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/Agoric/agoric-sdk/issues" + }, + "homepage": "https://github.com/Agoric/agoric-sdk#readme", + "dependencies": { + "@agoric/assert": "^0.2.12", + "@agoric/bundle-source": "^1.3.7", + "@agoric/captp": "^1.7.13", + "@agoric/deploy-script-support": "^0.2.9", + "@agoric/ertp": "^0.11.2", + "@agoric/eventual-send": "^0.13.14", + "@agoric/marshal": "^0.4.11", + "@agoric/nat": "^4.0.0", + "@agoric/notifier": "^0.3.14", + "@agoric/promise-kit": "^0.2.13", + "@agoric/store": "^0.4.14", + "@agoric/swingset-vat": "^0.17.2", + "@agoric/zoe": "^0.15.7" + }, + "devDependencies": { + "@agoric/install-ses": "^0.5.13", + "ava": "^3.12.1", + "esm": "^3.2.25", + "ses": "^0.12.7" + }, + "files": [ + "bundles/", + "src/", + "exported.js", + "NEWS.md" + ], + "ava": { + "files": [ + "test/**/test-*.js" + ], + "require": [ + "esm" + ], + "timeout": "10m" + }, + "eslintConfig": { + "extends": [ + "@agoric" + ] + }, + "eslintIgnore": [ + "bundle-*.js" + ], + "prettier": { + "trailingComma": "all", + "singleQuote": true + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/governance/src/param-manager.js b/packages/governance/src/param-manager.js new file mode 100644 index 00000000000..b06771ea882 --- /dev/null +++ b/packages/governance/src/param-manager.js @@ -0,0 +1,93 @@ +// @ts-check + +import { makeStore } from '@agoric/store'; +import { assert, details as X } from '@agoric/assert'; +import { Nat } from '@agoric/nat'; +import { assertIsRatio } from '@agoric/zoe/src/contractSupport'; +import { AmountMath, looksLikeBrand } from '@agoric/ertp'; + +const ParamType = { + NAT: 'nat', + BIGINT: 'bigint', + STRING: 'string', + RATIO: 'ratio', + AMOUNT: 'amount', + BRAND: 'brand', +}; +harden(ParamType); + +function assertType(type, value, name) { + if (!type) { + // undefined type means don't verify. Did we omit an interesting type? + return; + } + + switch (type) { + case ParamType.NAT: + Nat(value); + break; + case ParamType.BIGINT: + assert.typeof(value, 'bigint'); + break; + case ParamType.STRING: + assert.typeof(value, 'string'); + break; + case ParamType.RATIO: + assertIsRatio(value); + break; + case ParamType.AMOUNT: + // TODO(hibbert): is there a clean way to assert something is an amount? + // An alternate approach would be to say the contract knows the brand and + // the managed parameter only needs to supply the value. + assert( + AmountMath.isEqual(value, value), + X`value for ${name} must be an Amount, was ${value}`, + ); + break; + case ParamType.BRAND: + assert( + looksLikeBrand(value), + X`value for ${name} must be a brand, was ${value}`, + ); + break; + default: + assert.fail(X`unknown type guard ${type}`); + } +} + +function parse(paramDesc) { + const bindings = makeStore('name'); + const types = makeStore('name'); + + paramDesc.forEach(({ name, value, type }) => { + assert.typeof(name, 'string'); + assertType(type, value, name); + bindings.init(name, value); + types.init(name, type); + }); + + return { bindings, types }; +} + +/** @type {BuildParamManager} */ +function buildParamManager(paramDesc) { + const { bindings, types } = parse(paramDesc); + + const publicFacet = { + lookup(name) { + return bindings.get(name); + }, + }; + + const manager = { + update(name, value) { + assertType(types.get(name), value, name); + bindings.set(name, value); + }, + }; + + return { publicFacet, manager }; +} +harden(buildParamManager); + +export { ParamType, buildParamManager }; diff --git a/packages/governance/src/types.js b/packages/governance/src/types.js new file mode 100644 index 00000000000..0521a569f59 --- /dev/null +++ b/packages/governance/src/types.js @@ -0,0 +1,34 @@ +// @ts-check + +/** + * @typedef {'nat' | 'bigint' | 'string' | 'ratio' | 'amount' | 'brand' } ParamType + */ + +/** + * @typedef {Object} ParamManager + * @property {(name: string, value: any) => void} update + */ + +/** + * @typedef {Object} ParamManagerPublic + * @property {(name: string) => any} lookup + */ + +/** + * @typedef {{name: string, + * value: any, + * type: string, + * }} ParamDescription + */ + +// type: string above should be type: ParamType + +/** + * @typedef {Array} ParamDescriptions + */ + +/** + * @callback BuildParamManager + * @param {ParamDescriptions} paramDesc + * @returns {{publicFacet: ParamManagerPublic, manager: ParamManager }} + */ diff --git a/packages/governance/test/test-param-manager.js b/packages/governance/test/test-param-manager.js new file mode 100644 index 00000000000..60c3dfd3bab --- /dev/null +++ b/packages/governance/test/test-param-manager.js @@ -0,0 +1,163 @@ +// @ts-check + +import { test } from '@agoric/zoe/tools/prepare-test-env-ava'; +import '@agoric/zoe/exported'; +import { AmountMath, AssetKind, makeIssuerKit } from '@agoric/ertp'; +import { makeRatio } from '@agoric/zoe/src/contractSupport'; + +import { buildParamManager, ParamType } from '../src/param-manager'; + +const BASIS_POINTS = 10_000; + +test('params one Nat', async t => { + const { publicFacet, manager } = buildParamManager([ + { + name: 'number', + value: 13, + type: ParamType.NAT, + }, + ]); + t.is(publicFacet.lookup('number'), 13); + manager.update('number', 42); + t.is(publicFacet.lookup('number'), 42); + + t.throws( + () => manager.update('string', 'foo'), + { + message: '"name" not found: "string"', + }, + '"string" was not a registered name', + ); + t.throws( + () => manager.update('number', 18.1), + { + message: '18.1 not a safe integer', + }, + 'value should be a nat', + ); +}); + +test('params one String', async t => { + const { publicFacet, manager } = buildParamManager([ + { + name: 'string', + value: 'foo', + type: ParamType.STRING, + }, + ]); + t.is(publicFacet.lookup('string'), 'foo'); + manager.update('string', 'bar'); + t.is(publicFacet.lookup('string'), 'bar'); + + t.throws( + () => manager.update('number', 'foo'), + { + message: '"name" not found: "number"', + }, + '"number" was not a registered name', + ); + t.throws( + () => manager.update('string', 18.1), + { + message: '18.1 must be a string', + }, + 'value should be a string', + ); +}); + +test('params one Amount', async t => { + const { brand } = makeIssuerKit('roses', AssetKind.SET); + const { publicFacet, manager } = buildParamManager([ + { + name: 'amount', + value: AmountMath.makeEmpty(brand), + type: ParamType.AMOUNT, + }, + ]); + t.deepEqual(publicFacet.lookup('amount'), AmountMath.makeEmpty(brand)); + manager.update('amount', AmountMath.make(brand, [13])); + t.deepEqual(publicFacet.lookup('amount'), AmountMath.make(brand, [13])); + + t.throws( + () => manager.update('number', 13), + { + message: '"name" not found: "number"', + }, + '"number" was not a registered name', + ); + + t.throws( + () => manager.update('amount', 18.1), + { + message: + "The amount 18.1 doesn't look like an amount. Did you pass a value instead?", + }, + 'value should be a amount', + ); +}); + +test('params one BigInt', async t => { + const { publicFacet, manager } = buildParamManager([ + { + name: 'bigint', + value: 314159n, + type: ParamType.BIGINT, + }, + ]); + t.deepEqual(publicFacet.lookup('bigint'), 314159n); + manager.update('bigint', 271828182845904523536n); + t.deepEqual(publicFacet.lookup('bigint'), 271828182845904523536n); + + t.throws( + () => manager.update('bigint', 18.1), + { + message: '18.1 must be a bigint', + }, + 'value should be a bigint', + ); +}); + +test('params one ratio', async t => { + const { brand } = makeIssuerKit('roses', AssetKind.SET); + const { publicFacet, manager } = buildParamManager([ + { + name: 'ratio', + value: makeRatio(7, brand), + type: ParamType.RATIO, + }, + ]); + t.deepEqual(publicFacet.lookup('ratio'), makeRatio(7, brand)); + manager.update('ratio', makeRatio(701, brand, BASIS_POINTS)); + t.deepEqual(publicFacet.lookup('ratio'), makeRatio(701, brand, BASIS_POINTS)); + + t.throws( + () => manager.update('ratio', 18.1), + { + message: 'Ratio 18.1 must be a record with 2 fields.', + }, + 'value should be a ratio', + ); +}); + +test('params one brand', async t => { + const { brand: roseBrand } = makeIssuerKit('roses', AssetKind.SET); + const { brand: thornBrand } = makeIssuerKit('thorns'); + const { publicFacet, manager } = buildParamManager([ + { + name: 'brand', + value: roseBrand, + type: ParamType.BRAND, + }, + ]); + t.deepEqual(publicFacet.lookup('brand'), roseBrand); + manager.update('brand', thornBrand); + t.deepEqual(publicFacet.lookup('brand'), thornBrand); + + t.throws( + () => manager.update('brand', 18.1), + { + message: 'value for "brand" must be a brand, was 18.1', + }, + 'value should be a brand', + ); +});