Skip to content

Commit

Permalink
feat: stub Orchestration API
Browse files Browse the repository at this point in the history
  • Loading branch information
turadg committed Apr 24, 2024
1 parent 54d18d9 commit 1e054ac
Show file tree
Hide file tree
Showing 11 changed files with 800 additions and 17 deletions.
3 changes: 1 addition & 2 deletions packages/boot/test/bootstrapTests/test-vat-orchestration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,7 @@ test('ICA connection can be closed', async t => {
);
t.truthy(account, 'createAccount returns an account');

const res = await EV(account).close();
t.is(res, 'Connection closed');
await EV(account).close();

await t.throwsAsync(EV(account).executeEncodedTx([delegateMsgSuccess]), {
message: 'Connection closed',
Expand Down
1 change: 1 addition & 0 deletions packages/orchestration/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@agoric/network": "^0.1.0",
"@agoric/notifier": "^0.6.2",
"@agoric/store": "^0.9.2",
"@agoric/time": "^0.3.2",
"@agoric/vat-data": "^0.5.2",
"@agoric/vats": "^0.15.1",
"@agoric/zoe": "^0.26.2",
Expand Down
6 changes: 4 additions & 2 deletions packages/orchestration/src/contracts/stakingAccountHolder.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const { Fail } = assert;
* @typedef {{
* topicKit: RecorderKit<StakingAccountNotification>;
* account: ChainAccount;
* chainAddress: ChainAddress;
* chainAddress: string;
* }} State
*/

Expand Down Expand Up @@ -71,7 +71,7 @@ export const prepareStakingAccountHolder = (baggage, makeRecorderKit, zcf) => {
/**
* @param {ChainAccount} account
* @param {StorageNode} storageNode
* @param {ChainAddress} chainAddress
* @param {string} chainAddress
* @returns {State}
*/
(account, storageNode, chainAddress) => {
Expand All @@ -93,6 +93,8 @@ export const prepareStakingAccountHolder = (baggage, makeRecorderKit, zcf) => {
getUpdater() {
return this.state.topicKit.recorder;
},
// TODO move this beneath the Orchestration abstraction,
// to the OrchestrationAccount provided by createAccount()
/**
* _Assumes users has already sent funds to their ICA, until #9193
* @param {string} validatorAddress
Expand Down
102 changes: 102 additions & 0 deletions packages/orchestration/src/contracts/swapExample.contract.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// @ts-check
import { Fail } from '@agoric/assert';
import { AmountMath, AmountShape } from '@agoric/ertp';
import { E, Far } from '@endo/far';
import { M } from '@endo/patterns';
import { makeOrchestrationFacade } from '../facade.js';
import { orcUtils } from '../utils/orc.js';

/**
* @import {Orchestrator, ChainAccount, CosmosValidatorAddress} from '../types.js'
* @import {TimerService} from '@agoric/time';
* @import {ERef} from '@endo/far'
* @import {OrchestrationService} from '../service.js';
* @import {Zone} from '@agoric/zone';
*/

/**
* @param {ZCF} zcf
* @param {{
* orchestrationService: ERef<OrchestrationService>;
* storageNode: ERef<StorageNode>;
* timerService: ERef<TimerService>;
* zone: Zone;
* }} privateArgs
*/
export const start = async (zcf, privateArgs) => {
const { orchestrationService, storageNode, timerService, zone } = privateArgs;

const { orchestrate } = makeOrchestrationFacade({
zone,
timerService,
zcf,
storageNode,
orchestrationService,
});

/** deprecated historical example */
/** @type {OfferHandler} */
const swapAndStakeHandler = orchestrate(
'LSTTia',
{ zcf },
// eslint-disable-next-line no-shadow -- this `zcf` is enclosed in a membrane
async (/** @type {Orchestrator} */ orch, { zcf }, seat, offerArgs) => {
const { give } = seat.getProposal();
!AmountMath.isEmpty(give.USDC.value) || Fail`Must provide USDC.`;

const celestia = await orch.getChain('celestia');
const agoric = await orch.getChain('agoric');

const [celestiaAccount, localAccount] = await Promise.all([
celestia.createAccount(),
agoric.createAccount(),
]);

const tiaAddress = await celestiaAccount.getChainAddress();

// deposit funds from user seat to LocalChainAccount
const seatKit = zcf.makeEmptySeatKit();
zcf.atomicRearrange(harden([[seat, seatKit.zcfSeat, give]]));
// seat.exit() // exit user seat now, or later?
const payment = await E(seatKit.userSeat).getPayout('USDC');
await localAccount.deposit(payment);

// build swap instructions with orcUtils library
const transferMsg = orcUtils.makeOsmosisSwap({
destChain: 'celestia',
destAddress: tiaAddress,
amountIn: give.USDC,
brandOut: offerArgs.staked.brand,
slippage: 0.03,
});

await localAccount
.transferSteps(give.USDC, transferMsg)
.then(_txResult =>
celestiaAccount.delegate(offerArgs.validator, offerArgs.staked),
)
.catch(e => console.error(e));

// XXX close localAccount?
return celestiaAccount; // should be continuing inv since this is an offer?
},
);

const makeSwapAndStakeInvitation = () =>
zcf.makeInvitation(
swapAndStakeHandler,
'Swap for TIA and stake',
undefined,
harden({
give: { USDC: AmountShape },
want: {}, // XXX ChainAccount Ownable?
exit: M.any(),
}),
);

const publicFacet = Far('SwapAndStake Public Facet', {
makeSwapAndStakeInvitation,
});

return harden({ publicFacet });
};
87 changes: 87 additions & 0 deletions packages/orchestration/src/contracts/unbondExample.contract.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// @ts-check
import { Fail } from '@agoric/assert';
import { AmountMath, AmountShape } from '@agoric/ertp';
import { Far } from '@endo/far';
import { M } from '@endo/patterns';
import { makeOrchestrationFacade } from '../facade.js';

/**
* @import {Orchestrator, ChainAccount, CosmosValidatorAddress} from '../types.js'
* @import {TimerService} from '@agoric/time';
* @import {ERef} from '@endo/far'
* @import {OrchestrationService} from '../service.js';
* @import {Zone} from '@agoric/zone';
*/

/**
* @param {ZCF} zcf
* @param {{
* orchestrationService: ERef<OrchestrationService>;
* storageNode: ERef<StorageNode>;
* timerService: ERef<TimerService>;
* zone: Zone;
* }} privateArgs
*/
export const start = async (zcf, privateArgs) => {
const { orchestrationService, storageNode, timerService, zone } = privateArgs;

const { orchestrate } = makeOrchestrationFacade({
zone,
timerService,
zcf,
storageNode,
orchestrationService,
});

/** @type {OfferHandler} */
const unbondAndLiquidStake = orchestrate(
'LSTTia',
{ zcf },
// eslint-disable-next-line no-shadow -- this `zcf` is enclosed in a membrane
async (/** @type {Orchestrator} */ orch, { zcf }, seat, _offerArgs) => {
console.log('zcf within the membrane', zcf);
const { give } = seat.getProposal();
!AmountMath.isEmpty(give.USDC.value) || Fail`Must provide USDC.`;

// We would actually alreaady have the account from the orchestrator
// ??? could these be passed in? It would reduce the size of this handler,
// keeping it focused on long-running operations.
const celestia = await orch.getChain('celestia');
const celestiaAccount = await celestia.createAccount();

const delegations = await celestiaAccount.getDelegations();
const [undelegation] = await celestiaAccount.undelegate(delegations);

// wait for the undelegations to be complete (may take weeks)
await undelegation.completion;

// ??? should this be synchronous? depends on how names are resolved.
const stride = await orch.getChain('stride');
const strideAccount = await stride.createAccount();

// TODO the `TIA` string actually needs to be the Brand from AgoricNames
const tiaAmt = await celestiaAccount.getBalance('TIA');
await celestiaAccount.transfer(tiaAmt, strideAccount.getChainAddress());

await strideAccount.liquidStake(tiaAmt);
},
);

const makeUnbondAndLiquidStakeInvitation = () =>
zcf.makeInvitation(
unbondAndLiquidStake,
'Unbond and liquid stake',
undefined,
harden({
give: { USDC: AmountShape },
want: {}, // XXX ChainAccount Ownable?
exit: M.any(),
}),
);

const publicFacet = Far('SwapAndStake Public Facet', {
makeUnbondAndLiquidStakeInvitation,
});

return harden({ publicFacet });
};
47 changes: 47 additions & 0 deletions packages/orchestration/src/facade.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// @ts-check
/** @file Orchestration service */

/**
* @import {Zone} from '@agoric/zone';
* @import {TimerService} from '@agoric/time';
* @import {OrchestrationService} from './service.js';
* @import {OrchestrationHandlerMaker} from './types.js';
*/

/**
*
* @param {{
* zone: Zone;
* timerService: ERef<TimerService>;
* zcf: ERef<ZCF>;
* storageNode: ERef<StorageNode>;
* orchestrationService: ERef<OrchestrationService>;
* }} powers
*/
export const makeOrchestrationFacade = ({
zone,
timerService,
zcf,
storageNode,
orchestrationService,
}) => {
console.log('makeOrchestrationFacade got', {
zone,
timerService,
zcf,
storageNode,
orchestrationService,
});

return {
/**
* @type {OrchestrationHandlerMaker}
*/
orchestrate(durableName, ctx, fn) {
console.log('orchestrate got', durableName, ctx, fn);
throw new Error('Not yet implemented');
},
};
};
harden(makeOrchestrationFacade);
/** @typedef {ReturnType<typeof makeOrchestrationFacade>} OrchestrationFacade */
44 changes: 36 additions & 8 deletions packages/orchestration/src/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import { makeTracer } from '@agoric/internal';
import '@agoric/network/exported.js';
import { V as E } from '@agoric/vat-data/vow.js';
import { M } from '@endo/patterns';
import { PaymentShape, PurseShape } from '@agoric/ertp';
import { InvitationShape } from '@agoric/zoe/src/typeGuards.js';
import { makeICAConnectionAddress, parseAddress } from './utils/address.js';
import { makeTxPacket, parsePacketAck } from './utils/tx.js';

/**
* @import { AttenuatedNetwork } from './types.js';
* @import { AttenuatedNetwork, ChainAccount, ChainAddress } from './types.js';
* @import { IBCConnectionID } from '@agoric/vats';
* @import { Zone } from '@agoric/base-zone';
* @import { TxBody } from '@agoric/cosmic-proto/cosmos/tx/v1beta1/tx.js';
Expand All @@ -20,9 +22,6 @@ const trace = makeTracer('Orchestration');

/** @import {AnyJson} from '@agoric/cosmic-proto'; */

// TODO improve me
/** @typedef {string} ChainAddress */

/**
* @typedef {object} OrchestrationPowers
* @property {ERef<AttenuatedNetwork>} network
Expand Down Expand Up @@ -59,10 +58,14 @@ export const ChainAccountI = M.interface('ChainAccount', {
getLocalAddress: M.call().returns(M.string()),
getRemoteAddress: M.call().returns(M.string()),
getPort: M.call().returns(M.remotable('Port')),
executeTx: M.call(M.arrayOf(M.record())).returns(M.promise()),
executeEncodedTx: M.call(M.arrayOf(Proto3Shape))
.optional(M.record())
.returns(M.promise()),
close: M.callWhen().returns(M.string()),
close: M.callWhen().returns(M.undefined()),
deposit: M.callWhen(PaymentShape).returns(M.undefined()),
getPurse: M.callWhen().returns(PurseShape),
prepareTransfer: M.callWhen().returns(InvitationShape),
});

export const ConnectionHandlerI = M.interface('ConnectionHandler', {
Expand All @@ -88,7 +91,7 @@ const prepareChainAccount = zone =>
* localAddress: string | undefined;
* requestedRemoteAddress: string;
* remoteAddress: string | undefined;
* accountAddress: ChainAddress | undefined;
* accountAddress: string | undefined;
* }}
*/ (
harden({
Expand All @@ -102,6 +105,9 @@ const prepareChainAccount = zone =>
),
{
account: {
/**
* @returns {string} the address of the account on the chain
*/
getAccountAddress() {
return NonNullish(
this.state.accountAddress,
Expand All @@ -123,7 +129,11 @@ const prepareChainAccount = zone =>
getPort() {
return this.state.port;
},
executeTx() {
throw new Error('not yet implemented');
},
/**
* Submit a transaction on behalf of the remote account for execution on the remote chain.
* @param {AnyJson[]} msgs
* @param {Omit<TxBody, 'messages'>} [opts]
* @returns {Promise<string>} - base64 encoded bytes string. Can be decoded using the corresponding `Msg*Response` object.
Expand All @@ -138,14 +148,33 @@ const prepareChainAccount = zone =>
ack => parsePacketAck(ack),
);
},
/**
* Close the remote account
*/
async close() {
/// XXX what should the behavior be here? and `onClose`?
// - retrieve assets?
// - revoke the port?
const { connection } = this.state;
if (!connection) throw Fail`connection not available`;
await E(connection).close();
return 'Connection closed';
},
async deposit(payment) {
console.log('deposit got', payment);
throw new Error('not yet implemented');
},
/**
* get Purse for a brand to .withdraw() a Payment from the account
* @param {Brand} brand
*/
async getPurse(brand) {
console.log('getPurse got', brand);
throw new Error('not yet implemented');
},

/* transfer account to new holder */
async prepareTransfer() {
throw new Error('not yet implemented');
},
},
connectionHandler: {
Expand Down Expand Up @@ -253,7 +282,6 @@ export const prepareOrchestrationTools = zone => {
harden(prepareOrchestrationTools);

/** @typedef {ReturnType<ReturnType<typeof prepareChainAccount>>} ChainAccountKit */
/** @typedef {ChainAccountKit['account']} ChainAccount */
/** @typedef {ReturnType<typeof prepareOrchestrationTools>} OrchestrationTools */
/** @typedef {ReturnType<OrchestrationTools['makeOrchestration']>} OrchestrationKit */
/** @typedef {OrchestrationKit['public']} OrchestrationService */
Loading

0 comments on commit 1e054ac

Please sign in to comment.