Skip to content

Commit

Permalink
chore: add deps for auto-stake-it
Browse files Browse the repository at this point in the history
  • Loading branch information
amessbee committed Nov 19, 2024
1 parent 47fd183 commit 7199164
Show file tree
Hide file tree
Showing 42 changed files with 9,636 additions and 17 deletions.
22 changes: 13 additions & 9 deletions contract/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@agoric/vat-data": "^0.5.3-u17.1",
"@agoric/vow": "^0.2.0-u17.1",
"@agoric/zone": "^0.3.0-u17.1",
"@ava/typescript": "^5.0.0",
"@cosmjs/proto-signing": "^0.32.3",
"@endo/eslint-plugin": "^2.2.0",
"@endo/nat": "^5.0.9",
Expand All @@ -43,7 +44,7 @@
"@rollup/plugin-replace": "^5.0.5",
"@rollup/plugin-typescript": "^11.1.6",
"@types/fs-extra": "^11",
"@types/node": "^20.11.13",
"@types/node": "^22.9.0",
"@typescript-eslint/eslint-plugin": "^7.0.2",
"@typescript-eslint/parser": "^7.15.0",
"agoric": "^0.22.0-u17.1",
Expand All @@ -64,9 +65,10 @@
"prettier-plugin-jsdoc": "^1.0.0",
"rollup": "^4.18.0",
"starshipjs": "^2.4.0",
"ts-node": "^10.9.2",
"tsimp": "^2.0.10",
"type-coverage": "^2.26.3",
"typescript": "^5.3.3",
"typescript": "^5.6.3",
"typescript-eslint": "^7.18.0"
},
"dependencies": {
Expand All @@ -90,15 +92,17 @@
"ts": "module"
},
"files": [
"test/**/test-*.*",
"test/**/*.test.*",
"!test/orca-multichain.test.js"
"test/**/*.test.*"
],
"nodeArguments": [
"--loader=ts-blank-space/register",
"--no-warnings"
],
"require": [
"@endo/init/debug.js"
],
"timeout": "20m",
"workerThreads": false,
"match": [
"!test/*multichain*"
]
"workerThreads": false
},
"keywords": [],
"repository": {
Expand Down
154 changes: 154 additions & 0 deletions contract/src/auto-stake-it-tap-kit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { M, mustMatch } from '@endo/patterns';
import { E } from '@endo/far';
import { VowShape } from '@agoric/vow';
import { makeTracer } from '@agoric/internal';
import { atob } from '@endo/base64';
import { ChainAddressShape } from '../typeGuards.js';

const trace = makeTracer('AutoStakeItTap');

/**
* @import {IBCChannelID, VTransferIBCEvent} from '@agoric/vats';
* @import {VowTools} from '@agoric/vow';
* @import {Zone} from '@agoric/zone';
* @import {TargetApp} from '@agoric/vats/src/bridge-target.js';
* @import {ChainAddress, CosmosValidatorAddress, Denom, OrchestrationAccount, StakingAccountActions} from '@agoric/orchestration';
* @import {FungibleTokenPacketData} from '@agoric/cosmic-proto/ibc/applications/transfer/v2/packet.js';
* @import {TypedPattern} from '@agoric/internal';
*/

/**
* @typedef {{
* stakingAccount: ERef<OrchestrationAccount<any> & StakingAccountActions>;
* localAccount: ERef<OrchestrationAccount<{ chainId: 'agoric' }>>;
* validator: CosmosValidatorAddress;
* localChainAddress: ChainAddress;
* remoteChainAddress: ChainAddress;
* sourceChannel: IBCChannelID;
* remoteDenom: Denom;
* localDenom: Denom;
* }} StakingTapState
*/

/** @type {TypedPattern<StakingTapState>} */
const StakingTapStateShape = {
stakingAccount: M.remotable('CosmosOrchestrationAccount'),
localAccount: M.remotable('LocalOrchestrationAccount'),
validator: ChainAddressShape,
localChainAddress: ChainAddressShape,
remoteChainAddress: ChainAddressShape,
sourceChannel: M.string(),
remoteDenom: M.string(),
localDenom: M.string(),
};
harden(StakingTapStateShape);

/**
* @param {Zone} zone
* @param {VowTools} vowTools
*/
const prepareStakingTapKit = (zone, { watch }) => {
return zone.exoClassKit(
'StakingTapKit',
{
tap: M.interface('AutoStakeItTap', {
receiveUpcall: M.call(M.record()).returns(
M.or(VowShape, M.undefined()),
),
}),
transferWatcher: M.interface('TransferWatcher', {
onFulfilled: M.call(M.undefined())
.optional(M.bigint())
.returns(VowShape),
}),
},
/** @param {StakingTapState} initialState */
initialState => {
mustMatch(initialState, StakingTapStateShape);
return harden(initialState);
},
{
tap: {
/**
* Transfers from localAccount to stakingAccount, then delegates from
* the stakingAccount to `validator` if the expected token (remoteDenom)
* is received.
*
* @param {VTransferIBCEvent} event
*/
receiveUpcall(event) {
trace('receiveUpcall', event);

// ignore packets from unknown channels
if (event.packet.source_channel !== this.state.sourceChannel) {
return;
}

const tx = /** @type {FungibleTokenPacketData} */ (
JSON.parse(atob(event.packet.data))
);
trace('receiveUpcall packet data', tx);

const { remoteDenom, localChainAddress } = this.state;
// ignore outgoing transfers
if (tx.receiver !== localChainAddress.value) {
return;
}
// only interested in transfers of `remoteDenom`
if (tx.denom !== remoteDenom) {
return;
}

const { localAccount, localDenom, remoteChainAddress } = this.state;
return watch(
E(localAccount).transfer(remoteChainAddress, {
denom: localDenom,
value: BigInt(tx.amount),
}),
this.facets.transferWatcher,
BigInt(tx.amount),
);
},
},
transferWatcher: {
/**
* @param {void} _result
* @param {bigint} value the qty of uatom to delegate
*/
onFulfilled(_result, value) {
const { stakingAccount, validator, remoteDenom } = this.state;
return watch(
E(stakingAccount).delegate(validator, {
denom: remoteDenom,
value,
}),
);
},
},
},
);
};

/**
* Provides a {@link TargetApp} that reacts to an incoming IBC transfer by:
*
* 1. transferring the funds to the staking account specified at initialization
* 2. delegating the funds to the validator specified at initialization
*
* XXX consider a facet with a method for changing the validator
*
* XXX consider logic for multiple stakingAccounts + denoms
*
* @param {Zone} zone
* @param {VowTools} vowTools
* @returns {(
* ...args: Parameters<ReturnType<typeof prepareStakingTapKit>>
* ) => ReturnType<ReturnType<typeof prepareStakingTapKit>>['tap']}
*/
export const prepareStakingTap = (zone, vowTools) => {
const makeKit = prepareStakingTapKit(zone, vowTools);
return (...args) => makeKit(...args).tap;
};

/** @typedef {ReturnType<typeof prepareStakingTap>} MakeStakingTap */
/** @typedef {ReturnType<MakeStakingTap>} StakingTap */
76 changes: 76 additions & 0 deletions contract/src/auto-stake-it.contract.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import {
EmptyProposalShape,
InvitationShape,
} from '@agoric/zoe/src/typeGuards.js';
import { M } from '@endo/patterns';
import { prepareChainHubAdmin } from './exos/chain-hub-admin.js';
import { preparePortfolioHolder } from './exos/portfolio-holder-kit.js';
import { withOrchestration } from './utils/start-helper.js';
import { prepareStakingTap } from './auto-stake-it-tap-kit.js';
import * as flows from './auto-stake-it.flows.js';

/**
* @import {Zone} from '@agoric/zone';
* @import {OrchestrationPowers, OrchestrationTools} from './utils/start-helper.js';
*/

/**
* AutoStakeIt allows users to to create an auto-forwarding address that
* transfers and stakes tokens on a remote chain when received.
*
* To be wrapped with `withOrchestration`.
*
* @param {ZCF} zcf
* @param {OrchestrationPowers & {
* marshaller: Marshaller;
* }} _privateArgs
* @param {Zone} zone
* @param {OrchestrationTools} tools
*/
const contract = async (
zcf,
_privateArgs,
zone,
{ chainHub, orchestrateAll, vowTools },
) => {
const makeStakingTap = prepareStakingTap(
zone.subZone('stakingTap'),
vowTools,
);
const makePortfolioHolder = preparePortfolioHolder(
zone.subZone('portfolio'),
vowTools,
);

const { makeAccounts } = orchestrateAll(flows, {
makeStakingTap,
makePortfolioHolder,
chainHub,
});

const publicFacet = zone.exo(
'AutoStakeIt Public Facet',
M.interface('AutoStakeIt Public Facet', {
makeAccountsInvitation: M.callWhen().returns(InvitationShape),
}),
{
makeAccountsInvitation() {
return zcf.makeInvitation(
makeAccounts,
'Make Accounts',
undefined,
EmptyProposalShape,
);
},
},
);

const creatorFacet = prepareChainHubAdmin(zone, chainHub);

return { publicFacet, creatorFacet };
};

export const start = withOrchestration(contract);
harden(start);

/** @typedef {typeof start} AutoStakeItSF */
102 changes: 102 additions & 0 deletions contract/src/auto-stake-it.flows.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { Fail } from '@endo/errors';
import { denomHash } from '../utils/denomHash.js';

/**
* @import {ResolvedPublicTopic} from '@agoric/zoe/src/contractSupport/topics.js';
* @import {GuestInterface} from '@agoric/async-flow';
* @import {CosmosValidatorAddress, Orchestrator, CosmosInterchainService, Denom, OrchestrationAccount, StakingAccountActions, OrchestrationFlow} from '@agoric/orchestration';
* @import {MakeStakingTap} from './auto-stake-it-tap-kit.js';
* @import {MakePortfolioHolder} from '../exos/portfolio-holder-kit.js';
* @import {ChainHub} from '../exos/chain-hub.js';
*/

/**
* @satisfies {OrchestrationFlow}
* @param {Orchestrator} orch
* @param {{
* makeStakingTap: MakeStakingTap;
* makePortfolioHolder: MakePortfolioHolder;
* chainHub: GuestInterface<ChainHub>;
* }} ctx
* @param {ZCFSeat} seat
* @param {{
* chainName: string;
* validator: CosmosValidatorAddress;
* }} offerArgs
*/
export const makeAccounts = async (
orch,
{ makeStakingTap, makePortfolioHolder, chainHub },
seat,
{ chainName, validator },
) => {
seat.exit(); // no funds exchanged
const [agoric, remoteChain] = await Promise.all([
orch.getChain('agoric'),
orch.getChain(chainName),
]);
const { chainId, stakingTokens } = await remoteChain.getChainInfo();
const remoteDenom = stakingTokens[0].denom;
remoteDenom ||
Fail`${chainId || chainName} does not have stakingTokens in config`;
if (chainId !== validator.chainId) {
Fail`validator chainId ${validator.chainId} does not match remote chainId ${chainId}`;
}
const [localAccount, stakingAccount] = await Promise.all([
agoric.makeAccount(),
/** @type {Promise<OrchestrationAccount<any> & StakingAccountActions>} */ (
remoteChain.makeAccount()
),
]);

const [localChainAddress, remoteChainAddress] = await Promise.all([
localAccount.getAddress(),
stakingAccount.getAddress(),
]);
const agoricChainId = (await agoric.getChainInfo()).chainId;
const { transferChannel } = await chainHub.getConnectionInfo(
agoricChainId,
chainId,
);
assert(transferChannel.counterPartyChannelId, 'unable to find sourceChannel');

const localDenom = `ibc/${denomHash({ denom: remoteDenom, channelId: transferChannel.channelId })}`;

// Every time the `localAccount` receives `remoteDenom` over IBC, delegate it.
const tap = makeStakingTap({
localAccount,
stakingAccount,
validator,
localChainAddress,
remoteChainAddress,
sourceChannel: transferChannel.counterPartyChannelId,
remoteDenom,
localDenom,
});
// XXX consider storing appRegistration, so we can .revoke() or .updateTargetApp()
// @ts-expect-error tap.receiveUpcall: 'Vow<void> | undefined' not assignable to 'Promise<any>'
await localAccount.monitorTransfers(tap);

const accountEntries = harden(
/** @type {[string, OrchestrationAccount<any>][]} */ ([
['agoric', localAccount],
[chainName, stakingAccount],
]),
);
const publicTopicEntries = harden(
/** @type {[string, ResolvedPublicTopic<unknown>][]} */ (
await Promise.all(
accountEntries.map(async ([name, account]) => {
const { account: topicRecord } = await account.getPublicTopics();
return [name, topicRecord];
}),
)
),
);
const portfolioHolder = makePortfolioHolder(
accountEntries,
publicTopicEntries,
);
return portfolioHolder.asContinuingOffer();
};
harden(makeAccounts);
Loading

0 comments on commit 7199164

Please sign in to comment.