-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
42 changed files
with
9,636 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 */ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 */ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
Oops, something went wrong.