Skip to content

Commit

Permalink
feat(cosmosStakingAccount): publish chain address to vstorage (#9482)
Browse files Browse the repository at this point in the history
refs: #9042 
refs: #9193
refs: #9066

## Description

- **Motivation:** wallet clients need a way to view their address so they can send funds to it
- Updates `stakeAtom.contract.js` to create a vstorage entry for newly created accounts, keyed by `ChainAccount['address']` (e.g. `cosmos1testing1234`)
   - TODO: if we hit `UNPARSABLE_ADDRESS` in parseAddress we should throw, or use a fallback value
- Writes an empty string  to provided storageNode in `exos/cosmosOrchestrationAccount.js`
   - adds TODO for fleshing out vstorage requirements via #9066
  
 - Drive-by fixes:
    - use `AmountArg` consistently in `/exos/cosmosOrchestrationAccount.js`
    - remove `delegatorAddress` from `undelegate(Delegations[])` interface and implementation

### Security Considerations


### Scaling Considerations

This is some experimentation within a contract in order to get experience with what should be pushed down into the platform.

The approach delegates vstorage publishing responsibilities to guest contracts. We might consider posting information for all accounts to a shared global namespace.

### Documentation Considerations


### Testing Considerations

Tests, including ones for `swapExample.contract.js` and `facade.js`, were updated to accommodate these changes. Mocking facilities for `.getAddress()` are light - including in boostrap tests - so using `ChainAddress['address']` might lead to unexpected behavior (accounts overwriting each other in storage) in a testing environment.

### Upgrade Considerations
  • Loading branch information
mergify[bot] committed Jun 17, 2024
2 parents 5375019 + 11aea65 commit 24665a9
Show file tree
Hide file tree
Showing 18 changed files with 351 additions and 95 deletions.
58 changes: 47 additions & 11 deletions packages/boot/test/bootstrapTests/orchestration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,42 @@ test.serial('config', async t => {
}
});

test.serial('stakeOsmo - queries', async t => {
const {
buildProposal,
evalProposal,
runUtils: { EV },
} = t.context;
await evalProposal(
buildProposal('@agoric/builders/scripts/orchestration/init-stakeOsmo.js'),
);

const agoricNames = await EV.vat('bootstrap').consumeItem('agoricNames');
const instance: Instance<typeof startStakeIca> = await EV(agoricNames).lookup(
'instance',
'stakeOsmo',
);
t.truthy(instance, 'stakeOsmo instance is available');

const zoe: ZoeService = await EV.vat('bootstrap').consumeItem('zoe');
const publicFacet = await EV(zoe).getPublicFacet(instance);
t.truthy(publicFacet, 'stakeOsmo publicFacet is available');

const account = await EV(publicFacet).makeAccount();
t.log('account', account);
t.truthy(account, 'makeAccount returns an account on OSMO connection');

const queryRes = await EV(account).getBalance('uatom');
t.deepEqual(queryRes, { value: 0n, denom: 'uatom' });

const queryUnknownDenom = await EV(account).getBalance('some-invalid-denom');
t.deepEqual(
queryUnknownDenom,
{ value: 0n, denom: 'some-invalid-denom' },
'getBalance for unknown denom returns value: 0n',
);
});

test.serial('stakeAtom - repl-style', async t => {
const {
buildProposal,
Expand Down Expand Up @@ -111,19 +147,13 @@ test.serial('stakeAtom - repl-style', async t => {
};
await t.notThrowsAsync(EV(account).delegate(validatorAddress, atomAmount));

const queryRes = await EV(account).getBalance('uatom');
t.deepEqual(queryRes, { value: 0n, denom: 'uatom' });

const queryUnknownDenom = await EV(account).getBalance('some-invalid-denom');
t.deepEqual(
queryUnknownDenom,
{ value: 0n, denom: 'some-invalid-denom' },
'getBalance for unknown denom returns value: 0n',
);
await t.throwsAsync(EV(account).getBalance('uatom'), {
message: 'Queries not available for chain "cosmoshub-4"',
});
});

test.serial('stakeAtom - smart wallet', async t => {
const { agoricNamesRemotes } = t.context;
const { agoricNamesRemotes, readLatest } = t.context;

const wd = await t.context.walletFactoryDriver.provideSmartWallet(
'agoric1testStakAtom',
Expand All @@ -140,12 +170,18 @@ test.serial('stakeAtom - smart wallet', async t => {
});
t.like(wd.getCurrentWalletRecord(), {
offerToPublicSubscriberPaths: [
['request-account', { account: 'published.stakeAtom' }],
[
'request-account',
{
account: 'published.stakeAtom.accounts.cosmos1test',
},
],
],
});
t.like(wd.getLatestUpdateRecord(), {
status: { id: 'request-account', numWantsSatisfied: 1 },
});
t.is(readLatest('published.stakeAtom.accounts.cosmos1test'), '');

const { ATOM } = agoricNamesRemotes.brand;
ATOM || Fail`ATOM missing from agoricNames`;
Expand Down
13 changes: 1 addition & 12 deletions packages/builders/scripts/orchestration/init-stakeAtom.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
import { makeHelpers } from '@agoric/deploy-script-support';

/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').CoreEvalBuilder} */
export const defaultProposalBuilder = async (
{ publishRef, install },
options = {},
) => {
const {
hostConnectionId = 'connection-1',
controllerConnectionId = 'connection-0',
bondDenom = 'uatom',
} = options;
export const defaultProposalBuilder = async ({ publishRef, install }) => {
return harden({
sourceSpec: '@agoric/orchestration/src/proposals/start-stakeAtom.js',
getManifestCall: [
Expand All @@ -20,9 +12,6 @@ export const defaultProposalBuilder = async (
install('@agoric/orchestration/src/examples/stakeIca.contract.js'),
),
},
hostConnectionId,
controllerConnectionId,
bondDenom,
},
],
});
Expand Down
23 changes: 23 additions & 0 deletions packages/builders/scripts/orchestration/init-stakeOsmo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { makeHelpers } from '@agoric/deploy-script-support';

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

export default async (homeP, endowments) => {
const { writeCoreEval } = await makeHelpers(homeP, endowments);
await writeCoreEval('start-stakeOsmo', defaultProposalBuilder);
};
4 changes: 3 additions & 1 deletion packages/orchestration/src/cosmos-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,9 @@ export interface StakingAccountActions {
* The unbonding time is padded by 10 minutes to account for clock skew.
* @param delegations - the delegation to undelegate
*/
undelegate: (delegations: Delegation[]) => Promise<void>;
undelegate: (
delegations: Omit<Delegation, 'delegatorAddress'>[],
) => Promise<void>;

/**
* Withdraw rewards from all validators. The promise settles when the rewards are withdrawn.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export const start = async (zcf, privateArgs, baggage) => {
zone,
chainHub,
makeLocalChainAccountKit,
makeRecorderKit,
...orchPowers,
});

Expand Down
38 changes: 27 additions & 11 deletions packages/orchestration/src/examples/stakeIca.contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
import { makeTracer, StorageNodeShape } from '@agoric/internal';
import { TimerServiceShape } from '@agoric/time';
import { V as E } from '@agoric/vow/vat.js';
import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport';
import {
prepareRecorderKitMakers,
provideAll,
} from '@agoric/zoe/src/contractSupport';
import { InvitationShape } from '@agoric/zoe/src/typeGuards.js';
import { makeDurableZone } from '@agoric/zone/durable.js';
import { M } from '@endo/patterns';
Expand All @@ -24,6 +27,7 @@ export const meta = harden({
hostConnectionId: M.string(),
controllerConnectionId: M.string(),
bondDenom: M.string(),
icqEnabled: M.boolean(),
},
privateArgsShape: {
orchestration: M.remotable('orchestration'),
Expand All @@ -40,6 +44,7 @@ export const privateArgsShape = meta.privateArgsShape;
* hostConnectionId: IBCConnectionID;
* controllerConnectionId: IBCConnectionID;
* bondDenom: string;
* icqEnabled: boolean;
* }} StakeIcaTerms
*/

Expand All @@ -54,12 +59,21 @@ export const privateArgsShape = meta.privateArgsShape;
* @param {Baggage} baggage
*/
export const start = async (zcf, privateArgs, baggage) => {
const { chainId, hostConnectionId, controllerConnectionId, bondDenom } =
zcf.getTerms();
const {
chainId,
hostConnectionId,
controllerConnectionId,
bondDenom,
icqEnabled,
} = zcf.getTerms();
const { orchestration, marshaller, storageNode, timer } = privateArgs;

const zone = makeDurableZone(baggage);

const { accountsStorageNode } = await provideAll(baggage, {
accountsStorageNode: () => E(storageNode).makeChildNode('accounts'),
});

const { makeRecorderKit } = prepareRecorderKitMakers(baggage, marshaller);

const makeCosmosOrchestrationAccount = prepareCosmosOrchestrationAccount(
Expand All @@ -74,17 +88,19 @@ export const start = async (zcf, privateArgs, baggage) => {
hostConnectionId,
controllerConnectionId,
);
// TODO https://github.com/Agoric/agoric-sdk/issues/9326
// Should not fail if host does not have `async-icq` module;
// communicate to OrchestrationAccount that it can't send queries
const icqConnection = await E(orchestration).provideICQConnection(
controllerConnectionId,
);
// TODO permissionless queries https://github.com/Agoric/agoric-sdk/issues/9326
const icqConnection = icqEnabled
? await E(orchestration).provideICQConnection(controllerConnectionId)
: undefined;

const accountAddress = await E(account).getAddress();
trace('account address', accountAddress);
const accountNode = await E(accountsStorageNode).makeChildNode(
accountAddress.address,
);
const holder = makeCosmosOrchestrationAccount(accountAddress, bondDenom, {
account,
storageNode,
storageNode: accountNode,
icqConnection,
timer,
});
Expand Down Expand Up @@ -121,4 +137,4 @@ export const start = async (zcf, privateArgs, baggage) => {
return { publicFacet };
};

/** @typedef {typeof start} StakeAtomSF */
/** @typedef {typeof start} StakeIcaSF */
9 changes: 7 additions & 2 deletions packages/orchestration/src/examples/swapExample.contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { StorageNodeShape } from '@agoric/internal';
import { TimerServiceShape } from '@agoric/time';
import { withdrawFromSeat } from '@agoric/zoe/src/contractSupport/zoeHelpers.js';
import { makeDurableZone } from '@agoric/zone/durable.js';
import { Far } from '@endo/far';
import { E, Far } from '@endo/far';
import { deeplyFulfilled } from '@endo/marshal';
import { M, objectMap } from '@endo/patterns';
import { provideAll } from '@agoric/zoe/src/contractSupport';
import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/recorder.js';
import { makeOrchestrationFacade } from '../facade.js';
import { orcUtils } from '../utils/orc.js';
Expand Down Expand Up @@ -79,16 +80,20 @@ export const start = async (zcf, privateArgs, baggage) => {
timerService,
chainHub,
);
const { accountsStorageNode } = await provideAll(baggage, {
accountsStorageNode: () => E(storageNode).makeChildNode('accounts'),
});

const { orchestrate } = makeOrchestrationFacade({
localchain,
orchestrationService,
storageNode,
storageNode: accountsStorageNode,
timerService,
zcf,
zone,
chainHub,
makeLocalChainAccountKit,
makeRecorderKit,
});

/** deprecated historical example */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export const start = async (zcf, privateArgs, baggage) => {
zone,
chainHub: makeChainHub(agoricNames),
makeLocalChainAccountKit,
makeRecorderKit,
});

/** @type {OfferHandler} */
Expand Down
2 changes: 2 additions & 0 deletions packages/orchestration/src/exos/chainAccountKit.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ export const prepareChainAccountKit = zone =>
this.state.remoteAddress = remoteAddr;
this.state.localAddress = localAddr;
this.state.chainAddress = harden({
// FIXME need a fallback value like icacontroller-1-connection-1 if this fails
// https://github.com/Agoric/agoric-sdk/issues/9066
address: findAddressField(remoteAddr) || UNPARSABLE_CHAIN_ADDRESS,
chainId: this.state.chainId,
addressEncoding: 'bech32',
Expand Down
26 changes: 16 additions & 10 deletions packages/orchestration/src/exos/cosmosOrchestrationAccount.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
MsgUndelegateResponse,
} from '@agoric/cosmic-proto/cosmos/staking/v1beta1/tx.js';
import { Any } from '@agoric/cosmic-proto/google/protobuf/any.js';
import { AmountShape } from '@agoric/ertp';
import { makeTracer } from '@agoric/internal';
import { M } from '@agoric/vat-data';
import { TopicsRecordShape } from '@agoric/zoe/src/contractSupport/index.js';
Expand Down Expand Up @@ -60,7 +59,7 @@ const { Fail } = assert;
* topicKit: RecorderKit<ComosOrchestrationAccountNotification>;
* account: IcaAccount;
* chainAddress: ChainAddress;
* icqConnection: ICQConnection;
* icqConnection: ICQConnection | undefined;
* bondDenom: string;
* timer: Remote<TimerService>;
* }} State
Expand All @@ -75,11 +74,13 @@ export const IcaAccountHolderI = M.interface('IcaAccountHolder', {
holder: M.any(),
}),
getPublicTopics: M.call().returns(TopicsRecordShape),
delegate: M.callWhen(ChainAddressShape, AmountShape).returns(M.undefined()),
delegate: M.callWhen(ChainAddressShape, AmountArgShape).returns(
M.undefined(),
),
redelegate: M.callWhen(
ChainAddressShape,
ChainAddressShape,
AmountShape,
AmountArgShape,
).returns(M.undefined()),
withdrawReward: M.callWhen(ChainAddressShape).returns(
M.arrayOf(ChainAmountShape),
Expand Down Expand Up @@ -123,11 +124,11 @@ export const prepareCosmosOrchestrationAccountKit = (
helper: M.interface('helper', {
owned: M.call().returns(M.remotable()),
getUpdater: M.call().returns(M.remotable()),
amountToCoin: M.call(AmountShape).returns(M.record()),
amountToCoin: M.call(AmountArgShape).returns(M.record()),
}),
holder: IcaAccountHolderI,
invitationMakers: M.interface('invitationMakers', {
Delegate: M.callWhen(ChainAddressShape, AmountShape).returns(
Delegate: M.callWhen(ChainAddressShape, AmountArgShape).returns(
InvitationShape,
),
Redelegate: M.callWhen(
Expand All @@ -149,7 +150,7 @@ export const prepareCosmosOrchestrationAccountKit = (
* @param {object} io
* @param {IcaAccount} io.account
* @param {Remote<StorageNode>} io.storageNode
* @param {ICQConnection} io.icqConnection
* @param {ICQConnection | undefined} io.icqConnection
* @param {Remote<TimerService>} io.timer
* @returns {State}
*/
Expand All @@ -158,6 +159,8 @@ export const prepareCosmosOrchestrationAccountKit = (
// must be the fully synchronous maker because the kit is held in durable state
// @ts-expect-error XXX Patterns
const topicKit = makeRecorderKit(storageNode, PUBLIC_TOPICS.account[1]);
// TODO determine what goes in vstorage https://github.com/Agoric/agoric-sdk/issues/9066
void E(topicKit.recorder).write('');

return { chainAddress, bondDenom, topicKit, ...rest };
},
Expand Down Expand Up @@ -195,7 +198,7 @@ export const prepareCosmosOrchestrationAccountKit = (
invitationMakers: {
/**
* @param {CosmosValidatorAddress} validator
* @param {Amount<'nat'>} amount
* @param {AmountArg} amount
*/
Delegate(validator, amount) {
trace('Delegate', validator, amount);
Expand Down Expand Up @@ -231,7 +234,7 @@ export const prepareCosmosOrchestrationAccountKit = (
return this.facets.holder.withdrawReward(validator);
}, 'WithdrawReward');
},
/** @param {Delegation[]} delegations */
/** @param {Omit<Delegation, 'delegatorAddress'>[]} delegations */
Undelegate(delegations) {
trace('Undelegate', delegations);

Expand Down Expand Up @@ -363,6 +366,9 @@ export const prepareCosmosOrchestrationAccountKit = (
*/
async getBalance(denom) {
const { chainAddress, icqConnection } = this.state;
if (!icqConnection) {
throw Fail`Queries not available for chain ${chainAddress.chainId}`;
}
// TODO #9211 lookup denom from brand
assert.typeof(denom, 'string');

Expand Down Expand Up @@ -401,7 +407,7 @@ export const prepareCosmosOrchestrationAccountKit = (
throw assert.error('Not implemented');
},

/** @param {Delegation[]} delegations */
/** @param {Omit<Delegation, 'delegatorAddress'>[]} delegations */
async undelegate(delegations) {
trace('undelegate', delegations);
const { helper } = this.facets;
Expand Down
Loading

0 comments on commit 24665a9

Please sign in to comment.