From 21702e60d7d70cbba7ba9a8beadede093dc9ecbf Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Sat, 30 Dec 2023 19:34:06 -0600 Subject: [PATCH 1/4] chore: module-to-script tools for keeping proposals in module form but reducing to script for deployment --- src/utils/module-to-script.js | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/utils/module-to-script.js diff --git a/src/utils/module-to-script.js b/src/utils/module-to-script.js new file mode 100644 index 0000000..feec1be --- /dev/null +++ b/src/utils/module-to-script.js @@ -0,0 +1,10 @@ +// @ts-check + +export const redactImportDecls = txt => + txt.replace(/^\s*import\b\s*(.*)/gm, '// REDACTED: $1'); + +export const omitExportKewords = txt => txt.replace(/^\s*export\b\s*/gm, ''); + +// cf. ses rejectImportExpressions +// https://github.com/endojs/endo/blob/ebc8f66e9498f13085a8e64e17fc2f5f7b528faa/packages/ses/src/transforms.js#L143 +export const hideImportExpr = txt => txt.replace(/\bimport\b/g, 'XMPORT'); From 9c6e1d5c825b32084791a88101833489b1171ebc Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Sat, 30 Dec 2023 19:34:56 -0600 Subject: [PATCH 2/4] feat: add interchain asset proposal template --- src/lib/addInterchainAsset.js | 124 +++++++++++++++++++++++++++++ src/lib/addInterchainAsset.spec.ts | 55 +++++++++++++ 2 files changed, 179 insertions(+) create mode 100644 src/lib/addInterchainAsset.js create mode 100644 src/lib/addInterchainAsset.spec.ts diff --git a/src/lib/addInterchainAsset.js b/src/lib/addInterchainAsset.js new file mode 100644 index 0000000..2015bab --- /dev/null +++ b/src/lib/addInterchainAsset.js @@ -0,0 +1,124 @@ +/** + * 1. start a mintHolder instance + * a. publish displayInfo under boardAux + * 2. register the resulting issuer with the vbank + */ +// @ts-check +/* global assert, harden */ + +import { E } from '@endo/far'; + +const interchainAssetOptions = { + denom: 'ibc/...', + decimalPlaces: 6, + issuerName: 'Asset1', + proposedName: 'Asset1', +}; + +/** @template T @typedef {import('@endo/eventual-send').ERef} ERef */ +/** + * @typedef {object} StorageNode + * @property {(data: string) => Promise} setValue + * @property {(subPath: string, options?: {sequence?: boolean}) => StorageNode} makeChildNode + */ + +export const toCapDataString = x => + JSON.stringify({ body: '#' + JSON.stringify(x), slots: [] }); + +const BOARD_AUX = 'boardAux'; +/** + * Publish Brand displayInfo using boardAux conventions + * + * @param {ERef} chainStorage + * @param {ERef} board + * @param {Brand} brand + */ +const publishBrandInfo = async (chainStorage, board, brand) => { + const [boardId, displayInfo] = await Promise.all([ + E(board).getId(brand), + E(brand).getDisplayInfo(), + ]); + const boardAux = E(chainStorage).makeChildNode(BOARD_AUX); + const node = E(boardAux).makeChildNode(boardId); + const value = toCapDataString({ displayInfo }); + await E(node).setValue(value); +}; + +/** + * based on publishInterchainAssetFromBank + * https://github.com/Agoric/agoric-sdk/blob/fb940f84636c4ac9c984a593ec4b5a8ae5150039/packages/inter-protocol/src/proposals/addAssetToVault.js + * + * @param {*} powers + */ +const execute = async powers => { + const { + consume: { chainStorage, board, bankManager, startUpgradable }, + installation: { + consume: { mintHolder }, + }, + issuer: { produce: produceIssuer }, + brand: { produce: produceBrand }, + instance: { produce: produceInstance }, + } = powers; + + const { + denom, + decimalPlaces, + keyword, + issuerName = keyword, + proposedName = keyword, + } = interchainAssetOptions; + + assert.typeof(denom, 'string'); + assert.typeof(decimalPlaces, 'number'); + assert.typeof(issuerName, 'string'); + assert.typeof(proposedName, 'string'); + + const terms = { + keyword: issuerName, // "keyword" is a misnomer in mintHolder terms + assetKind: AssetKind.NAT, + displayInfo: { + decimalPlaces, + assetKind: AssetKind.NAT, + }, + }; + + const { + creatorFacet: mint, + publicFacet: issuer, + instance, + } = await E(startUpgradable)({ + installation: mintHolder, + label: issuerName, + privateArgs: undefined, + terms, + }); + + const brand = await E(issuer).getBrand(); + const kit = { mint, issuer, brand }; + + publishBrandInfo(chainStorage, board, brand); + [produceIssuer, produceBrand, produceInstance].forEach(p => + p[issuerName].reset() + ); + produceIssuer[issuerName].resolve(issuer); + produceBrand[issuerName].resolve(brand); + produceInstance[issuerName].resolve(instance); + + await E(bankManager).addAsset(denom, issuerName, proposedName, kit); +}; + +export const permit = { + consume: { + chainStorage: true, + board: true, + bankManager: true, + startUpgradable: true, + }, + installation: { + consume: { mintHolder: true }, + }, + issuer: { produce: true }, + brand: { produce: true }, + instance: { produce: true }, +}; diff --git a/src/lib/addInterchainAsset.spec.ts b/src/lib/addInterchainAsset.spec.ts new file mode 100644 index 0000000..b8cffae --- /dev/null +++ b/src/lib/addInterchainAsset.spec.ts @@ -0,0 +1,55 @@ +/** + * 1. start a mintHolder instance + * a. publish displayInfo under boardAux + * 2. register the resulting issuer with the vbank + */ +// @ts-check +/* global describe, expect, it, harden, Compartment */ + +import '../installSesLockdown.js'; +import { readFile } from 'fs/promises'; +import { createRequire } from 'module'; +import { makeMarshal } from '@endo/marshal'; +import { toCapDataString } from './addInterchainAsset.js'; +import { + hideImportExpr, + omitExportKewords, + redactImportDecls, +} from '../utils/module-to-script.js'; + +const nodeRequire = createRequire(import.meta.url); + +const assets = { + addInterchainAsset: nodeRequire.resolve('./addInterchainAsset.js'), +}; + +describe('toCapDataString', () => { + it('agrees with marshal', () => { + const cases = [ + { decimalPlaces: 6 }, + { assetKind: 'nat' }, + // HAZARD: caller has to sort keys + { assetKind: 'nat', decimalPlaces: 18 }, + ]; + const m = makeMarshal(undefined, undefined, { + serializeBodyFormat: 'smallcaps', + }); + for (const data of cases) { + harden(data); + const actual = toCapDataString(data); + const expected = JSON.stringify(m.toCapData(data)); + expect(actual).toBe(expected); + } + }); +}); + +describe('addInterchainAsset.js', () => { + it('can easily be rendered as a script', async () => { + const modText = await readFile(assets.addInterchainAsset, 'utf8'); + const script = hideImportExpr( + omitExportKewords(redactImportDecls(modText)) + ); + const c = new Compartment({ E: () => {}, Far: () => {} }); + expect(() => c.evaluate(script)).not.toThrow(); + }); +}); From 21aae6f2055f7d6679224bc6c6b969d131bd91a8 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Sat, 30 Dec 2023 19:35:38 -0600 Subject: [PATCH 3/4] feat: AssetInfo component (WIP) --- src/components/AssetInfo.tsx | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/components/AssetInfo.tsx diff --git a/src/components/AssetInfo.tsx b/src/components/AssetInfo.tsx new file mode 100644 index 0000000..9426277 --- /dev/null +++ b/src/components/AssetInfo.tsx @@ -0,0 +1,34 @@ +import React from 'react'; + +export const AssetInfo: React.FC = () => { + return ( +
+
+ Asset Info + +
+ +
+ +
    +
  • + stretch goal: oracle support: addresses +
  • +
  • + stretch goal: vault collateral option +
  • +
+
+
+ ); +}; From 162a2a1b6cec2c9d0e12f5e0f590c80973dd591a Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Sat, 30 Dec 2023 19:35:53 -0600 Subject: [PATCH 4/4] chore: add AssetInfo to proposal form (WIP) --- src/components/ProposalForm.tsx | 56 +++++++++++++++++---------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/src/components/ProposalForm.tsx b/src/components/ProposalForm.tsx index 63aebcb..5b8dc09 100644 --- a/src/components/ProposalForm.tsx +++ b/src/components/ProposalForm.tsx @@ -5,15 +5,16 @@ import { forwardRef, FormEvent, ReactNode, -} from "react"; -import { CodeInputGroup } from "./CodeInputGroup"; -import { CoreEval } from "@agoric/cosmic-proto/swingset/swingset.js"; -import { Button } from "./Button"; -import { ParamChange } from "cosmjs-types/cosmos/params/v1beta1/params"; -import { ParameterChangeFormSection } from "./ParameterChangeForm"; -import { DepositSection } from "./DepositSection"; -import { paramOptions } from "../config/agoric"; -import type { ParameterChangeTypeOption } from "../types/form"; +} from 'react'; +import { CodeInputGroup } from './CodeInputGroup'; +import { CoreEval } from '@agoric/cosmic-proto/swingset/swingset.js'; +import { Button } from './Button'; +import { ParamChange } from 'cosmjs-types/cosmos/params/v1beta1/params'; +import { ParameterChangeFormSection } from './ParameterChangeForm'; +import { DepositSection } from './DepositSection'; +import { paramOptions } from '../config/agoric'; +import type { ParameterChangeTypeOption } from '../types/form'; +import { AssetInfo } from './AssetInfo.tsx'; type BaseProposalArgs = { title: string; @@ -23,22 +24,22 @@ type BaseProposalArgs = { export type ProposalArgs = BaseProposalArgs & ProposalDetail; -export type QueryType = ReturnType<(typeof paramOptions)[number]["query"]>; +export type QueryType = ReturnType<(typeof paramOptions)[number]['query']>; export type SelectorReturnType = ReturnType< - (typeof paramOptions)[number]["selector"] + (typeof paramOptions)[number]['selector'] >; export type ProposalDetail = - | { msgType: "textProposal" } - | { msgType: "coreEvalProposal"; evals: CoreEval[] } - | { msgType: "parameterChangeProposal"; changes: ParamChange[] }; + | { msgType: 'textProposal' } + | { msgType: 'coreEvalProposal'; evals: CoreEval[] } + | { msgType: 'parameterChangeProposal'; changes: ParamChange[] }; interface ProposalFormProps { title: string; description: string | ReactNode; handleSubmit: (proposal: ProposalArgs) => void; titleDescOnly?: boolean; - msgType: QueryParams["msgType"]; + msgType: QueryParams['msgType']; governanceForumLink: string; } @@ -70,23 +71,23 @@ const ProposalForm = forwardRef( if (formRef?.current) { const formData = new FormData(formRef.current); if (formData) { - const title = (formData.get("title") as string) || ""; - const description = (formData.get("description") as string) || ""; - const depositBld = (formData.get("deposit") as string) || ""; + const title = (formData.get('title') as string) || ''; + const description = (formData.get('description') as string) || ''; + const depositBld = (formData.get('deposit') as string) || ''; const deposit = Number(depositBld) * 1_000_000; const args: BaseProposalArgs = { title, description, deposit }; - if (msgType === "coreEvalProposal" && evals.length) { + if (msgType === 'coreEvalProposal' && evals.length) { return handleSubmit({ ...args, msgType, evals }); - } else if (msgType == "textProposal") { + } else if (msgType == 'textProposal') { return handleSubmit({ ...args, msgType }); - } else if (msgType === "parameterChangeProposal") { + } else if (msgType === 'parameterChangeProposal') { const changes = paramChangeRef.current?.getChanges(); - if (!Array.isArray(changes)) throw new Error("No changes"); + if (!Array.isArray(changes)) throw new Error('No changes'); return handleSubmit({ ...args, msgType, changes }); } } } - throw new Error("Error reading form data."); + throw new Error('Error reading form data.'); }; return ( @@ -101,7 +102,7 @@ const ProposalForm = forwardRef(

- {msgType === "parameterChangeProposal" ? ( + {msgType === 'parameterChangeProposal' ? ( ref={paramChangeRef} options={ @@ -143,13 +144,13 @@ const ProposalForm = forwardRef( name="description" rows={3} className="block w-full max-w-2xl rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-teal-600 sm:text-sm sm:leading-6" - defaultValue={""} + defaultValue={''} placeholder="Description" />

Write a few sentences about the proposal and include any relevant links. Before proposing to Mainnet, please ensure - you've started a discussion on the{" "} + you've started a discussion on the{' '} (

- {msgType === "coreEvalProposal" ? ( + + {msgType === 'coreEvalProposal' ? (