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

feat(launchIt): launch a token (WIP) #7

Draft
wants to merge 13 commits into
base: dc-starter
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
37 changes: 35 additions & 2 deletions contract/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -84,17 +84,50 @@ print-key: /root/.agoric/user1.key
@agd keys show user1 -a --keyring-backend="test"
@echo

start-contract: start-game1.js start-game1-permit.json install-bundles
scripts/propose-start-contract.sh
SCRIPT1=start-game1.js
PERMIT1=start-game1-permit.json
start-contract: $(SCRIPT1) $(PERMIT1) install-bundles
SCRIPT=$(SCRIPT1) PERMIT=$(PERMIT1) \
./scripts/propose-start-contract.sh

install-bundles: bundles/bundle-list
./scripts/install-bundles.sh

BUNDLE_INSTALLS=bundles/bundle-contractStarter.json.installed
PERMIT2=bundles/deploy-starter-permit.json
SCRIPT2=bundles/deploy-starter.js
deploy-contract: $(SCRIPT2) $(PERMIT2) $(BUNDLE_INSTALLS)
SCRIPT=$(SCRIPT2) PERMIT=$(PERMIT2) TITLE="Deploy Starter Contract" \
./scripts/propose-start-contract.sh

build-proposal: bundles/bundle-list

bundles/bundle-list start-game1.js start-game1-permit.json:
./scripts/build-proposal.sh

bundles/deploy-starter-permit.json bundles/deploy-starter.js: rollup.config.mjs src/start-contractStarter.js
yarn rollup -c rollup.config.mjs src/start-contractStarter.js

install-launchIt: bundles/bundle-launchIt.json.installed

bundles/bundle-contractStarter.json.installed: bundles/bundle-contractStarter.json

bundles/bundle-contractStarter.json: src/contractStarter.js src/boardAux.js
yarn --silent bundle-source --cache-json bundles/ src/contractStarter.js contractStarter

bundles/bundle-launchIt.json: src/launchIt.js
yarn --silent bundle-source --cache-json bundles/ src/launchIt.js launchIt

%.json.installed: %.json
ls -sh $<
agd tx swingset install-bundle --compress "@$<" \
-o json \
--from $(ACCT_ADDR) \
$(SIGN_BROADCAST_OPTS) >$@ || rm $@
@echo TODO: try agoric publish to better track outcome
jq '{bundleID: ("b1-" + .endoZipBase64Sha512)}' $<
jq '{code: .code, height: .height}' $@


clean:
@rm -rf start-game1.js start-game1-permit.json bundles/
1 change: 1 addition & 0 deletions contract/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"patch-package": "^8.0.0",
"prettier": "^3.0.3",
"prettier-plugin-jsdoc": "^1.0.0",
"rollup-plugin-node-resolve": "^5.2.0",
"type-coverage": "^2.26.3",
"typescript": "~5.2.2"
},
Expand Down
2 changes: 2 additions & 0 deletions contract/rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* - `permit` export is emitted as JSON
*/
// @ts-check
import nodeResolve from 'rollup-plugin-node-resolve';
import {
coreEvalGlobals,
moduleToScript,
Expand All @@ -28,6 +29,7 @@ const config = {
},
external: ['@endo/far'],
plugins: [
nodeResolve(),
configureBundleID({
name: 'contractStarter',
rootModule: './src/contractStarter.js',
Expand Down
226 changes: 226 additions & 0 deletions contract/src/launchIt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
// @ts-check
import { Far } from '@endo/far';
import { M, mustMatch } from '@endo/patterns';
import { BrandShape, DisplayInfoShape } from '@agoric/ertp/src/typeGuards.js';
import { AmountMath, AssetKind } from '@agoric/ertp/src/amountMath.js';
import {
TimerServiceShape,
TimestampRecordShape,
TimestampShape,
} from '@agoric/time/src/typeGuards.js';
import { atomicRearrange } from '@agoric/zoe/src/contractSupport/index.js';
import {
floorMultiplyBy,
makeRatio,
} from '@agoric/zoe/src/contractSupport/ratio';

const { Fail } = assert;

/** @type {import('./types').ContractMeta} */
export const meta = {
customTermsShape: M.splitRecord(
{ name: M.string(), supplyQty: M.bigint(), deadline: TimestampShape },
{ displayInfo: DisplayInfoShape },
),
};
export const { customTermsShape } = meta;
/**
* @typedef {{
* name: string,
* supplyQty: bigint,
* deadline: import('@agoric/time/src/types').TimestampRecord,
* displayInfo?: DisplayInfo,
* }} LaunchTerms
*/

const kw = s => (/^[A-Z]/.test(s) ? s : `KW${s}`); // only addresses initial cap

/**
* @param {ZCF} zcf
* @param {string} kw
*/
const makeZcfIssuerKit = async (zcf, kw) => {
const mint = await zcf.makeZCFMint(kw);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it's better to throw, than to prepend with KW. Is ^[A-Z]/ something that could/should be supported by @endo/patterns?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah; that's clearly a KLUDGE

endo/patterns avoids regex stuff.

The core issue is: makeZCFMint conflates issuerName with keyword. I want the brand to print as BRD, but keywords are more naturally for a role played by a brand in the contract; in this case, Minted. IOU an agoric-sdk issue on this.

const kit = mint.getIssuerRecord();
return { ...kit, mint };
};

/**
* This contract is limited to fungible assets.
*
* @param {ZCF<LaunchTerms>} zcf
* @param {unknown} _privateArgs
* @param {import('@agoric/vat-data').Baggage} _baggage
*/
export const start = async (zcf, _privateArgs, _baggage) => {
const {
name,
supplyQty,
deadline,
displayInfo = {},
brands,
} = zcf.getTerms();
mustMatch(brands, M.splitRecord({ Deposit: BrandShape }));
Copy link
Contributor

@0xpatrickdev 0xpatrickdev Jan 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why M.splitRecord(...) instead of harden({ Deposit: BrandShape })? Are we expecting brands to be keyed with anything else?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the time, I only needed this brand to exist for the code I was writing to be correct.

Stepping back and looking at the whole thing now, no, I don't think there are any others.

const { zcfSeat: deposits } = zcf.makeEmptySeatKit();

const assetMint = await zcf.makeZCFMint(kw(name), AssetKind.NAT, displayInfo);
const asset = assetMint.getIssuerRecord();
const share = await makeZcfIssuerKit(zcf, 'Share');

const ShapeAmt = {
Asset: harden({ brand: asset.brand, value: M.nat() }),
Deposit: harden({ brand: brands.Deposit, value: M.nat() }),
Share: harden({ brand: share.brand, value: M.nat() }),
};
const ExitDeadlineShape = {
afterDeadline: {
timer: TimerServiceShape,
deadline: TimestampRecordShape,
},
};
// export these from a client interface module?
const Shape = harden({
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's a subtle difference between M.splitRecord({ give: X, want: Y}) and M.splitRecord({ give: X }, { want: Y })

merits a comment

ack: @dtribble

Amount: ShapeAmt,
Proposal: {
Collect: M.splitRecord(
{ exit: ExitDeadlineShape },
{ want: { Deposit: ShapeAmt.Deposit } },
),
Deposit: M.splitRecord(
{ give: { Deposit: ShapeAmt.Deposit } },
{ want: { Shares: ShapeAmt.Share } },
),
Withdraw: M.splitRecord({
want: { Deposit: ShapeAmt.Deposit },

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has different bracing than the previous lines. That seems more like a bug than a difference.

give: { Shares: ShapeAmt.Share },
}),
Redeem: M.splitRecord(
{ give: { Shares: ShapeAmt.Share } },
{ want: { Minted: ShapeAmt.Asset } },
),
},
});

const Minted = AmountMath.make(asset.brand, supplyQty);

/**
* @typedef {{ tag: 'start'}
* | { tag: 'committed', totalShares: Amount<'nat'>
* }} LaunchState
*
* ISSUE: require collect offer before accepting deposits?
*/
/** @type {LaunchState} */
let state = { tag: 'start' };
const getTotalShares = () => {
if (state.tag !== 'committed') throw Fail`launch not committed`;
return state.totalShares;
};

const { zcfSeat: stage } = zcf.makeEmptySeatKit();
const mintShares = value => {
state.tag !== 'committed' || Fail`too late to deposit`;
const Shares = AmountMath.make(share.brand, value);
stage.clear(); // Deprecated but not @deprecated
share.mint.mintGains({ Shares }, stage);
return Shares;
};

/** @type {OfferHandler} */
const depositHandler = subscriber => {
const { give } = subscriber.getProposal();
const { Deposit } = give;
const Shares = mintShares(Deposit.value);
atomicRearrange(
zcf,
harden([
[subscriber, deposits, give],
[stage, subscriber, { Shares }],
]),
);
subscriber.exit();
return true;
};

/** @type {OfferHandler} */
const withdrawHandler = subscriber => {
state.tag !== 'committed' || Fail`past deadline to withdraw`;
const { want, give } = subscriber.getProposal();
const { Shares } = give;
const { Deposit } = want;
mustMatch(Shares.value, M.gte(Deposit.value), 'insufficient shares');
dckc marked this conversation as resolved.
Show resolved Hide resolved
atomicRearrange(zcf, harden([[deposits, subscriber, { Deposit }]]));
share.mint.burnLosses({ Shares }, subscriber);
subscriber.exit();
return true;
};

const { zcfSeat: lockup } = zcf.makeEmptySeatKit();
assetMint.mintGains({ Minted }, lockup);

/** @param {ZCFSeat} subscriber */
const redeemHandler = subscriber => {
const denom = getTotalShares();
const { give } = subscriber.getProposal();
const { Shares } = give;
const gains = floorMultiplyBy(
Minted,
makeRatio(Shares.value, Minted.brand, denom.value, Minted.brand),
);
atomicRearrange(zcf, harden([[lockup, subscriber, { Minted: gains }]]));
share.mint.burnLosses({ Shares }, subscriber);
subscriber.exit();
return true;
};

const publicFacet = Far('launchItPublic', {
makeDepositInvitation: () =>
zcf.makeInvitation(
depositHandler,
'deposit',
undefined,
Shape.Proposal.Deposit,
),
makeWithrawInvitation: () =>
zcf.makeInvitation(
withdrawHandler,
'withdraw',
undefined,
Shape.Proposal.Withdraw,
),
makeRedeemInvitation: () =>
zcf.makeInvitation(
redeemHandler,
'redeem',
undefined,
Shape.Proposal.Redeem,
),
});

/** @param {ZCFSeat} creator */
const collectHandler = async creator => {
const { exit } = creator.getProposal();
mustMatch(
'afterDeadline' in exit && exit.afterDeadline.deadline,
M.gte(deadline),
'must collect after deadline from terms',
);
const gains = deposits.getAmountAllocated('Deposit', brands.Deposit);
const Shares = AmountMath.make(share.brand, gains.value);
atomicRearrange(zcf, harden([[deposits, creator, { Deposit: gains }]]));
state = { tag: 'committed', totalShares: Shares };
return state.totalShares.value; // walletFactory passes primitives thru
};

const creatorFacet = Far('launchItCreator', {
Collect: () =>
zcf.makeInvitation(
collectHandler,
'Collect',
undefined,
Shape.Proposal.Collect,
),
});

return { publicFacet, creatorFacet };
};
Loading