-
Notifications
You must be signed in to change notification settings - Fork 212
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: auto-stake-it example contract (#9666)
refs: #9042 refs: #9066 refs: #9193 ## Description - Enhances `LocalOrchestrationAccount` with `monitorTransfers`(vtransfer) method to register handlers for incoming and outgoing ICS20 transfers. - `writeAcknowledgement` (ability to confer acknowledgement or acknowledgement error to sending chain) capability is not exposed through the current orchestration api. users must work with `registerActiveTap` from `transfer.js` for this capability. - Implements `auto-stake-it` contract that uses .monitorTransfers to react to incoming IBC transfers, delegating them via an InterchainAccount (ICA). - referred to as "stakeAtom" on the ticket, but this seemed like a better name. not to be confused with _restaking (autocompounding rewards)_ - Introduces `PortfolioHolder` kit, combining `ContinuingOfferResults` from multiple `OrchestrationAccounts` into a single record. - Adds VTransferIBCEvent type for `acknowledgementPacket` and `writeAcknowledgement` events and an example mock for unit testing ### Documentation - Aims to improve documentation around vtransfer, monitorTransfers, registerTap, etc. ### Testing Considerations - Includes unit tests that inspect bridge messages to observe relevant transactions firing. - Includes multichain test demonstrating the flow from #9042, f.k.a. "stakeAtom"
- Loading branch information
Showing
45 changed files
with
2,170 additions
and
237 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,44 @@ | ||
diff --git a/node_modules/axios/dist/node/axios.cjs b/node_modules/axios/dist/node/axios.cjs | ||
index 9099d87..7104f6e 100644 | ||
--- a/node_modules/axios/dist/node/axios.cjs | ||
+++ b/node_modules/axios/dist/node/axios.cjs | ||
@@ -370,9 +370,9 @@ function merge(/* obj1, obj2, obj3, ... */) { | ||
const extend = (a, b, thisArg, {allOwnKeys}= {}) => { | ||
forEach(b, (val, key) => { | ||
if (thisArg && isFunction(val)) { | ||
- a[key] = bind(val, thisArg); | ||
+ Object.defineProperty(a, key, {value: bind(val, thisArg)}); | ||
} else { | ||
- a[key] = val; | ||
+ Object.defineProperty(a, key, {value: val}); | ||
} | ||
}, {allOwnKeys}); | ||
return a; | ||
@@ -403,7 +403,9 @@ const stripBOM = (content) => { | ||
*/ | ||
const inherits = (constructor, superConstructor, props, descriptors) => { | ||
constructor.prototype = Object.create(superConstructor.prototype, descriptors); | ||
- constructor.prototype.constructor = constructor; | ||
+ Object.defineProperty(constructor, 'constructor', { | ||
+ value: constructor | ||
+ }); | ||
Object.defineProperty(constructor, 'super', { | ||
value: superConstructor.prototype | ||
}); | ||
@@ -565,12 +567,14 @@ const isRegExp = kindOfTest('RegExp'); | ||
|
||
const reduceDescriptors = (obj, reducer) => { | ||
const descriptors = Object.getOwnPropertyDescriptors(obj); | ||
- const reducedDescriptors = {}; | ||
+ let reducedDescriptors = {}; | ||
|
||
forEach(descriptors, (descriptor, name) => { | ||
let ret; | ||
if ((ret = reducer(descriptor, name, obj)) !== false) { | ||
- reducedDescriptors[name] = ret || descriptor; | ||
+ reducedDescriptors = {...reducedDescriptors, | ||
+ [name]: ret || descriptor | ||
+ }; | ||
} | ||
}); | ||
|
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,56 @@ | ||
diff --git a/node_modules/protobufjs/src/util/minimal.js b/node_modules/protobufjs/src/util/minimal.js | ||
index 7f62daa..8d60657 100644 | ||
--- a/node_modules/protobufjs/src/util/minimal.js | ||
+++ b/node_modules/protobufjs/src/util/minimal.js | ||
@@ -259,14 +259,9 @@ util.newError = newError; | ||
* @returns {Constructor<Error>} Custom error constructor | ||
*/ | ||
function newError(name) { | ||
- | ||
function CustomError(message, properties) { | ||
- | ||
if (!(this instanceof CustomError)) | ||
return new CustomError(message, properties); | ||
- | ||
- // Error.call(this, message); | ||
- // ^ just returns a new error instance because the ctor can be called as a function | ||
|
||
Object.defineProperty(this, "message", { get: function() { return message; } }); | ||
|
||
@@ -280,13 +275,31 @@ function newError(name) { | ||
merge(this, properties); | ||
} | ||
|
||
- (CustomError.prototype = Object.create(Error.prototype)).constructor = CustomError; | ||
+ // Create a new object with Error.prototype as its prototype | ||
+ const proto = Object.create(Error.prototype); | ||
|
||
- Object.defineProperty(CustomError.prototype, "name", { get: function() { return name; } }); | ||
+ // Define properties on the prototype | ||
+ Object.defineProperties(proto, { | ||
+ constructor: { | ||
+ value: CustomError, | ||
+ writable: true, | ||
+ configurable: true | ||
+ }, | ||
+ name: { | ||
+ get: function() { return name; }, | ||
+ configurable: true | ||
+ }, | ||
+ toString: { | ||
+ value: function toString() { | ||
+ return this.name + ": " + this.message; | ||
+ }, | ||
+ writable: true, | ||
+ configurable: true | ||
+ } | ||
+ }); | ||
|
||
- CustomError.prototype.toString = function toString() { | ||
- return this.name + ": " + this.message; | ||
- }; | ||
+ // Set the prototype of CustomError | ||
+ CustomError.prototype = proto; | ||
|
||
return CustomError; | ||
} |
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
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,239 @@ | ||
import anyTest from '@endo/ses-ava/prepare-endo.js'; | ||
import type { ExecutionContext, TestFn } from 'ava'; | ||
import { useChain } from 'starshipjs'; | ||
import type { CosmosChainInfo, IBCConnectionInfo } from '@agoric/orchestration'; | ||
import type { SetupContextWithWallets } from './support.js'; | ||
import { chainConfig, commonSetup } from './support.js'; | ||
import { makeQueryClient } from '../tools/query.js'; | ||
import { makeDoOffer } from '../tools/e2e-tools.js'; | ||
import chainInfo from '../starship-chain-info.js'; | ||
import { | ||
createFundedWalletAndClient, | ||
makeIBCTransferMsg, | ||
} from '../tools/ibc-transfer.js'; | ||
|
||
const test = anyTest as TestFn<SetupContextWithWallets>; | ||
|
||
const accounts = ['agoricAdmin', 'cosmoshub', 'osmosis']; | ||
|
||
const contractName = 'autoAutoStakeIt'; | ||
const contractBuilder = | ||
'../packages/builders/scripts/testing/start-auto-stake-it.js'; | ||
|
||
test.before(async t => { | ||
const { deleteTestKeys, setupTestKeys, ...rest } = await commonSetup(t); | ||
deleteTestKeys(accounts).catch(); | ||
const wallets = await setupTestKeys(accounts); | ||
t.context = { ...rest, wallets, deleteTestKeys }; | ||
|
||
t.log('bundle and install contract', contractName); | ||
await t.context.deployBuilder(contractBuilder); | ||
const vstorageClient = t.context.makeQueryTool(); | ||
await t.context.retryUntilCondition( | ||
() => vstorageClient.queryData(`published.agoricNames.instance`), | ||
res => contractName in Object.fromEntries(res), | ||
`${contractName} instance is available`, | ||
); | ||
}); | ||
|
||
test.after(async t => { | ||
const { deleteTestKeys } = t.context; | ||
deleteTestKeys(accounts); | ||
}); | ||
|
||
const makeFundAndTransfer = (t: ExecutionContext<SetupContextWithWallets>) => { | ||
const { retryUntilCondition } = t.context; | ||
return async (chainName: string, agoricAddr: string, amount = 100n) => { | ||
const { staking } = useChain(chainName).chainInfo.chain; | ||
const denom = staking?.staking_tokens?.[0].denom; | ||
if (!denom) throw Error(`no denom for ${chainName}`); | ||
|
||
const { client, address, wallet } = await createFundedWalletAndClient( | ||
t, | ||
chainName, | ||
); | ||
const balancesResult = await retryUntilCondition( | ||
() => client.getAllBalances(address), | ||
coins => !!coins?.length, | ||
`Faucet balances found for ${address}`, | ||
); | ||
|
||
console.log('Balances:', balancesResult); | ||
|
||
const transferArgs = makeIBCTransferMsg( | ||
{ denom, value: amount }, | ||
{ address: agoricAddr, chainName: 'agoric' }, | ||
{ address: address, chainName }, | ||
Date.now(), | ||
); | ||
console.log('Transfer Args:', transferArgs); | ||
// TODO #9200 `sendIbcTokens` does not support `memo` | ||
// @ts-expect-error spread argument for concise code | ||
const txRes = await client.sendIbcTokens(...transferArgs); | ||
if (txRes && txRes.code !== 0) { | ||
console.error(txRes); | ||
throw Error(`failed to ibc transfer funds to ${chainName}`); | ||
} | ||
const { events: _events, ...txRest } = txRes; | ||
console.log(txRest); | ||
t.is(txRes.code, 0, `Transaction succeeded`); | ||
t.log(`Funds transferred to ${agoricAddr}`); | ||
return { | ||
client, | ||
address, | ||
wallet, | ||
}; | ||
}; | ||
}; | ||
|
||
const autoStakeItScenario = test.macro({ | ||
title: (_, chainName: string) => `auto-stake-it on ${chainName}`, | ||
exec: async (t, chainName: string) => { | ||
const { | ||
wallets, | ||
makeQueryTool, | ||
provisionSmartWallet, | ||
retryUntilCondition, | ||
} = t.context; | ||
|
||
const fundAndTransfer = makeFundAndTransfer(t); | ||
|
||
// 1. Send initial tokens so denom is available (debatably necessary, but | ||
// allows us to trace the denom until we have ibc denoms in chainInfo) | ||
const agAdminAddr = wallets['agoricAdmin']; | ||
console.log('Sending tokens to', agAdminAddr, `from ${chainName}`); | ||
await fundAndTransfer(chainName, agAdminAddr); | ||
|
||
// 2. Find 'stakingDenom' denom on agoric | ||
const agoricConns = chainInfo['agoric'].connections as Record< | ||
string, | ||
IBCConnectionInfo | ||
>; | ||
const remoteChainInfo = (chainInfo as Record<string, CosmosChainInfo>)[ | ||
chainName | ||
]; | ||
// const remoteChainId = remoteChainInfo.chain.chain_id; | ||
// const agoricToRemoteConn = agoricConns[remoteChainId]; | ||
const { portId, channelId } = | ||
agoricConns[remoteChainInfo.chainId].transferChannel; | ||
const agoricQueryClient = makeQueryClient( | ||
useChain('agoric').getRestEndpoint(), | ||
); | ||
const stakingDenom = remoteChainInfo?.stakingTokens?.[0].denom; | ||
if (!stakingDenom) throw Error(`staking denom found for ${chainName}`); | ||
const { hash } = await retryUntilCondition( | ||
() => | ||
agoricQueryClient.queryDenom(`/${portId}/${channelId}`, stakingDenom), | ||
denomTrace => !!denomTrace.hash, | ||
`local denom hash for ${stakingDenom} found`, | ||
); | ||
t.log(`found ibc denom hash for ${stakingDenom}:`, hash); | ||
|
||
// 3. Find a remoteChain validator to delegate to | ||
const remoteQueryClient = makeQueryClient( | ||
useChain(chainName).getRestEndpoint(), | ||
); | ||
const { validators } = await remoteQueryClient.queryValidators(); | ||
const validatorAddress = validators[0]?.operator_address; | ||
t.truthy( | ||
validatorAddress, | ||
`found a validator on ${chainName} to delegate to`, | ||
); | ||
t.log( | ||
{ validatorAddress }, | ||
`found a validator on ${chainName} to delegate to`, | ||
); | ||
|
||
// 4. Send an Offer to make the accounts and set up the transfer tap | ||
const agoricUserAddr = wallets[chainName]; | ||
const wdUser = await provisionSmartWallet(agoricUserAddr, { | ||
BLD: 100n, | ||
IST: 100n, | ||
}); | ||
const doOffer = makeDoOffer(wdUser); | ||
t.log(`${chainName} makeAccount offer`); | ||
const offerId = `${chainName}-makeAccountsInvitation-${Date.now()}`; | ||
|
||
await doOffer({ | ||
id: offerId, | ||
invitationSpec: { | ||
source: 'agoricContract', | ||
instancePath: [contractName], | ||
callPipe: [['makeAccountsInvitation']], | ||
}, | ||
offerArgs: { | ||
chainName, | ||
validator: { | ||
value: validatorAddress, | ||
encoding: 'bech32', | ||
chainId: remoteChainInfo.chainId, | ||
}, | ||
localDenom: `ibc/${hash}`, | ||
}, | ||
proposal: {}, | ||
}); | ||
|
||
// FIXME https://github.com/Agoric/agoric-sdk/issues/9643 | ||
const vstorageClient = makeQueryTool(); | ||
const currentWalletRecord = await retryUntilCondition( | ||
() => | ||
vstorageClient.queryData(`published.wallet.${agoricUserAddr}.current`), | ||
({ offerToPublicSubscriberPaths }) => | ||
Object.fromEntries(offerToPublicSubscriberPaths)[offerId], | ||
`${offerId} continuing invitation is in vstorage`, | ||
); | ||
|
||
const offerToPublicSubscriberMap = Object.fromEntries( | ||
currentWalletRecord.offerToPublicSubscriberPaths, | ||
); | ||
|
||
// 5. look up LOA address in vstorage | ||
console.log('offerToPublicSubscriberMap', offerToPublicSubscriberMap); | ||
const lcaAddress = offerToPublicSubscriberMap[offerId]?.agoric | ||
.split('.') | ||
.pop(); | ||
const icaAddress = offerToPublicSubscriberMap[offerId]?.[chainName] | ||
.split('.') | ||
.pop(); | ||
console.log({ lcaAddress, icaAddress }); | ||
t.regex(lcaAddress, /^agoric1/, 'LOA address is valid'); | ||
t.regex( | ||
icaAddress, | ||
new RegExp(`^${chainConfig[chainName].expectedAddressPrefix}1`), | ||
'COA address is valid', | ||
); | ||
|
||
// 6. transfer in some tokens over IBC | ||
const transferAmount = 99n; | ||
await fundAndTransfer(chainName, lcaAddress, transferAmount); | ||
|
||
// 7. verify the COA has active delegations | ||
if (chainName === 'cosmoshub') { | ||
// FIXME: delegations are not visible on cosmoshub | ||
return t.pass('skipping verifying delegations on cosmoshub'); | ||
} | ||
const { delegation_responses } = await retryUntilCondition( | ||
() => remoteQueryClient.queryDelegations(icaAddress), | ||
({ delegation_responses }) => !!delegation_responses.length, | ||
`delegations visible on ${chainName}`, | ||
); | ||
t.log('delegation balance', delegation_responses[0]?.balance); | ||
t.like( | ||
delegation_responses[0].balance, | ||
{ denom: stakingDenom, amount: String(transferAmount) }, | ||
'delegations balance', | ||
); | ||
t.log( | ||
`Orchestration Account Delegations on ${chainName}`, | ||
delegation_responses, | ||
); | ||
|
||
// XXX consider using PortfolioHolder continuing inv to undelegate | ||
|
||
// XXX how to test other tokens do not result in an attempted MsgTransfer or MsgDelegate? | ||
// query tx history of the LOA via an rpc node? | ||
}, | ||
}); | ||
|
||
test.serial(autoStakeItScenario, 'osmosis'); | ||
test.serial(autoStakeItScenario, 'cosmoshub'); |
Oops, something went wrong.