-
Notifications
You must be signed in to change notification settings - Fork 212
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: auto-stake-it example contract #9666
Changes from all commits
6a43cf1
0f18bb1
ee5080c
d1b8a45
7119df3
b64c671
b57c2d5
2c9df2b
aee13b7
b87ecba
d809d65
cacd4ea
8f1618b
f66c60e
8f0a067
fd3d14b
350ff5e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
+ }; | ||
} | ||
}); | ||
|
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; | ||
} |
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({ | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this abstraction doesn't seem to carry its weight.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It calls into |
||||||
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'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sendIbcTokens
is deprecated, since it can't supply a memo field on the MsgTransfer (just the outer tx). Only using it here sinceclient.simulate()
andclient.signAndBroadcast()
kept yielding:It would seem like an obvious error to address, but appears to be more of a red herring as sourcePort was definitely provided. I suspected a mismatch between the client registry and the encoder (
@agoric/cosmic-proto
), but registering a custom messge did not seem to do the trick either. I similarly had no luck withcosmjs-types
, so maybe something else.An approach maybe worth exploring is using the RPC client from telescope via
@agoric/cosmic-proto
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is fine for this PR. Should I prioritize #9200 ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#9200 would helpful, didn't realize we had a ticket.
In terms of priority, I don't see this comes into play until we need to send things besides
MsgTransfer
, e.g. seeding liquidity on Osmosis or Umee.At this time, #9643 seems like it'd be more valuable.