Skip to content

Commit

Permalink
feat(cosmos-orch-account): expose .executeEncodedTx (#10341)
Browse files Browse the repository at this point in the history
closes: #10345

## Description
- exposes `.executeEncodedTx` on `CosmosOrchestrationAccount` so developers can send custom messages
- improve `OrchestrationAccount` type

### Security Considerations
N/A

### Scaling Considerations
N/A

### Documentation Considerations
Tests show how to use it and make sense of the response.

### Testing Considerations
Includes unit tests and type tests

### Upgrade Considerations
Library code for an NPM release
  • Loading branch information
mergify[bot] authored Nov 13, 2024
2 parents ff9b882 + 3262f6c commit 9b34be9
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 41 deletions.
66 changes: 40 additions & 26 deletions packages/orchestration/src/cosmos-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
} from '@agoric/cosmic-proto/cosmos/staking/v1beta1/staking.js';
import type { TxBody } from '@agoric/cosmic-proto/cosmos/tx/v1beta1/tx.js';
import type { MsgTransfer } from '@agoric/cosmic-proto/ibc/applications/transfer/v1/tx.js';
import type { FungibleTokenPacketData } from '@agoric/cosmic-proto/ibc/applications/transfer/v2/packet.js';
import type {
State as IBCChannelState,
Order,
Expand All @@ -19,7 +20,11 @@ import type {
} from '@agoric/cosmic-proto/tendermint/abci/types.js';
import type { Brand, Purse, Payment, Amount } from '@agoric/ertp/src/types.js';
import type { Port } from '@agoric/network';
import type { IBCChannelID, IBCConnectionID } from '@agoric/vats';
import type {
IBCChannelID,
IBCConnectionID,
VTransferIBCEvent,
} from '@agoric/vats';
import type {
TargetApp,
TargetRegistration,
Expand Down Expand Up @@ -226,20 +231,10 @@ export interface StakingAccountActions {
}

/**
* Low level object that supports queries and operations for an account on a remote chain.
* Low level methods from IcaAccount that we pass through to CosmosOrchestrationAccount
*/
export interface IcaAccount {
/**
* @returns the address of the account on the remote chain
*/
getAddress: () => ChainAddress;

/**
* Submit a transaction on behalf of the remote account for execution on the remote chain.
* @param msgs - records for the transaction
* @returns acknowledgement string
*/
executeTx: (msgs: TypedJson[]) => Promise<string>;
export interface IcaAccountMethods {
/**
* Submit a transaction on behalf of the remote account for execution on the remote chain.
* @param msgs - records for the transaction
Expand Down Expand Up @@ -267,6 +262,23 @@ export interface IcaAccount {
* @throws {Error} if connection is currently active
*/
reactivate: () => Promise<void>;
}

/**
* Low level object that supports queries and operations for an account on a remote chain.
*/
export interface IcaAccount extends IcaAccountMethods {
/**
* @returns the address of the account on the remote chain
*/
getAddress: () => ChainAddress;

/**
* Submit a transaction on behalf of the remote account for execution on the remote chain.
* @param msgs - records for the transaction
* @returns acknowledgement string
*/
executeTx: (msgs: TypedJson[]) => Promise<string>;
/** @returns the address of the remote channel */
getRemoteAddress: () => RemoteIbcAddress;
/** @returns the address of the local channel */
Expand All @@ -280,16 +292,22 @@ export interface LiquidStakingMethods {
liquidStake: (amount: AmountArg) => Promise<void>;
}

// TODO support StakingAccountQueries
/** Methods supported only on Agoric chain accounts */
export interface LocalAccountMethods {
export interface LocalAccountMethods extends StakingAccountActions {
/** deposit payment (from zoe, for example) to the account */
deposit: (payment: Payment<'nat'>) => Promise<void>;
/** withdraw a Payment from the account */
withdraw: (amount: Amount<'nat'>) => Promise<Payment<'nat'>>;
/**
* Register a handler that receives an event each time ICS-20 transfers are
* sent or received by the underlying account. Each account may be associated
* with at most one handler at a given time.
* sent or received by the underlying account.
*
* Handler includes {@link VTransferIBCEvent} and
* {@link FungibleTokenPacketData} that can be used for application logic.
*
* Each account may be associated with at most one handler at a given time.
*
* Does not grant the handler the ability to intercept a transfer. For a
* blocking handler, aka 'IBC Hooks', leverage `registerActiveTap` from
* `transferMiddleware` directly.
Expand Down Expand Up @@ -320,16 +338,12 @@ export interface IBCMsgTransferOptions {
* @see {OrchestrationAccountI}
*/
export type CosmosChainAccountMethods<CCI extends CosmosChainInfo> =
(CCI extends {
icaEnabled: true;
}
? IcaAccount
: {}) &
CCI extends {
stakingTokens: {};
}
? StakingAccountActions & StakingAccountQueries
: {};
IcaAccountMethods &
(CCI extends {
stakingTokens: {};
}
? StakingAccountActions & StakingAccountQueries
: {});

export type ICQQueryFunction = (
msgs: JsonSafe<RequestQuery>[],
Expand Down
9 changes: 5 additions & 4 deletions packages/orchestration/src/exos/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Exo structure

Last verified 2024-09-06
Last verified 2024-10-30

```mermaid
classDiagram
Expand Down Expand Up @@ -52,9 +52,9 @@ classDiagram
%% In other vats
class Port {
getLocalAddress()
addListener()
connect()
getLocalAddress()
removeListener()
revoke()
}
Expand All @@ -76,9 +76,8 @@ classDiagram
deposit()
executeTx()
getBalance()
withdraw()
executeTx()
monitorTransfers()
withdraw()
}
%% In api consumer vats
Expand Down Expand Up @@ -113,12 +112,14 @@ classDiagram
timer: Timer
topicKit: RecorderKit<OrchestrationAccountNotification>
asContinuingOffer()
deactivate()
delegate()
executeEncodedTx()
getAddress()
getBalance()
getBalances()
getPublicTopics()
reactivate()
redelegate()
send()
sendAll()
Expand Down
15 changes: 13 additions & 2 deletions packages/orchestration/src/exos/cosmos-orchestration-account.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ import {
DelegationShape,
DenomAmountShape,
IBCTransferOptionsShape,
Proto3Shape,
TxBodyOptsShape,
} from '../typeGuards.js';
import { coerceCoin, coerceDenom } from '../utils/amounts.js';
import {
Expand Down Expand Up @@ -143,6 +145,9 @@ export const IcaAccountHolderI = M.interface('IcaAccountHolder', {
...stakingAccountQueriesMethods,
deactivate: M.call().returns(VowShape),
reactivate: M.call().returns(VowShape),
executeEncodedTx: M.call(M.arrayOf(Proto3Shape))
.optional(TxBodyOptsShape)
.returns(VowShape),
});

/** @type {{ [name: string]: [description: string, valueShape: Matcher] }} */
Expand Down Expand Up @@ -955,11 +960,11 @@ export const prepareCosmosOrchestrationAccountKit = (
},
/** @type {HostOf<IcaAccount['deactivate']>} */
deactivate() {
return watch(E(this.facets.helper.owned()).deactivate());
return asVow(() => watch(E(this.facets.helper.owned()).deactivate()));
},
/** @type {HostOf<IcaAccount['reactivate']>} */
reactivate() {
return watch(E(this.facets.helper.owned()).reactivate());
return asVow(() => watch(E(this.facets.helper.owned()).reactivate()));
},
/** @type {HostOf<StakingAccountQueries['getDelegation']>} */
getDelegation(validator) {
Expand Down Expand Up @@ -1093,6 +1098,12 @@ export const prepareCosmosOrchestrationAccountKit = (
return watch(results, this.facets.rewardsQueryWatcher);
});
},
/** @type {HostOf<IcaAccount['executeEncodedTx']>} */
executeEncodedTx(msgs, opts) {
return asVow(() =>
watch(E(this.facets.helper.owned()).executeEncodedTx(msgs, opts)),
);
},
},
},
);
Expand Down
12 changes: 9 additions & 3 deletions packages/orchestration/src/exos/orchestrator.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
* @import {CosmosInterchainService} from './exo-interfaces.js';
* @import {MakeLocalChainFacade} from './local-chain-facade.js';
* @import {MakeRemoteChainFacade} from './remote-chain-facade.js';
* @import {Chain, ChainInfo, IBCConnectionInfo, Orchestrator} from '../types.js';
* @import {Chain, ChainInfo, IBCConnectionInfo, KnownChains, Orchestrator} from '../types.js';
*/

const { Vow$ } = NetworkShape; // TODO #9611
Expand Down Expand Up @@ -148,14 +148,20 @@ const prepareOrchestratorKit = (
if (maybeChain.pending) {
throw Fail`wait until getChain(${q(chainName)}) completes before getDenomInfo(${q(denom)})`;
}
const chain = maybeChain.value;
const chain =
/** @type {HostInterface<Chain<KnownChains[keyof KnownChains]>>} */ (
maybeChain.value
);
chainByName.has(baseName) ||
Fail`use getChain(${q(baseName)}) before getDenomInfo(${q(denom)})`;
const maybeBase = chainByName.get(baseName);
if (maybeBase.pending) {
throw Fail`wait until getChain(${q(baseName)}) completes before getDenomInfo(${q(denom)})`;
}
const base = maybeBase.value;
const base =
/** @type {HostInterface<Chain<KnownChains[keyof KnownChains]>>} */ (
maybeBase.value
);
return harden({ chain, base, brand, baseDenom });
},
/** @type {HostOf<Orchestrator['asAmount']>} */
Expand Down
7 changes: 2 additions & 5 deletions packages/orchestration/src/orchestration-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@
import type { Amount, Brand, NatAmount } from '@agoric/ertp/src/types.js';
import type { CurrentWalletRecord } from '@agoric/smart-wallet/src/smartWallet.js';
import type { Timestamp } from '@agoric/time';
import type {
LocalChainAccount,
QueryManyFn,
} from '@agoric/vats/src/localchain.js';
import type { QueryManyFn } from '@agoric/vats/src/localchain.js';
import type { ResolvedPublicTopic } from '@agoric/zoe/src/contractSupport/topics.js';
import type { Passable } from '@endo/marshal';
import type {
Expand Down Expand Up @@ -76,7 +73,7 @@ export type ChainAddress = {
export type OrchestrationAccount<CI extends ChainInfo> = OrchestrationAccountI &
(CI extends CosmosChainInfo
? CI['chainId'] extends `agoric${string}`
? CosmosChainAccountMethods<CI> & LocalAccountMethods
? LocalAccountMethods
: CosmosChainAccountMethods<CI>
: {});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ import {
QueryDelegationRewardsResponse,
QueryDelegationTotalRewardsResponse,
} from '@agoric/cosmic-proto/cosmos/distribution/v1beta1/query.js';
import { Any } from '@agoric/cosmic-proto/google/protobuf/any.js';
import {
MsgDelegate,
MsgDelegateResponse,
} from '@agoric/cosmic-proto/cosmos/staking/v1beta1/tx.js';
import { decodeBase64 } from '@endo/base64';
import { commonSetup } from '../supports.js';
import type {
AmountArg,
Expand All @@ -48,6 +54,7 @@ import {
parseOutgoingTxPacket,
} from '../../tools/ibc-mocks.js';
import type { CosmosValidatorAddress } from '../../src/cosmos-api.js';
import { protoMsgMocks } from '../ibc-mocks.js';

type TestContext = Awaited<ReturnType<typeof commonSetup>>;

Expand Down Expand Up @@ -816,3 +823,38 @@ test('not yet implemented', async t => {
message: 'Not Implemented. Try using withdrawReward.',
});
});

test('executeEncodedTx', async t => {
const makeTestCOAKit = prepareMakeTestCOAKit(t, t.context);
const account = await makeTestCOAKit();

const delegateMsgSuccess = Any.toJSON(
MsgDelegate.toProtoMsg({
delegatorAddress: 'cosmos1test',
validatorAddress: 'cosmosvaloper1test',
amount: { denom: 'uatom', amount: '10' },
}),
);

const res = await E(account).executeEncodedTx([delegateMsgSuccess]);
t.is(
res,
'Ei0KKy9jb3Ntb3Muc3Rha2luZy52MWJldGExLk1zZ0RlbGVnYXRlUmVzcG9uc2U=', // cosmos.staking.v1beta1.MsgDelegateResponse
'delegateMsgSuccess',
);
const decodedRes = MsgDelegateResponse.decode(decodeBase64(res));
t.deepEqual(decodedRes, {}, 'MsgDelegate returns MsgDelegateResponse');

t.context.mocks.ibcBridge.addMockAck(
// Delegate 100 ubld from cosmos1test to cosmosvaloper1test observed in console, timeoutHeight: 6n
'eyJ0eXBlIjoxLCJkYXRhIjoiQ2xVS0l5OWpiM050YjNNdWMzUmhhMmx1Wnk1Mk1XSmxkR0V4TGsxelowUmxiR1ZuWVhSbEVpNEtDMk52YzIxdmN6RjBaWE4wRWhKamIzTnRiM04yWVd4dmNHVnlNWFJsYzNRYUN3b0ZkV0YwYjIwU0FqRXdHQVk9IiwibWVtbyI6IiJ9',
protoMsgMocks.delegate.ack,
);
t.is(
await E(account).executeEncodedTx([delegateMsgSuccess], {
timeoutHeight: 6n,
}),
'Ei0KKy9jb3Ntb3Muc3Rha2luZy52MWJldGExLk1zZ0RlbGVnYXRlUmVzcG9uc2U=', // cosmos.staking.v1beta1.MsgDelegateResponse
'delegateMsgSuccess',
);
});
55 changes: 54 additions & 1 deletion packages/orchestration/test/types.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/

import type { HostInterface, HostOf } from '@agoric/async-flow';
import { type JsonSafe, typedJson } from '@agoric/cosmic-proto';
import { type AnyJson, type JsonSafe, typedJson } from '@agoric/cosmic-proto';
import type {
QueryAllBalancesResponse,
QueryBalanceResponse,
Expand All @@ -14,10 +14,16 @@ import type { Vow, VowTools } from '@agoric/vow';
import type { ResolvedPublicTopic } from '@agoric/zoe/src/contractSupport/topics.js';
import type { Passable } from '@endo/marshal';
import { expectAssignable, expectNotType, expectType } from 'tsd';
import type { TxBody } from '@agoric/cosmic-proto/cosmos/tx/v1beta1/tx.js';
import type {
TargetApp,
TargetRegistration,
} from '@agoric/vats/src/bridge-target.js';
import { prepareCosmosOrchestrationAccount } from '../src/exos/cosmos-orchestration-account.js';
import type { LocalOrchestrationAccountKit } from '../src/exos/local-orchestration-account.js';
import type { OrchestrationFacade } from '../src/facade.js';
import type {
AmountArg,
Chain,
ChainAddress,
ChainInfo,
Expand Down Expand Up @@ -267,3 +273,50 @@ expectNotType<CosmosValidatorAddress>(chainAddr);
expectAssignable<Passable>(addr as CosmosValidatorAddress);
expectAssignable<Passable>(denomAmount as DenomAmount);
}

// Test LocalAccountMethods
{
type ChainFacade = Chain<CosmosChainInfo & { chainId: 'agoric-3' }>;
const remoteChain: ChainFacade = null as any;

const account = await remoteChain.makeAccount();

// Verify monitorTransfers is available
expectType<(tap: TargetApp) => Promise<TargetRegistration>>(
account.monitorTransfers,
);

// Verify StakingAccountActions are available (StakingAccountQueries not yet supported)
expectType<
(validator: CosmosValidatorAddress, amount: AmountArg) => Promise<void>
>(account.delegate);

// @ts-expect-error executeEncodedTx not available on localAccount
expectType<() => Promise<string>>(account.executeEncodedTx);
}

// Test CosmosChainAccountMethods
{
type ChainFacade = Chain<
CosmosChainInfo & {
chainId: 'cosmoshub-4';
stakingTokens: [{ denom: 'uatom' }];
}
>;
const remoteChain: ChainFacade = null as any;

const account = await remoteChain.makeAccount();

// Verify executeEncodedTx is available
expectType<
(
msgs: AnyJson[],
opts?: Partial<Omit<TxBody, 'messages'>>,
) => Promise<string>
>(account.executeEncodedTx);

// Verify delegate is available via stakingTokens parameter
expectType<
(validator: CosmosValidatorAddress, amount: AmountArg) => Promise<void>
>(account.delegate);
}

0 comments on commit 9b34be9

Please sign in to comment.