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

Agoric to EVM via Axelar #10976

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
90 changes: 90 additions & 0 deletions packages/boot/test/orchestration/axelar-gmp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/** @file Bootstrap test of restarting contracts using orchestration */
import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js';
import type { TestFn } from 'ava';
import { withChainCapabilities } from '@agoric/orchestration';
import {
makeWalletFactoryContext,
type WalletFactoryTestContext,
} from '../bootstrapTests/walletFactory.js';
import { minimalChainInfos } from '../tools/chainInfo.js';

const test: TestFn<WalletFactoryTestContext> = anyTest;
test.before(async t => {
t.context = await makeWalletFactoryContext(
t,
'@agoric/vm-config/decentral-itest-orchestration-config.json',
);
});
test.after.always(t => t.context.shutdown?.());

/**
* This test core-evals an installation of the axelarGmp contract that
* initiates an IBC Transfer.
*/
test('start axelarGmp and send an offer', async t => {
const {
walletFactoryDriver,
bridgeUtils: { runInbound },
buildProposal,
evalProposal,
storage,
} = t.context;

const { IST } = t.context.agoricNamesRemotes.brand;

t.log('start axelarGmp');
await evalProposal(
buildProposal('@agoric/builders/scripts/testing/init-axelar-gmp.js', [
'--chainInfo',
JSON.stringify(withChainCapabilities(minimalChainInfos)),
'--assetInfo',
JSON.stringify([
[
'uist',
{
baseDenom: 'uist',
baseName: 'agoric',
chainName: 'agoric',
},
],
]),
]),
);

t.log('making offer');
const wallet = await walletFactoryDriver.provideSmartWallet('agoric1test');
// no money in wallet to actually send
const zero = { brand: IST, value: 0n };
// send because it won't resolve
await wallet.sendOffer({
id: 'invokeEVMContract',
invitationSpec: {
source: 'agoricContract',
instancePath: ['axelarGmp'],
callPipe: [['makeSendInvitation']],
},
proposal: {
// @ts-expect-error XXX BoardRemote
give: { Send: zero },
},
offerArgs: {
destAddr: '0x20E68F6c276AC6E297aC46c84Ab260928276691D',
type: 1,
destinationEVMChain: 'ethereum',
gasAmount: 33,
contractInvocationPayload: [1, 0, 0, 1, 1],
},
});

const getLogged = () =>
JSON.parse(storage.data.get('published.axelarGmp.log')!).values;

// This log shows the flow started, but didn't get past the IBC Transfer settlement
t.deepEqual(getLogged(), [
'initiating sendIt',
'got info for denoms: ibc/FE98AAD68F02F03565E9FA39A5E627946699B2B07115889ED812D8BA639576A9, ibc/toyatom, ibc/toyusdc, ubld, uist',
'got info for chain: osmosis-1',
'completed transfer to localAccount',
'payload received',
]);
});
65 changes: 65 additions & 0 deletions packages/builders/scripts/testing/init-axelar-gmp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { makeHelpers } from '@agoric/deploy-script-support';
import {
getManifest,
startAxelarGmp,
} from '@agoric/orchestration/src/proposals/start-axelar-gmp.js';
import { parseArgs } from 'node:util';

/**
* @import {ParseArgsConfig} from 'node:util'
*/

/** @type {ParseArgsConfig['options']} */
const parserOpts = {
chainInfo: { type: 'string' },
assetInfo: { type: 'string' },
};

/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').CoreEvalBuilder} */
export const defaultProposalBuilder = async (
{ publishRef, install },
options,
) =>
harden({
sourceSpec: '@agoric/orchestration/src/proposals/start-axelar-gmp.js',
getManifestCall: [
getManifest.name,
{
installationRef: publishRef(
install('@agoric/orchestration/src/examples/axelar-gmp.contract.js'),
),
options,
},
],
});

/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').DeployScriptFunction} */
export default async (homeP, endowments) => {
const { scriptArgs } = endowments;

const {
values: { chainInfo, assetInfo },
} = parseArgs({
args: scriptArgs,
options: parserOpts,
});

const parseChainInfo = () => {
if (typeof chainInfo !== 'string') return undefined;
return JSON.parse(chainInfo);
};
const parseAssetInfo = () => {
if (typeof assetInfo !== 'string') return undefined;
return JSON.parse(assetInfo);
};
const opts = harden({
chainInfo: parseChainInfo(),
assetInfo: parseAssetInfo(),
});

const { writeCoreEval } = await makeHelpers(homeP, endowments);

await writeCoreEval(startAxelarGmp.name, utils =>
defaultProposalBuilder(utils, opts),
);
};
101 changes: 101 additions & 0 deletions packages/orchestration/src/examples/axelar-gmp.contract.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { InvitationShape } from '@agoric/zoe/src/typeGuards.js';
import { E } from '@endo/far';
import { M } from '@endo/patterns';
import { prepareChainHubAdmin } from '../exos/chain-hub-admin.js';
import { AnyNatAmountShape } from '../typeGuards.js';
import { withOrchestration } from '../utils/start-helper.js';
import { registerChainsAndAssets } from '../utils/chain-hub-helper.js';
import * as flows from './axelar.flows.js';
import * as sharedFlows from './shared.flows.js';

/**
* @import {Vow} from '@agoric/vow';
* @import {Zone} from '@agoric/zone';
* @import {OrchestrationPowers, OrchestrationTools} from '../utils/start-helper.js';
* @import {CosmosChainInfo, Denom, DenomDetail} from '@agoric/orchestration';
*/

export const SingleNatAmountRecord = M.and(
M.recordOf(M.string(), AnyNatAmountShape, {
numPropertiesLimit: 1,
}),
M.not(harden({})),
);
harden(SingleNatAmountRecord);

/**
* Orchestration contract to be wrapped by withOrchestration for Zoe
*
* @param {ZCF} zcf
* @param {OrchestrationPowers & {
* marshaller: Marshaller;
* chainInfo?: Record<string, CosmosChainInfo>;
* assetInfo?: [Denom, DenomDetail & { brandKey?: string }][];
* }} privateArgs
* @param {Zone} zone
* @param {OrchestrationTools} tools
*/
export const contract = async (
zcf,
privateArgs,
zone,
{ chainHub, orchestrateAll, vowTools, zoeTools },
) => {
const creatorFacet = prepareChainHubAdmin(zone, chainHub);

// UNTIL https://github.com/Agoric/agoric-sdk/issues/9066
const logNode = E(privateArgs.storageNode).makeChildNode('log');
/** @type {(msg: string) => Vow<void>} */
const log = msg => vowTools.watch(E(logNode).setValue(msg));

const { makeLocalAccount } = orchestrateAll(sharedFlows, {});
/**
* Setup a shared local account for use in async-flow functions. Typically,
* exo initState functions need to resolve synchronously, but `makeOnce`
* allows us to provide a Promise. When using this inside a flow, we must
* await it to ensure the account is available for use.
*
* @type {any} sharedLocalAccountP expects a Promise but this is a vow
* https://github.com/Agoric/agoric-sdk/issues/9822
*/
const sharedLocalAccountP = zone.makeOnce('localAccount', () =>
makeLocalAccount(),
);

// orchestrate uses the names on orchestrationFns to do a "prepare" of the associated behavior
const orchFns = orchestrateAll(flows, {
log,
sharedLocalAccountP,
zoeTools,
});

const publicFacet = zone.exo(
'Send PF',
M.interface('Send PF', {
makeSendInvitation: M.callWhen().returns(InvitationShape),
}),
{
makeSendInvitation() {
return zcf.makeInvitation(
orchFns.sendIt,
'send',
undefined,
M.splitRecord({ give: SingleNatAmountRecord }),
);
},
},
);

registerChainsAndAssets(
chainHub,
zcf.getTerms().brands,
privateArgs.chainInfo,
privateArgs.assetInfo,
);

return { publicFacet, creatorFacet };
};
harden(contract);

export const start = withOrchestration(contract);
harden(start);
136 changes: 136 additions & 0 deletions packages/orchestration/src/examples/axelar.flows.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { NonNullish } from '@agoric/internal';
import { makeError, q } from '@endo/errors';

/**
* @import {GuestInterface, GuestOf} from '@agoric/async-flow';
* @import {Vow} from '@agoric/vow';
* @import {LocalOrchestrationAccountKit} from '../exos/local-orchestration-account.js';
* @import {ZoeTools} from '../utils/zoe-tools.js';
* @import {Orchestrator, OrchestrationFlow, LocalAccountMethods} from '../types.js';
*/

const { entries } = Object;

const addresses = {
AXELAR_GMP:
'axelar1dv4u5k73pzqrxlzujxg3qp8kvc3pje7jtdvu72npnt5zhq05ejcsn5qme5',
AXELAR_GAS: 'axelar1zl3rxpp70lmte2xr6c4lgske2fyuj3hupcsvcd',
OSMOSIS_RECEIVER: 'osmo1yh3ra8eage5xtr9a3m5utg6mx0pmqreytudaqj',
};

const channels = {
AGORIC_XNET_TO_OSMOSIS: 'channel-6',
AGORIC_DEVNET_TO_OSMOSIS: 'channel-61',
OSMOSIS_TO_AXELAR: 'channel-4118',
};

/**
* @satisfies {OrchestrationFlow}
* @param {Orchestrator} orch
* @param {object} ctx
* @param {Promise<GuestInterface<LocalOrchestrationAccountKit['holder']>>} ctx.sharedLocalAccountP
* @param {GuestInterface<ZoeTools>} ctx.zoeTools
* @param {GuestOf<(msg: string) => Vow<void>>} ctx.log
* @param {ZCFSeat} seat
* @param {{
* destAddr: string;
* type: number;
* destinationEVMChain: string;
* gasAmount: number;
* contractInvocationPayload: number[];
* }} offerArgs
*/
export const sendIt = async (
orch,
{ sharedLocalAccountP, log, zoeTools: { localTransfer, withdrawToSeat } },
seat,
offerArgs,
) => {
const {
destAddr,
type,
destinationEVMChain,
gasAmount,
contractInvocationPayload,
} = offerArgs;

void log(`initiating sendIt`);

const { give } = seat.getProposal();
const [[_kw, amt]] = entries(give);

const agoric = await orch.getChain('agoric');
const assets = await agoric.getVBankAssetInfo();
void log(`got info for denoms: ${assets.map(a => a.denom).join(', ')}`);
const { denom } = NonNullish(
assets.find(a => a.brand === amt.brand),
`${amt.brand} not registered in vbank`,
);

const osmosisChain = await orch.getChain('osmosis');
const info = await osmosisChain.getChainInfo();
const { chainId } = info;
assert(typeof chainId === 'string', 'bad chainId');
void log(`got info for chain: ${chainId}`);

/**
* @type {any} XXX methods returning vows
* https://github.com/Agoric/agoric-sdk/issues/9822
*/
const sharedLocalAccount = await sharedLocalAccountP;
await localTransfer(seat, sharedLocalAccount, give);

void log(`completed transfer to localAccount`);

const payload = type === 1 || type === 2 ? contractInvocationPayload : null;

void log(`payload received`);

const memoToAxelar = {
destination_chain: destinationEVMChain,
destination_address: destAddr,
payload,
type,
};

if (type === 1 || type === 2) {
memoToAxelar.fee = {
amount: gasAmount,
recipient: addresses.AXELAR_GAS,
};
}

const memo = {
forward: {
receiver: addresses.AXELAR_GMP,
port: 'transfer',
channel: channels.OSMOSIS_TO_AXELAR,
timeout: '10m',
retries: 2,
next: JSON.stringify(memoToAxelar),
},
};

try {
await sharedLocalAccount.transfer(
{
value: addresses.OSMOSIS_RECEIVER,
encoding: 'bech32',
chainId,
},
{ denom, value: amt.value },
{ memo: JSON.stringify(memo) },
);
void log(`completed transfer to ${destAddr}`);
} catch (e) {
await withdrawToSeat(sharedLocalAccount, seat, give);
const errorMsg = `IBC Transfer failed ${q(e)}`;
void log(`ERROR: ${errorMsg}`);
seat.exit(errorMsg);
throw makeError(errorMsg);
}

seat.exit();
void log(`transfer complete, seat exited`);
};
harden(sendIt);
Loading
Loading