Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

interchain asset proposal builder (WIP) #18

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/components/AssetInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from 'react';

export const AssetInfo: React.FC<unknown> = () => {
return (
<form name="assetInfo">
<fieldset>
<caption>Asset Info</caption>
<label>
Name:
<br /> <input type="text" name="issuerName" placeholder="ABC" />
</label>
<br />
<label>
decimalPlaces:
<br />
<input type="number" name="decimalPlaces" defaultValue={6} min={1} />
</label>
<br />
<label>
denom:
<br /> <input type="text" name="denom" placeholder="ibc/DEADBEEF" />
</label>
<ul style={{ fontSize: 'small', fontStyle: 'italic' }}>
<li>
<em>stretch goal: oracle support: addresses</em>
</li>
<li>
<em>stretch goal: vault collateral option</em>
</li>
</ul>
</fieldset>
</form>
);
};
56 changes: 29 additions & 27 deletions src/components/ProposalForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}

Expand Down Expand Up @@ -70,23 +71,23 @@ const ProposalForm = forwardRef<ProposalFormMethods, ProposalFormProps>(
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 (
Expand All @@ -101,7 +102,7 @@ const ProposalForm = forwardRef<ProposalFormMethods, ProposalFormProps>(
</p>

<div className="mt-10 space-y-8 border-b border-gray-900/10 pb-12 sm:space-y-0 sm:divide-y sm:divide-gray-900/10 sm:border-t sm:pb-0">
{msgType === "parameterChangeProposal" ? (
{msgType === 'parameterChangeProposal' ? (
<ParameterChangeFormSection<QueryType, SelectorReturnType>
ref={paramChangeRef}
options={
Expand Down Expand Up @@ -143,13 +144,13 @@ const ProposalForm = forwardRef<ProposalFormMethods, ProposalFormProps>(
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"
/>
<p className="mt-3 text-sm leading-6 text-gray-600">
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{' '}
<a
className="cursor-pointer hover:text-gray-900 underline"
href={governanceForumLink}
Expand All @@ -161,7 +162,8 @@ const ProposalForm = forwardRef<ProposalFormMethods, ProposalFormProps>(
</div>
</div>

{msgType === "coreEvalProposal" ? (
<AssetInfo />
{msgType === 'coreEvalProposal' ? (
<div className="sm:grid sm:grid-cols-4 sm:items-start sm:gap-4 sm:py-6">
<label
htmlFor="description"
Expand Down
124 changes: 124 additions & 0 deletions src/lib/addInterchainAsset.js
Original file line number Diff line number Diff line change
@@ -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<T>} ERef<T> */
/**
* @typedef {object} StorageNode
* @property {(data: string) => Promise<void>} 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<StorageNode>} chainStorage
* @param {ERef<import('@agoric/vats').Board>} 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 },
};
55 changes: 55 additions & 0 deletions src/lib/addInterchainAsset.spec.ts
Original file line number Diff line number Diff line change
@@ -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';

Check failure on line 13 in src/lib/addInterchainAsset.spec.ts

View workflow job for this annotation

GitHub Actions / web

Could not find a declaration file for module './addInterchainAsset.js'. '/home/runner/work/cosmos-proposal-builder/cosmos-proposal-builder/src/lib/addInterchainAsset.js' implicitly has an 'any' type.
import {
hideImportExpr,
omitExportKewords,
redactImportDecls,
} from '../utils/module-to-script.js';

Check failure on line 18 in src/lib/addInterchainAsset.spec.ts

View workflow job for this annotation

GitHub Actions / web

Could not find a declaration file for module '../utils/module-to-script.js'. '/home/runner/work/cosmos-proposal-builder/cosmos-proposal-builder/src/utils/module-to-script.js' implicitly has an 'any' type.

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);

Check failure on line 38 in src/lib/addInterchainAsset.spec.ts

View workflow job for this annotation

GitHub Actions / web

Cannot find name 'harden'.
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: () => {} });

Check failure on line 52 in src/lib/addInterchainAsset.spec.ts

View workflow job for this annotation

GitHub Actions / web

Cannot find name 'Compartment'.
expect(() => c.evaluate(script)).not.toThrow();
});
});
10 changes: 10 additions & 0 deletions src/utils/module-to-script.js
Original file line number Diff line number Diff line change
@@ -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');
Loading