Skip to content

Commit

Permalink
feat(localchain): add transfer method to LocalChainAccountKit holder
Browse files Browse the repository at this point in the history
- refs #9193
  • Loading branch information
0xpatrickdev committed May 17, 2024
1 parent 4286fe7 commit 9a89435
Show file tree
Hide file tree
Showing 11 changed files with 418 additions and 30 deletions.
7 changes: 7 additions & 0 deletions packages/cosmic-proto/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import type {
} from './codegen/cosmos/staking/v1beta1/tx.js';
import { RequestQuery } from './codegen/tendermint/abci/types.js';
import type { Any } from './codegen/google/protobuf/any.js';
import {
MsgTransfer,
MsgTransferResponse,
} from './codegen/ibc/applications/transfer/v1/tx.js';

/**
* The result of Any.toJSON(). The type in cosms-types says it returns
Expand All @@ -28,13 +32,16 @@ export type Proto3Shape = {
'/cosmos.bank.v1beta1.QueryAllBalancesResponse': QueryAllBalancesResponse;
'/cosmos.staking.v1beta1.MsgDelegate': MsgDelegate;
'/cosmos.staking.v1beta1.MsgDelegateResponse': MsgDelegateResponse;
'/ibc.applications.transfer.v1.MsgTransfer': MsgTransfer;
'/ibc.applications.transfer.v1.MsgTransferResponse': MsgTransferResponse;
};

// Often s/Request$/Response/ but not always
type ResponseMap = {
'/cosmos.bank.v1beta1.MsgSend': '/cosmos.bank.v1beta1.MsgSendResponse';
'/cosmos.bank.v1beta1.QueryAllBalancesRequest': '/cosmos.bank.v1beta1.QueryAllBalancesResponse';
'/cosmos.staking.v1beta1.MsgDelegate': '/cosmos.staking.v1beta1.MsgDelegateResponse';
'/ibc.applications.transfer.v1.MsgTransfer': '/ibc.applications.transfer.v1.MsgTransferResponse';
};

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/orchestration/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
"@endo/patterns": "^1.4.0"
},
"devDependencies": {
"@agoric/swingset-liveslots": "^0.10.2",
"@agoric/swingset-vat": "^0.32.2",
"@cosmjs/amino": "^0.32.3",
"@cosmjs/proto-signing": "^0.32.3",
"@endo/ses-ava": "^1.2.2",
Expand Down
45 changes: 31 additions & 14 deletions packages/orchestration/src/cosmos-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import type {
LocalIbcAddress,
RemoteIbcAddress,
} from '@agoric/vats/tools/ibc-utils.js';
import { MsgTransfer } from '@agoric/cosmic-proto/ibc/applications/transfer/v1/tx.js';
import { IBCChannelID } from '@agoric/vats';
import { MapStore } from '@agoric/store';
import type { AmountArg, ChainAddress, DenomAmount } from './types.js';

/** A helper type for type extensions. */
Expand All @@ -29,25 +32,33 @@ export type CosmosValidatorAddress = ChainAddress & {
addressEncoding: 'bech32';
};

export type IBCConnectionInfo = {
id: string; // e.g. connection-0
client_id: string; // '07-tendermint-0'
state: 'OPEN' | 'TRYOPEN' | 'INIT' | 'CLOSED';
counterparty: {
client_id: string;
connection_id: string;
prefix: {
key_prefix: string;
};
};
versions: { identifier: string; features: string[] }[];
delay_period: bigint;
transferChannel: {
portId: string;
channelId: IBCChannelID;
counterPartyPortId: string;
counterPartyChannelId: IBCChannelID;
};
};

/**
* Info for a Cosmos-based chain.
*/
export type CosmosChainInfo = {
chainId: string;
ibcConnectionInfo: {
id: string; // e.g. connection-0
client_id: string; // '07-tendermint-0'
state: 'OPEN' | 'TRYOPEN' | 'INIT' | 'CLOSED';
counterparty: {
client_id: string;
connection_id: string;
prefix: {
key_prefix: string;
};
};
versions: { identifier: string; features: string[] }[];
delay_period: bigint;
};
connections: MapStore<string, IBCConnectionInfo>; // chainId or wellKnownName
icaEnabled: boolean;
icqEnabled: boolean;
pfmEnabled: boolean;
Expand Down Expand Up @@ -197,3 +208,9 @@ export interface IcaAccount {
export type LiquidStakingMethods = {
liquidStake: (amount: AmountArg) => Promise<void>;
};

export type IBCMsgTransferOptions = {
timeoutHeight?: MsgTransfer['timeoutHeight'];
timeoutTimestamp?: MsgTransfer['timeoutTimestamp'];
memo?: string;
};
23 changes: 20 additions & 3 deletions packages/orchestration/src/examples/stakeBld.contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import { E } from '@endo/far';
import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/recorder.js';
import { atomicTransfer } from '@agoric/zoe/src/contractSupport/atomicTransfer.js';
import { prepareLocalchainAccountKit } from '../exos/localchainAccountKit.js';
import { prepareMockChainInfo } from '../utils/mockChainInfo.js';

/**
* @import {TimerBrand, TimerService} from '@agoric/time';
* @import {CosmosChainInfo} from '@agoric/orchestration';
*/

const trace = makeTracer('StakeBld');

Expand All @@ -20,6 +26,8 @@ const trace = makeTracer('StakeBld');
* localchain: import('@agoric/vats/src/localchain.js').LocalChain;
* marshaller: Marshaller;
* storageNode: StorageNode;
* chainTimerBrand: TimerBrand;
* chainTimerService: TimerService;
* }} privateArgs
* @param {import("@agoric/vat-data").Baggage} baggage
*/
Expand All @@ -40,6 +48,10 @@ export const start = async (zcf, privateArgs, baggage) => {
zcf,
);

// Mocked until #8879
// Would expect this to be instantiated elsewhere, and a reference passed in
const agoricChainInfo = prepareMockChainInfo(zone);

const publicFacet = zone.exo('StakeBld', undefined, {
makeStakeBldInvitation() {
return zcf.makeInvitation(
Expand All @@ -48,17 +60,22 @@ export const start = async (zcf, privateArgs, baggage) => {
trace('makeStakeBldInvitation', give);
// XXX type appears local but it's remote
const account = await E(privateArgs.localchain).makeAccount();
const address = await E(account).getAddress();
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 } = makeLocalchainAccountKit(
const { holder, invitationMakers } = makeLocalchainAccountKit({
account,
privateArgs.storageNode,
);
address,
storageNode: privateArgs.storageNode,
timer: privateArgs.chainTimerService,
timerBrand: privateArgs.chainTimerBrand,
agoricChainInfo,
});
trace('awaiting deposit');
await E(account).deposit(await payouts.In);

Expand Down
124 changes: 117 additions & 7 deletions packages/orchestration/src/exos/localchainAccountKit.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,33 @@
// @ts-checck

/** @file Use-object for the owner of a localchain account */
import { typedJson } from '@agoric/cosmic-proto/vatsafe';
import { AmountShape } from '@agoric/ertp';
import { AmountShape, PaymentShape } from '@agoric/ertp';
import { makeTracer } from '@agoric/internal';
import { UnguardedHelperI } from '@agoric/internal/src/typeGuards.js';
import { TimeMath } from '@agoric/time';
import { M, prepareExoClassKit } from '@agoric/vat-data';
import { TopicsRecordShape } from '@agoric/zoe/src/contractSupport/index.js';
import { E } from '@endo/far';
import { matches } from '@endo/patterns';
import {
NANOSECONDS_PER_SECOND,
SECONDS_PER_MINUTE,
} from '../utils/constants.js';
import {
ChainAddressShape,
ChainAmountShape,
IBCTransferOptionsShape,
} from '../typeGuards.js';

/**
* @import {LocalChainAccount} from '@agoric/vats/src/localchain.js';
* @import {RelativeTimeRecord, TimerBrand, TimerService} from '@agoric/time';
* @import {AmountArg, ChainAddress, IBCMsgTransferOptions, CosmosChainInfo} from '@agoric/orchestration';
*/

// partial until #8879
/** @typedef {Pick<CosmosChainInfo, 'connections'>} AgoricChainInfo */

const trace = makeTracer('LCAH');

Expand All @@ -18,7 +40,11 @@ const { Fail } = assert;
/**
* @typedef {{
* topicKit: import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit<LocalChainAccountNotification>;
* account: import('@agoric/vats/src/localchain.js').LocalChainAccount | null;
* account: LocalChainAccount | null;
* address: ChainAddress['address'];
* timer: TimerService;
* timerBrand: TimerBrand;
* agoricChainInfo: AgoricChainInfo;
* }} State
*/

Expand All @@ -27,6 +53,11 @@ const HolderI = M.interface('holder', {
makeDelegateInvitation: M.call(M.string(), AmountShape).returns(M.promise()),
makeCloseAccountInvitation: M.call().returns(M.promise()),
makeTransferAccountInvitation: M.call().returns(M.promise()),
deposit: M.callWhen(PaymentShape).returns(AmountShape),
withdraw: M.callWhen(AmountShape).returns(PaymentShape),
transfer: M.call(ChainAmountShape, ChainAddressShape)
.optional(IBCTransferOptionsShape)
.returns(M.promise()),
});

/** @type {{ [name: string]: [description: string, valueShape: Pattern] }} */
Expand Down Expand Up @@ -54,16 +85,21 @@ export const prepareLocalchainAccountKit = (baggage, makeRecorderKit, zcf) => {
}),
},
/**
* @param {import('@agoric/vats/src/localchain.js').LocalChainAccount} account
* @param {StorageNode} storageNode
* @param {object} initState
* @param {LocalChainAccount} initState.account
* @param {ChainAddress['address']} initState.address
* @param {StorageNode} initState.storageNode
* @param {TimerService} initState.timer
* @param {TimerBrand} initState.timerBrand
* @param {AgoricChainInfo} initState.agoricChainInfo
* @returns {State}
*/
(account, storageNode) => {
({ account, address, storageNode, timer, timerBrand, agoricChainInfo }) => {
// 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]);

return { account, topicKit };
return { account, address, topicKit, timer, timerBrand, agoricChainInfo };
},
{
helper: {
Expand All @@ -78,6 +114,27 @@ export const prepareLocalchainAccountKit = (baggage, makeRecorderKit, zcf) => {
getUpdater() {
return this.state.topicKit.recorder;
},
/**
* Takes the current time from ChainTimerService and adds a relative
* time to determine a timeout timestamp in nanoseconds.
* Useful for TimeoutTimestamp in ibc MsgTransfer.
* @param {RelativeTimeRecord} [relativeTime] defaults to 5 minutes
* @returns {Promise<bigint>} Timeout timestamp in absolute nanoseconds since unix epoch
*/
async getTimeoutTimestamp(relativeTime) {
const { timer, timerBrand } = this.state;
const currentTime = await E(timer).getCurrentTimestamp();
return (
TimeMath.addAbsRel(
currentTime,
relativeTime ||
TimeMath.coerceRelativeTimeRecord(
SECONDS_PER_MINUTE * 5n,
timerBrand,
),
).absValue * NANOSECONDS_PER_SECOND
);
},
},
invitationMakers: {
Delegate(validatorAddress, amount) {
Expand Down Expand Up @@ -126,7 +183,7 @@ export const prepareLocalchainAccountKit = (baggage, makeRecorderKit, zcf) => {
trace('lca', lca);
const delegatorAddress = await E(lca).getAddress();
trace('delegatorAddress', delegatorAddress);
const result = await E(lca).executeTx([
const [result] = await E(lca).executeTx([
typedJson('/cosmos.staking.v1beta1.MsgDelegate', {
amount,
validatorAddress,
Expand All @@ -147,6 +204,59 @@ export const prepareLocalchainAccountKit = (baggage, makeRecorderKit, zcf) => {
makeTransferAccountInvitation() {
throw Error('not yet implemented');
},
/** @type {LocalChainAccount['deposit']} */
deposit(payment) {
return E(this.facets.helper.owned()).deposit(payment);
},
/** @type {LocalChainAccount['withdraw']} */
withdraw(amount) {
return E(this.facets.helper.owned()).withdraw(amount);
},
/**
* @param {AmountArg} amount
* @param {ChainAddress} destination
* @param {IBCMsgTransferOptions} [opts] if either timeoutHeight or timeoutTimestamp are not supplied, a default timeoutTimestamp will be set for 5 minutes in the future
* @returns {Promise<void>}
*/
async transfer(amount, destination, opts) {
trace('Transferring funds from LCA over IBC');
// TODO #9211 lookup denom from brand
matches(amount, AmountShape) || Fail`ERTP Amounts not yet supported`;

// TODO #8879 chainInfo and #9063 well-known chains
const { transferChannel } =
this.state.agoricChainInfo.connections.get(destination.chainId);

await null;
// set a `timeoutTimestamp` if caller does not supply either `timeoutHeight` or `timeoutTimestamp`
// TODO #9324 what's a reasonable default? currently 5 minutes
const timeoutTimestamp =
opts?.timeoutTimestamp ??
(opts?.timeoutHeight
? 0n
: await this.facets.helper.getTimeoutTimestamp());

const [result] = await E(this.facets.helper.owned()).executeTx([
typedJson('/ibc.applications.transfer.v1.MsgTransfer', {
sourcePort: transferChannel.portId,
sourceChannel: transferChannel.channelId,
token: {
amount: String(amount.value),
// @ts-expect-error AmountArg already narrowed to DenomAmount
denom: amount.denom,
},
sender: this.state.address,
receiver: destination.address,
timeoutHeight: opts?.timeoutHeight ?? {
revisionHeight: 0n,
revisionNumber: 0n,
},
timeoutTimestamp,
memo: opts?.memo ?? '',
}),
]);
void trace('MsgTransfer result', result);
},
},
},
);
Expand Down
10 changes: 5 additions & 5 deletions packages/orchestration/src/orchestration-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type {
} from '@agoric/ertp/exported.js';
import type { LocalChainAccount } from '@agoric/vats/src/localchain.js';
import type { Timestamp } from '@agoric/time';
import type { KnownChains } from './types.js';
import type { IBCMsgTransferOptions, KnownChains } from './types.js';

/**
* A denom that designates a path to a token type on some blockchain.
Expand Down Expand Up @@ -145,17 +145,17 @@ export interface OrchestrationAccountI {
/**
* Transfer an amount to another account, typically on another chain.
* The promise settles when the transfer is complete.
* @param amount - the amount to transfer.
* @param destination - the account to transfer the amount to.
* @param memo - an optional memo to include with the transfer, which could drive custom PFM behavior
* @param {AmountArg} amount - the amount to transfer.
* @param {ChainAddress} destination - the account to transfer the amount to.
* @param {IBCMsgTransferOptions} [opts] - an optional memo to include with the transfer, which could drive custom PFM behavior, and timeout parameters
* @returns void
*
* TODO document the mapping from the address to the destination chain.
*/
transfer: (
amount: AmountArg,
destination: ChainAddress,
memo?: string,
opts?: IBCMsgTransferOptions,
) => Promise<void>;

/**
Expand Down
Loading

0 comments on commit 9a89435

Please sign in to comment.