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 (
+
+ );
+};
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' ? (
} 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();
+ });
+});
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');