Skip to content

Commit

Permalink
feat: sendAnywhere contract, test
Browse files Browse the repository at this point in the history
  • Loading branch information
dckc committed Jun 7, 2024
1 parent 77374be commit 2f9963d
Show file tree
Hide file tree
Showing 2 changed files with 361 additions and 0 deletions.
168 changes: 168 additions & 0 deletions packages/orchestration/src/examples/sendAnywhere.contract.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { makeDurableZone } from '@agoric/zone/durable.js';
import { M, mustMatch } from '@endo/patterns';
import { InvitationShape } from '@agoric/zoe/src/typeGuards.js';
import { E } from '@endo/far';
import { withdrawFromSeat } from '@agoric/zoe/src/contractSupport/zoeHelpers.js';

import { AmountShape } from '@agoric/ertp';
import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/recorder.js';
import { CosmosChainInfoShape } from '../typeGuards.js';
import { makeOrchestrationFacade } from '../facade.js';
import { prepareLocalChainAccountKit } from '../exos/local-chain-account-kit.js';
import { makeChainHub } from '../utils/chainHub.js';

const { entries } = Object;
const { Fail } = assert;

/**
* @import {Baggage} from '@agoric/vat-data';
* @import {CosmosChainInfo, IBCConnectionInfo} from '../cosmos-api';
* @import {TimerService, TimerBrand} from '@agoric/time';
* @import {LocalChain} from '@agoric/vats/src/localchain.js';
* @import {OrchestrationService} from '../service.js';
* @import {NameHub} from '@agoric/vats';
* @import {Remote} from '@agoric/vow';
*/

/**
* @typedef {{
* localchain: Remote<LocalChain>;
* orchestrationService: Remote<OrchestrationService>;
* storageNode: Remote<StorageNode>;
* timerService: Remote<TimerService>;
* agoricNames: Remote<NameHub>;
* }} OrchestrationPowers
*/

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

/**
* @param {ZCF} zcf
* @param {OrchestrationPowers & {
* marshaller: Marshaller;
* }} privateArgs
* @param {Baggage} baggage
*/
export const start = async (zcf, privateArgs, baggage) => {
const zone = makeDurableZone(baggage);

const chainHub = makeChainHub(privateArgs.agoricNames);

// TODO once durability is settled, provide some helpers to reduce boilerplate
const { marshaller, ...orchPowers } = privateArgs;
const { makeRecorderKit } = prepareRecorderKitMakers(baggage, marshaller);
const makeLocalChainAccountKit = prepareLocalChainAccountKit(
zone,
makeRecorderKit,
zcf,
privateArgs.timerService,
chainHub,
);
const { orchestrate } = makeOrchestrationFacade({
zcf,
zone,
chainHub,
makeLocalChainAccountKit,
...orchPowers,
});

let contractAccount;

const findBrandInVBank = async brand => {
const assets = await E(
E(privateArgs.agoricNames).lookup('vbankAsset'),
).values();
const it = assets.find(a => a.brand === brand);
it || Fail`brand ${brand} not in agoricNames.vbankAsset`;
return it;
};

/** @type {OfferHandler} */
const sendIt = orchestrate(
'sendIt',
{ zcf },
// eslint-disable-next-line no-shadow -- this `zcf` is enclosed in a membrane
async (orch, { zcf }, seat, offerArgs) => {
mustMatch(
offerArgs,
harden({ chainName: M.scalar(), destAddr: M.string() }),
);
const { chainName, destAddr } = offerArgs;
const { give } = seat.getProposal();
const [[kw, amt]] = entries(give);
const { denom } = await findBrandInVBank(amt.brand);
const chain = await orch.getChain(chainName);

// XXX ok to use a heap var crossing the membrane scope this way?
if (!contractAccount) {
const agoricChain = await orch.getChain('agoric');
contractAccount = await agoricChain.makeAccount();
}

const info = await chain.getChainInfo();
const { chainId } = info;
const { [kw]: pmtP } = await withdrawFromSeat(zcf, seat, give);
await E.when(pmtP, pmt => contractAccount.deposit(pmt, amt));
await contractAccount.transfer(
{ denom, value: amt.value },
{
address: destAddr,
addressEncoding: 'bech32',
chainId,
},
);
},
);

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

let nonce = 0n;
const ConnectionInfoShape = M.record(); // TODO
const creatorFacet = zone.exo(
'Send CF',
M.interface('Send CF', {
addChain: M.callWhen(CosmosChainInfoShape, ConnectionInfoShape).returns(
M.scalar(),
),
}),
{
/**
* @param {CosmosChainInfo} chainInfo
* @param {IBCConnectionInfo} connectionInfo
*/
async addChain(chainInfo, connectionInfo) {
const chainKey = `${chainInfo.chainId}-${(nonce += 1n)}`;
const agoricChainInfo = await chainHub.getChainInfo('agoric');
chainHub.registerChain(chainKey, chainInfo);
chainHub.registerConnection(
agoricChainInfo.chainId,
chainInfo.chainId,
connectionInfo,
);
return chainKey;
},
},
);

return { publicFacet, creatorFacet };
};
193 changes: 193 additions & 0 deletions packages/orchestration/test/examples/sendAnywhere.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js';

import { setUpZoeForTest } from '@agoric/zoe/tools/setup-zoe.js';
import { E } from '@endo/far';
import path from 'path';

import { mustMatch } from '@endo/patterns';
import { makeIssuerKit } from '@agoric/ertp';
import { CosmosChainInfo, IBCConnectionInfo } from '../../src/cosmos-api.js';
import { commonSetup } from '../supports.js';
import { SingleAmountRecord } from '../../src/examples/sendAnywhere.contract.js';

const dirname = path.dirname(new URL(import.meta.url).pathname);

const contractName = 'sendAnywhere';
const contractFile = `${dirname}/../../src/examples/${contractName}.contract.js`;
type StartFn =
typeof import('../../src/examples/sendAnywhere.contract.js').start;

const chainInfoDefaults = {
connections: {},
allowedMessages: [],
allowedQueries: [],
ibcHooksEnabled: false,
icaEnabled: false,
icqEnabled: false,
pfmEnabled: false,
};

const connectionDefaults = {
versions: [
{
identifier: '1',
features: ['ORDER_ORDERED', 'ORDER_UNORDERED'],
},
],
delay_period: 0n,
};

const txChannelDefaults = {
counterPartyPortId: 'transfer',
version: 'ics20-1',
portId: 'transfer',
ordering: 1, // ORDER_UNORDERED
state: 3, // STATE_OPEN
};

test('single amount proposal shape (keyword record)', async t => {
const { brand } = makeIssuerKit('IST');
const amt = harden({ brand, value: 1n });
const cases = harden({
good: [{ Kw: amt }],
bad: [
{ give: { Kw1: amt, Kw2: amt }, msg: /more than 1/ },
{ give: {}, msg: /fail negated pattern: {}/ },
{ give: { Kw: 123 }, msg: /Must be a copyRecord/ },
{ give: { Kw: { brand: 1, value: 1n } }, msg: /Must be a remotable/ },
],
});
for (const give of cases.good) {
t.notThrows(() => mustMatch(give, SingleAmountRecord));
}
for (const { give, msg } of cases.bad) {
t.throws(() => mustMatch(give, SingleAmountRecord), {
message: msg,
});
}
});

test('send using arbitrary chain info', async t => {
t.log('bootstrap, orchestration core-eval');
const {
bootstrap,
commonPrivateArgs,
brands: { ist },
utils: { inspectLocalBridge, pourPayment },
} = await commonSetup(t);

const { zoe, bundleAndInstall } = await setUpZoeForTest();

t.log('contract coreEval', contractName);

const installation: Installation<StartFn> =
await bundleAndInstall(contractFile);

const { instance, creatorFacet } = await E(zoe).startInstance(
installation,
{ Stable: ist.issuer },
{},
{
...commonPrivateArgs,
storageNode: await E(bootstrap.storage.rootNode).makeChildNode(
contractName,
),
},
);

t.log('admin adds hot new chain to contract');
const hotChainInfo = harden({
chainId: 'hot-new-chain-0',
stakingTokens: [{ denom: 'uhot' }],
...chainInfoDefaults,
}) as CosmosChainInfo;
const agoricToHotConnection = {
...connectionDefaults,
id: 'connection-1',
client_id: '07-tendermint-1',
state: 3, // STATE_OPEN
counterparty: {
client_id: '07-tendermint-2109',
connection_id: 'connection-1649',
prefix: {
key_prefix: 'aWJj',
},
},
transferChannel: {
counterPartyChannelId: 'channel-1',
channelId: 'channel-0',
...txChannelDefaults,
},
} as IBCConnectionInfo;
t.log('add chain using creatorFacet', hotChainInfo.chainId);
const chainName = await E(creatorFacet).addChain(
hotChainInfo,
agoricToHotConnection,
);

t.log('client uses contract to send to hot new chain');
{
const publicFacet = await E(zoe).getPublicFacet(instance);
const inv = E(publicFacet).makeSendInvitation();

const amt = await E(zoe).getInvitationDetails(inv);
t.is(amt.description, 'send');

const anAmt = ist.units(3.5);
const Send = await pourPayment(anAmt);
const dest = { destAddr: 'hot1destAddr', chainName };
const userSeat = await E(zoe).offer(
inv,
{ give: { Send: anAmt } },
{ Send },
dest,
);
await E(userSeat).getOfferResult();

const history = inspectLocalBridge();
t.like(history, [
{ type: 'VLOCALCHAIN_ALLOCATE_ADDRESS' },
{ type: 'VLOCALCHAIN_EXECUTE_TX' },
]);
const [_alloc, { messages, address: execAddr }] = history;
t.is(messages.length, 1);
const [txfr] = messages;
t.log('local bridge', txfr);
t.like(txfr, {
'@type': '/ibc.applications.transfer.v1.MsgTransfer',
receiver: 'hot1destAddr',
sender: execAddr,
sourceChannel: 'channel-0',
sourcePort: 'transfer',
token: { amount: '3500000', denom: 'uist' },
});
}

t.log('well-known chains such as cosmos work the same way');
{
const anAmt = ist.units(1.25);
const Send = await pourPayment(anAmt);
const dest = { destAddr: 'cosmos1destAddr', chainName: 'cosmos' };
const publicFacet = await E(zoe).getPublicFacet(instance);
const inv = E(publicFacet).makeSendInvitation();
const userSeat = await E(zoe).offer(
inv,
{ give: { Send: anAmt } },
{ Send },
dest,
);
await E(userSeat).getOfferResult();
const history = inspectLocalBridge();
const { messages, address: execAddr } = history.at(-1);
t.is(messages.length, 1);
const [txfr] = messages;
t.log('local bridge', txfr);
t.like(txfr, {
'@type': '/ibc.applications.transfer.v1.MsgTransfer',
receiver: 'cosmos1destAddr',
sender: execAddr,
sourceChannel: 'channel-1',
token: { amount: '1250000', denom: 'uist' },
});
}
});

0 comments on commit 2f9963d

Please sign in to comment.