Skip to content

Commit

Permalink
test: multichain test of auto-stake-it
Browse files Browse the repository at this point in the history
- auto-stake-it transfer tokens to an InterchainAccount and delegates them when received over IBC to a contract controlled account
- includes patches for @cosmjs/stargate, since we are using it to execute IBC transfers from external accounts
- refs: #9042
  • Loading branch information
0xpatrickdev committed Jul 15, 2024
1 parent 315c42d commit 4ce37d6
Show file tree
Hide file tree
Showing 11 changed files with 467 additions and 15 deletions.
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;
}
231 changes: 231 additions & 0 deletions multichain-testing/test/auto-stake-it.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
import anyTest from '@endo/ses-ava/prepare-endo.js';
import type { ExecutionContext, TestFn } from 'ava';
import { useChain } from 'starshipjs';
import type { ChainName, SetupContextWithWallets } from './support.js';
import { chainConfig, chainNames, 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 = ['admin1', 'user1', 'user2'];

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: ChainName, 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',
);

console.log('Balances:', balancesResult);

const transferArgs = makeIBCTransferMsg(
{ denom, value: amount },
{ address: agoricAddr, chainName: 'agoric' },
{ address: address, chainName },
Date.now(),
);
// 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 send 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: ChainName) => `auto-stake-it on ${chainName}`,
exec: async (t, chainName: ChainName) => {
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['admin1'];
console.log('Sending tokens to', agAdminAddr, `from ${chainName}`);
await fundAndTransfer(chainName, agAdminAddr);

// 2. Find 'stakingDenom' denom on agoric
const remoteChainInfo = useChain(chainName).chainInfo;
const { portId, channelId } =
chainInfo['agoric'].connections[remoteChainInfo.chain.chain_id]
.transferChannel;
const agoricQueryClient = makeQueryClient(
useChain('agoric').getRestEndpoint(),
);
const { hash } = await retryUntilCondition(
() =>
agoricQueryClient.queryDenom(
`/${portId}/${channelId}`,
chainConfig[chainName].denom,
),
denomTrace => !!denomTrace.hash,
`local denom hash for ${chainConfig[chainName].denom} found`,
);
t.log(`found ibc denom hash for ${chainConfig[chainName].denom}:`, 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[accounts[chainNames.indexOf(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.chain.chain_id,
},
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: chainConfig[chainName].denom, 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');
3 changes: 1 addition & 2 deletions multichain-testing/test/basic-flows.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const makeAccountScenario = test.macro({
// FIXME we get payouts but not an offer result; it times out
// https://github.com/Agoric/agoric-sdk/issues/9643
// chain logs shows an UNPUBLISHED result
const _offerResult = await doOffer({
await doOffer({
id: offerId,
invitationSpec: {
source: 'agoricContract',
Expand All @@ -77,7 +77,6 @@ const makeAccountScenario = test.macro({
offerArgs: { chainName },
proposal: {},
});
t.true(_offerResult);
// t.is(await _offerResult, 'UNPUBLISHED', 'representation of continuing offer');

// TODO fix above so we don't have to poll for the offer result to be published
Expand Down
Loading

0 comments on commit 4ce37d6

Please sign in to comment.