Skip to content

Commit

Permalink
feat: stakeBld contract
Browse files Browse the repository at this point in the history
  • Loading branch information
turadg committed Mar 18, 2024
1 parent 36a38ce commit 2767bb5
Show file tree
Hide file tree
Showing 11 changed files with 449 additions and 2 deletions.
1 change: 1 addition & 0 deletions packages/boot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@agoric/inter-protocol": "^0.16.1",
"@agoric/kmarshal": "^0.1.0",
"@agoric/notifier": "^0.6.2",
"@agoric/orchestration": "^0.1.0",
"@agoric/swing-store": "^0.9.1",
"@agoric/swingset-vat": "^0.32.2",
"@agoric/telemetry": "^0.6.2",
Expand Down
88 changes: 88 additions & 0 deletions packages/boot/test/bootstrapTests/test-orchestration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js';

import type { TestFn } from 'ava';

import { Fail } from '@agoric/assert';
import type { start as stakeBldStart } from '@agoric/orchestration/src/contracts/stakeBld.contract.js';
import type { Instance } from '@agoric/zoe/src/zoeService/utils.js';
import { makeWalletFactoryContext } from './walletFactory.ts';

type DefaultTestContext = Awaited<ReturnType<typeof makeWalletFactoryContext>>;

const test: TestFn<DefaultTestContext> = anyTest;

test.before(async t => (t.context = await makeWalletFactoryContext(t)));
test.after.always(t => t.context.shutdown?.());

test('stakeBld', async t => {
const {
agoricNamesRemotes,
buildProposal,
evalProposal,
refreshAgoricNamesRemotes,
} = t.context;
// TODO move into a vm-config for u15
await evalProposal(
buildProposal('@agoric/builders/scripts/vats/init-localchain.js'),
);
// start-stakeBld depends on this. Sanity check in case the context changes.
const { BLD } = agoricNamesRemotes.brand;
BLD || Fail`BLD missing from agoricNames`;
await evalProposal(
buildProposal('@agoric/builders/scripts/orchestration/init-stakeBld.js'),
);
// update now that stakeBld is instantiated
refreshAgoricNamesRemotes();
const stakeBld = agoricNamesRemotes.instance.stakeBld as Instance<
typeof stakeBldStart
>;
t.truthy(stakeBld);

const wd = await t.context.walletFactoryDriver.provideSmartWallet(
'agoric1testStakeBld',
);

await wd.executeOffer({
id: 'request-stake',
invitationSpec: {
source: 'agoricContract',
instancePath: ['stakeBld'],
callPipe: [['makeStakeBldInvitation']],
},
proposal: {
give: {
// @ts-expect-error XXX BoardRemote
In: { brand: BLD, value: 10n },
},
},
});

const current = await wd.getCurrentWalletRecord();
const latest = await wd.getLatestUpdateRecord();
console.log({ current, latest });
t.like(current, {
offerToPublicSubscriberPaths: [
// TODO publish something useful
['request-stake', { account: 'published.stakeBld' }],
],
});
t.like(latest, {
status: { id: 'request-stake', numWantsSatisfied: 1 },
});

await wd.executeOffer({
id: 'request-delegate',
invitationSpec: {
source: 'continuing',
previousOffer: 'request-stake',
invitationMakerName: 'Delegate',
invitationArgs: ['agoric1validator1', { brand: BLD, value: 10n }],
},
proposal: {
give: {
// @ts-expect-error XXX BoardRemote
In: { brand: BLD, value: 10n },
},
},
});
});
10 changes: 10 additions & 0 deletions packages/boot/tools/supports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,16 @@ export const makeSwingsetTestKit = async (
return undefined;
case BridgeId.STORAGE:
return storage.toStorage(obj);
case BridgeId.VLOCALCHAIN:
switch (obj.type) {
case 'VLOCALCHAIN_ALLOCATE_ADDRESS':
return 'agoric1mockVlocalchainAddress';
case 'VLOCALCHAIN_EXECUTE_TX':
// returns one empty object per message
return obj.messages.map(() => ({}));
default:
throw Error(`VLOCALCHAIN message of unknown type ${obj.type}`);
}
default:
throw Error(`unknown bridgeId ${bridgeId}`);
}
Expand Down
23 changes: 23 additions & 0 deletions packages/builders/scripts/orchestration/init-stakeBld.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// @ts-check
import { makeHelpers } from '@agoric/deploy-script-support';

/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').ProposalBuilder} */
export const defaultProposalBuilder = async ({ publishRef, install }) =>
harden({
sourceSpec: '@agoric/orchestration/src/proposals/start-stakeBld.js',
getManifestCall: [
'getManifestForStakeBld',
{
installKeys: {
stakeBld: publishRef(
install('@agoric/orchestration/src/contracts/stakeBld.contract.js'),
),
},
},
],
});

export default async (homeP, endowments) => {
const { writeCoreProposal } = await makeHelpers(homeP, endowments);
await writeCoreProposal('start-stakeBld', defaultProposalBuilder);
};
8 changes: 6 additions & 2 deletions packages/orchestration/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,20 @@
},
"homepage": "https://github.com/Agoric/agoric-sdk#readme",
"dependencies": {
"@agoric/ertp": "^0.16.2",
"@agoric/internal": "^0.3.2",
"@agoric/notifier": "^0.6.2",
"@agoric/vat-data": "^0.5.2",
"@agoric/vats": "^0.15.1",
"@agoric/zoe": "^0.26.2",
"@agoric/zone": "^0.2.2",
"@endo/far": "^1.0.4",
"@endo/marshal": "^1.3.0",
"@endo/patterns": "^1.1.0"
},
"devDependencies": {
"@agoric/zoe": "^0.26.2",
"@cosmjs/amino": "^0.32.3",
"@cosmjs/proto-signing": "^0.32.3",
"@endo/far": "^1.0.4",
"ava": "^5.3.0",
"cosmjs-types": "^0.9.0"
},
Expand Down
154 changes: 154 additions & 0 deletions packages/orchestration/src/contracts/localchainAccountHolder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// @ts-check
/** @file Use-object for the owner of a localchain account */
import { AmountShape } from '@agoric/ertp';
import { makeTracer } from '@agoric/internal';
import { UnguardedHelperI } from '@agoric/internal/src/typeGuards.js';
import { M, prepareExoClassKit } from '@agoric/vat-data';
import { TopicsRecordShape } from '@agoric/zoe/src/contractSupport/index.js';
import { E } from '@endo/far';

const trace = makeTracer('LCAH');

const { Fail } = assert;
/**
* @typedef {object} LocalChainAccountNotification
* @property {string} address
*/

/**
* @typedef {{
* topicKit: import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit<LocalChainAccountNotification>;
* account: import('@agoric/vats/src/localchain.js').LocalChainAccount | null;
* }} State
*/

const HolderI = M.interface('holder', {
getPublicTopics: M.call().returns(TopicsRecordShape),
makeDelegateInvitation: M.call(M.string(), AmountShape).returns(M.promise()),
makeCloseAccountInvitation: M.call().returns(M.promise()),
makeTransferAccountInvitation: M.call().returns(M.promise()),
});

/** @type {{ [name: string]: [description: string, valueShape: Pattern] }} */
const PUBLIC_TOPICS = {
account: ['Account holder status', M.any()],
};

/**
* @param {import('@agoric/ertp').Baggage} baggage
* @param {import('@agoric/zoe/src/contractSupport/recorder.js').MakeRecorderKit} makeRecorderKit
* @param {ZCF} zcf
*/
export const prepareAccountHolder = (baggage, makeRecorderKit, zcf) => {
const makeAccountHolderKit = prepareExoClassKit(
baggage,
'Account Holder',
{
helper: UnguardedHelperI,
holder: HolderI,
invitationMakers: M.interface('invitationMakers', {
Delegate: HolderI.payload.methodGuards.makeDelegateInvitation,
CloseAccount: HolderI.payload.methodGuards.makeCloseAccountInvitation,
TransferAccount:
HolderI.payload.methodGuards.makeTransferAccountInvitation,
}),
},
/**
* @param {import('@agoric/vats/src/localchain.js').LocalChainAccount} account
* @param {StorageNode} storageNode
* @returns {State}
*/
(account, storageNode) => {
// must be the fully synchronous maker because the kit is held in durable state
const topicKit = makeRecorderKit(storageNode, PUBLIC_TOPICS.account[1]);

return { account, topicKit };
},
{
helper: {
/** @throws if this holder no longer owns the account */
owned() {
const { account } = this.state;
if (!account) {
throw Fail`Using account holder after transfer`;
}
return account;
},
getUpdater() {
return this.state.topicKit.recorder;
},
},
invitationMakers: {
Delegate(validatorAddress, amount) {
return this.facets.holder.makeDelegateInvitation(
validatorAddress,
amount,
);
},
CloseAccount() {
return this.facets.holder.makeCloseAccountInvitation();
},
TransferAccount() {
return this.facets.holder.makeTransferAccountInvitation();
},
},
holder: {
getPublicTopics() {
const { topicKit } = this.state;
return harden({
account: {
description: PUBLIC_TOPICS.account[0],
subscriber: topicKit.subscriber,
storagePath: topicKit.recorder.getStoragePath(),
},
});
},
/**
*
* @param {string} validatorAddress
* @param {Amount<'nat'>} ertpAmount
*/
async makeDelegateInvitation(validatorAddress, ertpAmount) {
trace('makeDelegateInvitation', validatorAddress, ertpAmount);

// FIXME get values from proposal or args
// FIXME brand handling and amount scaling
const amount = {
amount: ertpAmount.value,
denom: 'ubld',
};

return zcf.makeInvitation(async seat => {
// TODO should it allow delegating more BLD?
seat.exit();
const lca = this.facets.helper.owned();
trace('lca', lca);
const delegatorAddress = await E(lca).getAddress();
trace('delegatorAddress', delegatorAddress);
const result = await E(lca).executeTx([
{
'@type': '/cosmos.staking.v1beta1.MsgDelegate',
amount,
validatorAddress,
delegatorAddress,
},
]);
trace('got result', result);
return result;
}, 'Delegate');
},
makeCloseAccountInvitation() {
throw Error('not yet implemented');
},
/**
* Starting a transfer revokes the account holder. The associated updater
* will get a special notification that the account is being transferred.
*/
makeTransferAccountInvitation() {
throw Error('not yet implemented');
},
},
},
);
return makeAccountHolderKit;
};
82 changes: 82 additions & 0 deletions packages/orchestration/src/contracts/stakeBld.contract.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// @ts-check
/**
* @file Stake BLD contract
*
*/

import { makeTracer } from '@agoric/internal';
import { makeDurableZone } from '@agoric/zone/durable.js';
import { M } from '@endo/patterns';
import { E } from '@endo/far';
import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/recorder.js';
import { atomicTransfer } from '@agoric/zoe/src/contractSupport/atomicTransfer.js';
import { prepareAccountHolder } from './localchainAccountHolder.js';

const trace = makeTracer('StakeBld');

/**
*
* @param {ZCF} zcf
* @param {{
* localchain: import('@agoric/vats/src/localchain.js').LocalChain;
* marshaller: Marshaller;
* storageNode: StorageNode;
* }} privateArgs
* @param {import("@agoric/vat-data").Baggage} baggage
*/
export const start = async (zcf, privateArgs, baggage) => {
const { BLD } = zcf.getTerms().brands;

const bldAmountShape = await E(BLD).getAmountShape();

const zone = makeDurableZone(baggage);

const { makeRecorderKit } = prepareRecorderKitMakers(
baggage,
privateArgs.marshaller,
);
const makeAccountHolderKit = prepareAccountHolder(
baggage,
makeRecorderKit,
zcf,
);

const publicFacet = zone.exo('StakeBld', undefined, {
makeStakeBldInvitation() {
return zcf.makeInvitation(
async seat => {
const { give } = seat.getProposal();
trace('makeStakeBldInvitation', give);
// XXX type appears local but it's remote
const account = await E(privateArgs.localchain).createAccount();
const lcaSeatKit = zcf.makeEmptySeatKit();
atomicTransfer(zcf, seat, lcaSeatKit.zcfSeat, give);
seat.exit();
trace('makeStakeBldInvitation tryExit lca userSeat');
await E(lcaSeatKit.userSeat).tryExit();
trace('awaiting payouts');
const payouts = await E(lcaSeatKit.userSeat).getPayouts();
const { holder, invitationMakers } = makeAccountHolderKit(
account,
privateArgs.storageNode,
);
trace('awaiting deposit');
await E(account).deposit(await payouts.In);

return {
publicSubscribers: holder.getPublicTopics(),
invitationMakers,
account: holder,
};
},
'wantStake',
undefined,
M.splitRecord({
give: { In: bldAmountShape },
}),
);
},
});

return { publicFacet };
};
Loading

0 comments on commit 2767bb5

Please sign in to comment.