Skip to content
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

Merged
merged 17 commits into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion multichain-testing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
"typescript": "^5.3.3"
},
"resolutions": {
"node-fetch": "2.6.12"
"node-fetch": "2.6.12",
"axios": "1.6.7"
},
"ava": {
"extensions": {
Expand Down
44 changes: 44 additions & 0 deletions multichain-testing/patches/axios+1.6.7.patch
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
+ };
}
});

56 changes: 56 additions & 0 deletions multichain-testing/patches/protobufjs+6.11.4.patch
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;
}
2 changes: 1 addition & 1 deletion multichain-testing/scripts/fetch-starship-chain-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const chainInfo = await convertChainInfo({
});

const record = JSON.stringify(chainInfo, null, 2);
const src = `/** @file Generated by fetch-starship-chain-info.ts */\nexport default /** @type {const} } */ (${record});`;
const src = `/** @file Generated by fetch-starship-chain-info.ts */\nexport default /** @type {const} */ (${record});`;
const prettySrc = await prettier.format(src, {
parser: 'babel', // 'typescript' fails to preserve parens for typecast
singleQuote: true,
Expand Down
2 changes: 1 addition & 1 deletion multichain-testing/starship-chain-info.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** @file Generated by fetch-starship-chain-info.ts */
export default /** @type {const} } */ ({
export default /** @type {const} */ ({
agoric: {
chainId: 'agoriclocal',
stakingTokens: [
Expand Down
239 changes: 239 additions & 0 deletions multichain-testing/test/auto-stake-it.test.ts
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);
Copy link
Member Author

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 since client.simulate() and client.signAndBroadcast() kept yielding:

invalid source port ID: identifier cannot be blank: invalid ident

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 with cosmjs-types, so maybe something else.

An approach maybe worth exploring is using the RPC client from telescope via @agoric/cosmic-proto.

Copy link
Member

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 ?

Copy link
Member Author

@0xpatrickdev 0xpatrickdev Jul 12, 2024

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.

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({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this abstraction doesn't seem to carry its weight.

Suggested change
await doOffer({
await wallet.offers.executeOffer({

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It calls into seatLike, which has a bit of logic to ensure we get payouts. Seems like a bit more than this to inline.

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');
Loading
Loading