Skip to content

Commit

Permalink
feat: examples/auto-stake-it.contract.js
Browse files Browse the repository at this point in the history
- creates an example contract that user .monitorTransfers to react to an incoming IBC transfer. when the transfer is received, it's sent to an ICA
  then delegated. Both accounts are put in a PortfolioHolder kit, which combines ContinuingOfferResults into a single record
- includes logic to ignore outgoing transfers, uknown denoms, and unkown sourceChannels
- does not include logic to look for a specific value in the transfer memo field, but this could be added
- refs: #9042
  • Loading branch information
0xpatrickdev committed Jul 17, 2024
1 parent aee13b7 commit b87ecba
Show file tree
Hide file tree
Showing 5 changed files with 630 additions and 0 deletions.
10 changes: 10 additions & 0 deletions packages/boot/test/bootstrapTests/orchestration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,3 +310,13 @@ test('basic-flows', async t => {
});
t.is(readLatest('published.basicFlows.agoric1mockVlocalchainAddress'), '');
});

test.serial('auto-stake-it - proposal', async t => {
const { buildProposal, evalProposal } = t.context;

await t.notThrowsAsync(
evalProposal(
buildProposal('@agoric/builders/scripts/testing/start-auto-stake-it.js'),
),
);
});
126 changes: 126 additions & 0 deletions packages/builders/scripts/testing/start-auto-stake-it.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/**
* @file A proposal to start the auto-stake-it contract.
*
* AutoStakeIt allows users to to create an auto-forwarding address that
* transfers and stakes tokens on a remote chain when received.
*/
import { makeTracer } from '@agoric/internal';
import { makeStorageNodeChild } from '@agoric/internal/src/lib-chainStorage.js';
import { E } from '@endo/far';
import { deeplyFulfilled } from '@endo/marshal';

/**
* @import {AutoStakeItSF} from '@agoric/orchestration/src/examples/auto-stake-it.contract.js';
*/

const contractName = 'autoAutoStakeIt';
const trace = makeTracer(contractName, true);

/**
* @param {BootstrapPowers} powers
*/
export const startAutoStakeIt = async ({
consume: {
agoricNames,
board,
chainStorage,
chainTimerService,
cosmosInterchainService,
localchain,
startUpgradable,
},
installation: {
// @ts-expect-error not a WellKnownName
consume: { [contractName]: installation },
},
instance: {
// @ts-expect-error not a WellKnownName
produce: { [contractName]: produceInstance },
},
}) => {
trace(`start ${contractName}`);
await null;

const storageNode = await makeStorageNodeChild(chainStorage, contractName);
const marshaller = await E(board).getPublishingMarshaller();

/** @type {StartUpgradableOpts<AutoStakeItSF>} */
const startOpts = {
label: 'autoAutoStakeIt',
installation,
terms: undefined,
privateArgs: await deeplyFulfilled(
harden({
agoricNames,
orchestrationService: cosmosInterchainService,
localchain,
storageNode,
marshaller,
timerService: chainTimerService,
}),
),
};

const { instance } = await E(startUpgradable)(startOpts);
produceInstance.resolve(instance);
};
harden(startAutoStakeIt);

export const getManifestForContract = (
{ restoreRef },
{ installKeys, ...options },
) => {
return {
manifest: {
[startAutoStakeIt.name]: {
consume: {
agoricNames: true,
board: true,
chainStorage: true,
chainTimerService: true,
cosmosInterchainService: true,
localchain: true,
startUpgradable: true,
},
installation: {
consume: { [contractName]: true },
},
instance: {
produce: { [contractName]: true },
},
},
},
installations: {
[contractName]: restoreRef(installKeys[contractName]),
},
options,
};
};

/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').CoreEvalBuilder} */
export const defaultProposalBuilder = async ({ publishRef, install }) => {
return harden({
// Somewhat unorthodox, source the exports from this builder module
sourceSpec: '@agoric/builders/scripts/testing/start-auto-stake-it.js',
getManifestCall: [
'getManifestForContract',
{
installKeys: {
autoAutoStakeIt: publishRef(
install(
'@agoric/orchestration/src/examples/auto-stake-it.contract.js',
),
),
},
},
],
});
};

export default async (homeP, endowments) => {
// import dynamically so the module can work in CoreEval environment
const dspModule = await import('@agoric/deploy-script-support');
const { makeHelpers } = dspModule;
const { writeCoreEval } = await makeHelpers(homeP, endowments);
await writeCoreEval(startAutoStakeIt.name, defaultProposalBuilder);
};
157 changes: 157 additions & 0 deletions packages/orchestration/src/examples/auto-stake-it-tap-kit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
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(
{
denom: localDenom,
value: BigInt(tx.amount),
},
remoteChainAddress,
),
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 */
Loading

0 comments on commit b87ecba

Please sign in to comment.