diff --git a/packages/orch-skel/README.md b/packages/orch-skel/README.md new file mode 100644 index 00000000000..2f82a37a8cc --- /dev/null +++ b/packages/orch-skel/README.md @@ -0,0 +1,27 @@ +# orch-skel - skeletal orchestration contract package + +To develop an orchestration contract: + 1. make a rough sequence diagram - `test/my-orch-sequence.mmd` + 2. refine the sequence diagram to `@agoric/orchestration` objects and messages + 3. prototype each of the objects in the sequence diagram and make a test to exercise them - `test/my-orch-seq-sim.test.ts` + 4. refine the prototype into a contract (`src/my.contract.ts`) with flows (`my.flows.ts`) and make a test for it (`test/my-orch-contract.test.ts`) + +## Install Dependencies + +``` +yarn install +``` + +## Run Static Checks + +```console +yarn lint +``` + +## Run Tests + +```console +yarn test +``` + +Don't be surprised by `Error#2: TODO!`: the contract and flows are incomplete. diff --git a/packages/orch-skel/package.json b/packages/orch-skel/package.json new file mode 100644 index 00000000000..3e16529bdc5 --- /dev/null +++ b/packages/orch-skel/package.json @@ -0,0 +1,63 @@ +{ + "name": "@aglocal/orch-skel-contract", + "private": true, + "version": "0.1.0", + "description": "Smart contract component of orchestration skeleton", + "type": "module", + "files": [ + "src", + "tools" + ], + "scripts": { + "build": "exit 0", + "test": "ava", + "test:c8": "c8 --all $C8_OPTIONS ava", + "test:xs": "exit 0", + "lint-fix": "yarn lint:eslint --fix", + "lint": "run-s --continue-on-error lint:*", + "lint:types": "tsc", + "lint:eslint": "eslint ." + }, + "devDependencies": { + "@agoric/cosmic-proto": "^0.4.0", + "@agoric/ertp": "^0.16.2", + "@agoric/fast-usdc": "^0.1.0", + "@agoric/internal": "^0.3.2", + "@agoric/swingset-liveslots": "^0.10.2", + "@agoric/vow": "^0.1.0", + "@agoric/zoe": "^0.26.2", + "@agoric/zone": "^0.2.2", + "@endo/init": "^1.1.9", + "@endo/nat": "^5.1.0", + "@endo/promise-kit": "^1.1.10", + "@fast-check/ava": "^2.0.1", + "ava": "^5.3.0", + "c8": "^10.1.3", + "ts-blank-space": "^0.6.1" + }, + "dependencies": { + "@agoric/orchestration": "^0.1.0", + "@agoric/vats": "^0.15.1", + "@endo/errors": "^1.2.10", + "@endo/far": "^1.1.11", + "@endo/pass-style": "^1.5.0", + "@endo/patterns": "^1.5.0" + }, + "ava": { + "extensions": { + "js": true, + "ts": "module" + }, + "files": [ + "test/**/*.test.*" + ], + "nodeArguments": [ + "--import=ts-blank-space/register", + "--no-warnings" + ], + "require": [ + "@endo/init/debug.js" + ], + "timeout": "20m" + } +} diff --git a/packages/orch-skel/src/my.contract.ts b/packages/orch-skel/src/my.contract.ts new file mode 100644 index 00000000000..d7c2a784b4c --- /dev/null +++ b/packages/orch-skel/src/my.contract.ts @@ -0,0 +1,60 @@ +import { + OrchestrationPowersShape, + withOrchestration, + type OrchestrationTools, +} from '@agoric/orchestration'; +import { type VTransferIBCEvent } from '@agoric/vats'; +import type { Zone } from '@agoric/zone'; +import { E } from '@endo/far'; +import { M } from '@endo/patterns'; +import * as flows from './my.flows.ts'; + +const interfaceTODO = undefined; + +export const meta = M.splitRecord({ + privateArgsShape: { + // @ts-expect-error TypedPattern not recognized as record + ...OrchestrationPowersShape, + marshaller: M.remotable('marshaller'), + }, +}); +harden(meta); + +export const contract = async ( + _zcf, + _privateArgs, + zone: Zone, + tools: OrchestrationTools, +) => { + const { orchestrateAll } = tools; + const { makeHookAccount, makePosition } = orchestrateAll(flows, {}); + + const { when } = tools.vowTools; + + const tap = zone.makeOnce('tapPosition', _key => { + console.log('making tap'); + return zone.exo('tap', interfaceTODO, { + async receiveUpcall(event: VTransferIBCEvent) { + console.log('receiveUpcall', event); + // TODO: use watch() rather than when for resumability + await when(makePosition()).catch(error => { + console.log('receiveUpcall: flow failed:', error); + }); + }, + }); + }); + + const hookAccountV = zone.makeOnce('hookAccount', _key => + makeHookAccount(tap), + ); + + return { + publicFacet: zone.exo('MyPub', interfaceTODO, { + getHookAddress: () => E(when(hookAccountV)).getAddress(), + }), + creatorFacet: zone.exo('MyCreator', undefined, {}), + }; +}; + +export const start = withOrchestration(contract); +harden(start); diff --git a/packages/orch-skel/src/my.flows.ts b/packages/orch-skel/src/my.flows.ts new file mode 100644 index 00000000000..3b44e7562c2 --- /dev/null +++ b/packages/orch-skel/src/my.flows.ts @@ -0,0 +1,29 @@ +import type { + OrchestrationAccount, + OrchestrationFlow, + Orchestrator, +} from '@agoric/orchestration'; +import type { TargetApp } from '@agoric/vats/src/bridge-target.js'; +import type { Passable } from '@endo/pass-style'; + +export const makeHookAccount = (async ( + orch: Orchestrator, + _ctx: unknown, + tap: TargetApp & Passable, +) => { + const agoricChain = await orch.getChain('agoric'); + const hookAccount = + (await agoricChain.makeAccount()) as OrchestrationAccount<{ + chainId: 'agoric-any'; + }>; + + const registration = hookAccount.monitorTransfers(tap); + console.warn('TODO: keep registration', registration); + + return hookAccount; +}) satisfies OrchestrationFlow; +harden(makeHookAccount); + +export const makePosition = (async (orch: Orchestrator) => { + throw Error('TODO!'); +}) satisfies OrchestrationFlow; diff --git a/packages/orch-skel/test/mocks.ts b/packages/orch-skel/test/mocks.ts new file mode 100644 index 00000000000..f0e392b7925 --- /dev/null +++ b/packages/orch-skel/test/mocks.ts @@ -0,0 +1,132 @@ +import type { HostInterface } from '@agoric/async-flow'; +import type { Brand, Issuer, Payment } from '@agoric/ertp'; +import type { + CosmosChainAddress, + DenomAmount, + OrchestrationAccount, +} from '@agoric/orchestration'; +import type { VowTools } from '@agoric/vow'; +import { makeRatio } from '@agoric/ertp/src/ratio.js'; +import type { AmountUtils } from '@agoric/zoe/tools/test-utils.js'; +import type { Zone } from '@agoric/zone'; +import type { FeeConfig, LogFn } from '@agoric/fast-usdc/src/types.js'; +import { makePromiseKit } from '@endo/promise-kit'; + +export const prepareMockOrchAccounts = ( + zone: Zone, + { + vowTools: { makeVowKit, asVow }, + log, + usdc, + }: { + vowTools: VowTools; + log: (...args: any[]) => void; + usdc: { brand: Brand<'nat'>; issuer: Issuer<'nat'> }; + }, +) => { + // each can only be resolved/rejected once per test + const poolAccountSendPK = makePromiseKit(); + const poolAccountTransferPK = makePromiseKit(); + const settleAccountTransferPK = makePromiseKit(); + const settleAccountSendPK = makePromiseKit(); + const intermediateAccountTransferPK = makePromiseKit(); + const intermediateAccountDepositForBurnPK = makePromiseKit(); + + const mockedPoolAccount = zone.exo('Mock Pool LocalOrchAccount', undefined, { + transfer(destination: CosmosChainAddress, amount: DenomAmount) { + log('PoolAccount.transfer() called with', destination, amount); + return poolAccountTransferPK.promise; + }, + deposit(payment: Payment<'nat'>) { + log('PoolAccount.deposit() called with', payment); + // XXX consider a mock for deposit failure + return asVow(async () => usdc.issuer.getAmountOf(payment)); + }, + send(destination: CosmosChainAddress, amount: DenomAmount) { + log('PoolAccount.send() called with', destination, amount); + return poolAccountSendPK.promise; + }, + }); + + const poolAccount = mockedPoolAccount as unknown as HostInterface< + OrchestrationAccount<{ chainId: 'agoric-any' }> + >; + + const settlementCallLog = [] as any[]; + const settlementAccountMock = zone.exo('Mock Settlement Account', undefined, { + transfer(...args) { + settlementCallLog.push(harden(['transfer', ...args])); + return settleAccountTransferPK.promise; + }, + send(...args) { + settlementCallLog.push(harden(['send', ...args])); + return settleAccountSendPK.promise; + }, + }); + const settlementAccount = settlementAccountMock as unknown as HostInterface< + OrchestrationAccount<{ chainId: 'agoric-any' }> + >; + const intermediateCallLog = [] as any[]; + const intermediateAccountMock = zone.exo('Mock Noble ICA', undefined, { + getAddress(): CosmosChainAddress { + return { + chainId: 'noble-1', + encoding: 'bech32', + value: 'noble1test', + }; + }, + transfer(...args) { + intermediateCallLog.push(harden(['transfer', ...args])); + return intermediateAccountTransferPK.promise; + }, + depositForBurn(...args) { + intermediateCallLog.push(harden(['depositForBurn', ...args])); + return intermediateAccountDepositForBurnPK.promise; + }, + }); + const intermediateAccount = + intermediateAccountMock as unknown as HostInterface< + OrchestrationAccount<{ chainId: 'noble-any' }> + >; + return { + // These each have VResolver for "vow" resolver. The mocks actually + // deal in promises but the flow that awaits them expects that it's actually + // awaiting a vow (made by the membrane to look like a promise). + mockPoolAccount: { + account: poolAccount, + transferVResolver: poolAccountTransferPK, + sendVResolver: poolAccountSendPK, + }, + settlement: { + account: settlementAccount, + callLog: settlementCallLog, + transferVResolver: settleAccountTransferPK, + sendVResolver: settleAccountSendPK, + }, + intermediate: { + account: intermediateAccount, + callLog: intermediateCallLog, + transferVResolver: intermediateAccountTransferPK, + depositForBurnVResolver: intermediateAccountDepositForBurnPK, + }, + }; +}; + +export const makeTestLogger = (logger: LogFn) => { + const logs: unknown[][] = []; + const log = (...args: any[]) => { + logs.push(args); + logger(args); + }; + const inspectLogs = (index?: number) => + typeof index === 'number' ? logs[index] : logs; + return { log, inspectLogs }; +}; +export type TestLogger = ReturnType; + +export const makeTestFeeConfig = (usdc: Omit): FeeConfig => + harden({ + flat: usdc.make(1n), + variableRate: makeRatio(2n, usdc.brand), + contractRate: makeRatio(20n, usdc.brand), + }); diff --git a/packages/orch-skel/test/my-orch-contract.test.ts b/packages/orch-skel/test/my-orch-contract.test.ts new file mode 100644 index 00000000000..15cdbf83eea --- /dev/null +++ b/packages/orch-skel/test/my-orch-contract.test.ts @@ -0,0 +1,70 @@ +// prepare-test-env has to go 1st; use a blank line to separate it +import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; + +import type { CoinSDKType } from '@agoric/cosmic-proto/cosmos/base/v1beta1/coin.js'; +import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; +import { heapVowE as VE } from '@agoric/vow/vat.js'; +import { setUpZoeForTest } from '@agoric/zoe/tools/setup-zoe.js'; +import { E, passStyleOf } from '@endo/far'; +import { Nat } from '@endo/nat'; +import { M, mustMatch } from '@endo/patterns'; +import { createRequire } from 'module'; +import { ChainAddressShape } from '@agoric/orchestration'; +import { buildVTransferEvent } from '@agoric/orchestration/tools/ibc-mocks.js'; +import { commonSetup } from './supports.js'; + +const nodeRequire = createRequire(import.meta.url); + +const contractName = 'myOrchContract'; +const contractFile = nodeRequire.resolve('../src/my.contract.ts'); +type StartFn = typeof import('../src/my.contract.ts').start; + +test('start my orch contract', async t => { + const common = await commonSetup(t); + const { zoe, bundleAndInstall } = await setUpZoeForTest(); + t.log('contract deployment', contractName); + + const installation: Installation = + await bundleAndInstall(contractFile); + t.is(passStyleOf(installation), 'remotable'); + + const myKit = await E(zoe).startInstance( + installation, + {}, // issuers + {}, // terms + common.commonPrivateArgs, + ); + t.notThrows(() => + mustMatch( + myKit, + M.splitRecord({ + instance: M.remotable(), + publicFacet: M.remotable(), + creatorFacet: M.remotable(), + // ...others are not relevant here + }), + ), + ); + + const hookAddress = await E(myKit.publicFacet).getHookAddress(); + t.log('hookAddress', hookAddress); + t.notThrows(() => mustMatch(hookAddress, ChainAddressShape)); + + const { transferBridge } = common.mocks; + const deposit = async (coins: CoinSDKType) => { + const target = 'agoric1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqp7zqht'; // TODO: where does this come from? + await VE(transferBridge).fromBridge( + buildVTransferEvent({ + receiver: 'rx1...TODO', + target, + sourceChannel: 'channel-1', // TODO: hubToAg.transferChannel.counterPartyChannelId, + denom: coins.denom, + amount: Nat(BigInt(coins.amount)), + sender: 'cosmos1xyz', + }), + ); + await eventLoopIteration(); // let contract do work + }; + + await t.notThrowsAsync(deposit({ amount: '10000000', denom: 'uatom' })); +}); diff --git a/packages/orch-skel/test/my-orch-seq-sim.test.ts b/packages/orch-skel/test/my-orch-seq-sim.test.ts new file mode 100644 index 00000000000..898c4e42883 --- /dev/null +++ b/packages/orch-skel/test/my-orch-seq-sim.test.ts @@ -0,0 +1,147 @@ +/** + * @file Orchestration Contract template: Test simulation from sequence diagram. + * + * For each (kind of) actor / participant in the diagram (my-orch-sequence.mmd), + * we have a function to make one. + * + * Each arrow in the diagram represents a method call on the receiving object. + */ +import test from 'ava'; +import type { ExecutionContext as Ex } from 'ava'; + +const { freeze } = Object; + +type Coins = { denom: string; amount: number }[]; // XXX rough +type PFM = { to: string; action?: string } | undefined; + +const makeStrideAccount = (addr: string) => { + const base = makeCosmosAccount(addr); + return freeze({ + ...base, + async deposit(t: Ex, amt: Coins, fwd?: PFM) { + await base.deposit(t, amt); + // After receiving ATOM, send ack and convert to stATOM + if (amt[0].denom === 'ATOM' && fwd?.action === 'Liquid Stake to stATOM') { + const strideAmt = [{ denom: 'stATOM', amount: amt[0].amount * 0.9 }]; + // Send stATOM to user's Elys account + await base.send(t, strideAmt, makeCosmosAccount(fwd.to)); + } + }, + }); +}; + +const makeCosmosAccount = (addr: string) => { + const self = { + toString: () => `<${addr}>`, + getAddress: () => addr, + async send( + t: Ex, + amt: Coins, + dest: ReturnType, + fwd?: PFM, + ) { + t.log( + addr, + 'sending', + amt, + 'to', + `${dest}`, + fwd ? `fwd: ${JSON.stringify(fwd)}` : '', + ); + dest.deposit(t, amt, fwd); + }, + async deposit(t: Ex, amt: Coins, fwd?: PFM) { + t.log(addr, 'received', amt, fwd ? `fwd: ${JSON.stringify(fwd)}` : ''); + // If we have forwarding instructions, do so + if (fwd) { + const { to } = fwd; + const dest = accountMaker(to)(to, 'stride123'); + await self.send(t, amt, dest); + } + }, + async getBalances(t: Ex): Promise { + t.log(addr, 'checking balances'); + return [{ denom: 'stATOM', amount: 9 }]; // Mock balance for demo + }, + }; + return freeze(self); +}; +type CosmosAccount = ReturnType; + +const accountMaker = (to: string) => { + if (to.startsWith('stride1')) return makeStrideAccount; + if (to.startsWith('agoric1')) return makeLocalOrchAccount; + return makeCosmosAccount; +}; + +const makeLocalOrchAccount = (addr: string, strideAddr: string) => { + const base = makeCosmosAccount(addr); + let tap = false; + const self = freeze({ + ...base, + async monitorTransfers() { + tap = true; + }, + async deposit(t: Ex, amt: Coins, fwd?: PFM) { + await base.deposit(t, amt, fwd); + if (tap) { + await self.receiveUpcall(t, amt, fwd); + } + }, + async receiveUpcall(t: Ex, amt: Coins, fwd?: PFM) { + t.log('orch hook received', amt); + // Send back to cosmos account first with forwarding instructions + await base.send(t, amt, makeStrideAccount(strideAddr), { + to: 'elys145', + action: 'Liquid Stake to stATOM', + }); + }, + }); + return self; +}; + +const makeOrchContract = async () => { + const strideAddr = 'stride123'; + const hookAcct = makeLocalOrchAccount('agoric1orchFEED', strideAddr); + await hookAcct.monitorTransfers(); + return freeze({ + getHookAccount: async () => hookAcct, + getStrideAddr: () => strideAddr, + }); +}; + +const makeUA = (orch: Awaited>) => { + const myAcct = makeCosmosAccount('cosmos1xyz'); + const hookAcctP = orch.getHookAccount(); + + const signAndBroadcast = async ( + t: Ex, + amt: Coins, + destAddr: string, + memo = '', + ) => { + if (destAddr !== myAcct.getAddress()) throw Error('unsupported'); + const acct = await hookAcctP; + return acct.deposit(t, amt); + }; + + const self = freeze({ + async openPosition(t: Ex, amt: Coins): Promise { + await signAndBroadcast(t, amt, await myAcct.getAddress()); + // Check final balance + const destAcct = makeCosmosAccount('elsy176'); + t.log('checking final balance at', `${destAcct}`); + const balance = await destAcct.getBalances(t); + t.log('final balance:', balance); + return balance; + }, + }); + return self; +}; + +test('user opens a position with 10 ATOM', async t => { + const orch = await makeOrchContract(); + const u1 = makeUA(orch); + const balance = await u1.openPosition(t, [{ denom: 'ATOM', amount: 10 }]); + t.deepEqual(balance, [{ denom: 'stATOM', amount: 9 }]); +}); diff --git a/packages/orch-skel/test/my-orch-sequence.mmd b/packages/orch-skel/test/my-orch-sequence.mmd new file mode 100644 index 00000000000..47b7e9142cc --- /dev/null +++ b/packages/orch-skel/test/my-orch-sequence.mmd @@ -0,0 +1,53 @@ +%%{init: { + 'theme': 'base', + 'themeVariables': { + 'primaryColor': '#f0f8ff', + 'primaryTextColor': '#2c3e50', + 'primaryBorderColor': '#7fb2e6', + 'lineColor': '#7fb2e6', + 'secondaryColor': '#f6f8fa', + 'tertiaryColor': '#fff5e6' + } +}}%% +sequenceDiagram + title Elys Orchestration Flow - Transactional Model + autonumber + actor webUA as Elys WebApp
[Browser] + %% [Where it runs] + box Aqua Cosmos Chain + participant acctOrig as User Acct + end + box Red Agoric Chain + participant myOrch as Orch. Contract + participant orchLCA1 as agoric1orchFEED + end + box Yellow as Stride Chain + participant chainLS as Stride Chain + end + box Grey Elys Chain + participant chainMy as Elys Chain + participant ICA1 as ICA 145
controlled by Agoric contract + participant acctDest as User Account
elsy176 + end + + %% Notation: ->> for initial message, -->> for consequences + + note left of myOrch: contract starts + myOrch ->> myOrch: makeLocalAccount() + myOrch ->> orchLCA1: monitorTranfers(...) + myOrch ->> chainMy: makeAccount() + chainMy -->> myOrch: elys145... + + note right of webUA: User Initiates Action + webUA ->> webUA: openPosition(10 ATOM) + webUA -->> acctOrig: send(10 ATOM, agoric1orchFEED) + acctOrig -->> orchLCA1: receiveUpcall(10 ATOM) + orchLCA1 -->> acctOrig: send(10 ATOM, stride123...,
fwd: to stATOM, send to ICA 145 on Elys) + acctOrig -->> chainLS: deposit(10 ATOM, to stATOM, fwd: to ICA 145 on Elys) + chainLS -->> ICA1: deposit(10 ATOM) + ICA1 -->> orchLCA1: resolve(ack) + orchLCA1 -->> ICA1: send(9 stATOM, elsy176) + ICA1 -->> acctDest: deposit(9 stATOM) + webUA -->> acctDest: getBalances() + acctDest -->> webUA: resolve(9 stATOM) + note right of webUA: User notified that execution
is complete and position is created diff --git a/packages/orch-skel/test/supports.ts b/packages/orch-skel/test/supports.ts new file mode 100644 index 00000000000..b6c64c7d38f --- /dev/null +++ b/packages/orch-skel/test/supports.ts @@ -0,0 +1,304 @@ +import { makeIssuerKit } from '@agoric/ertp'; +import { VTRANSFER_IBC_EVENT } from '@agoric/internal/src/action-types.js'; +import { + defaultSerializer, + makeFakeStorageKit, +} from '@agoric/internal/src/storage-test-utils.js'; +import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; +import { + denomHash, + withChainCapabilities, + type CosmosChainInfo, + type Denom, +} from '@agoric/orchestration'; +import { registerKnownChains } from '@agoric/orchestration/src/chain-info.js'; +import { + makeChainHub, + type DenomDetail, +} from '@agoric/orchestration/src/exos/chain-hub.js'; +import { prepareCosmosInterchainService } from '@agoric/orchestration/src/exos/cosmos-interchain-service.js'; +import fetchedChainInfo from '@agoric/orchestration/src/fetched-chain-info.js'; +import { setupFakeNetwork } from '@agoric/orchestration/test/network-fakes.js'; +import { buildVTransferEvent } from '@agoric/orchestration/tools/ibc-mocks.js'; +import { makeTestAddress } from '@agoric/orchestration/tools/make-test-address.js'; +import { reincarnate } from '@agoric/swingset-liveslots/tools/setup-vat-data.js'; +import { makeNameHubKit } from '@agoric/vats'; +import { prepareBridgeTargetModule } from '@agoric/vats/src/bridge-target.js'; +import { makeWellKnownSpaces } from '@agoric/vats/src/core/utils.js'; +import { prepareLocalChainTools } from '@agoric/vats/src/localchain.js'; +import { prepareTransferTools } from '@agoric/vats/src/transfer.js'; +import { makeFakeBankManagerKit } from '@agoric/vats/tools/bank-utils.js'; +import { makeFakeBoard } from '@agoric/vats/tools/board-utils.js'; +import { + makeFakeLocalchainBridge, + makeFakeTransferBridge, +} from '@agoric/vats/tools/fake-bridge.js'; +import { prepareSwingsetVowTools } from '@agoric/vow/vat.js'; +import type { Installation } from '@agoric/zoe/src/zoeService/utils.js'; +import { buildZoeManualTimer } from '@agoric/zoe/tools/manualTimer.js'; +import { withAmountUtils } from '@agoric/zoe/tools/test-utils.js'; +import { makeHeapZone, type Zone } from '@agoric/zone'; +import { makeDurableZone } from '@agoric/zone/durable.js'; +import { E } from '@endo/far'; +import type { ExecutionContext } from 'ava'; +import cctpChainInfo from '@agoric/orchestration/src/cctp-chain-info.js'; +import { objectMap } from '@endo/patterns'; +import type { ChainHubChainInfo } from '@agoric/fast-usdc/src/types.js'; +import { CosmosChainInfoShapeV1 } from '@agoric/fast-usdc/src/type-guards.js'; +import { makeTestFeeConfig } from './mocks.js'; + +export { + makeFakeLocalchainBridge, + makeFakeTransferBridge, +} from '@agoric/vats/tools/fake-bridge.js'; + +const assetOn = ( + baseDenom: Denom, + baseName: string, + chainName?: string, + infoOf?: Record, + brandKey?: string, +): [string, DenomDetail & { brandKey?: string }] => { + if (!chainName) { + return [baseDenom, { baseName, chainName: baseName, baseDenom }]; + } + if (!infoOf) throw Error(`must provide infoOf`); + const issuerInfo = infoOf[baseName]; + const holdingInfo = infoOf[chainName]; + if (!holdingInfo) throw Error(`${chainName} missing`); + if (!holdingInfo.connections) + throw Error(`connections missing for ${chainName}`); + const { channelId } = + holdingInfo.connections[issuerInfo.chainId].transferChannel; + const denom = `ibc/${denomHash({ denom: baseDenom, channelId })}`; + return [denom, { baseName, chainName, baseDenom, brandKey }]; +}; + +export const [uusdcOnAgoric, agUSDCDetail] = assetOn( + 'uusdc', + 'noble', + 'agoric', + fetchedChainInfo, + 'USDC', +); + +export const commonSetup = async (t: ExecutionContext) => { + t.log('bootstrap vat dependencies'); + // The common setup cannot support a durable zone because many of the fakes are not durable. + // They were made before we had durable kinds (and thus don't take a zone or baggage). + // To test durability in unit tests, test a particular entity with `relaxDurabilityRules: false`. + // To test durability integrating multiple vats, use a RunUtils/bootstrap test. + const rootZone = makeHeapZone(); + + const { nameHub: agoricNames, nameAdmin: agoricNamesAdmin } = + makeNameHubKit(); + + const usdc = withAmountUtils(makeIssuerKit('USDC')); + const bankBridgeMessages = [] as any[]; + const { bankManager, pourPayment } = await makeFakeBankManagerKit({ + onToBridge: obj => bankBridgeMessages.push(obj), + }); + await E(bankManager).addAsset( + uusdcOnAgoric, + 'USDC', + 'USD Circle Stablecoin', + usdc.issuerKit, + ); + // These mints no longer stay in sync with bankManager. + // Use pourPayment() for IST. + const { mint: _i, ...usdcSansMint } = usdc; + // XXX real bankManager does this. fake should too? + // TODO https://github.com/Agoric/agoric-sdk/issues/9966 + await makeWellKnownSpaces(agoricNamesAdmin, t.log, ['vbankAsset']); + await E(E(agoricNamesAdmin).lookupAdmin('vbankAsset')).update( + uusdcOnAgoric, + /** @type {AssetInfo} */ harden({ + brand: usdc.brand, + issuer: usdc.issuer, + issuerName: 'USDC', + denom: 'uusdc', + proposedName: 'USDC', + displayInfo: { IOU: true }, + }), + ); + + const vowTools = prepareSwingsetVowTools(rootZone.subZone('vows')); + + const transferBridge = makeFakeTransferBridge(rootZone); + const { makeBridgeTargetKit } = prepareBridgeTargetModule( + rootZone.subZone('bridge'), + ); + const { makeTransferMiddlewareKit } = prepareTransferTools( + rootZone.subZone('transfer'), + vowTools, + ); + + const { finisher, interceptorFactory, transferMiddleware } = + makeTransferMiddlewareKit(); + const bridgeTargetKit = makeBridgeTargetKit( + transferBridge, + VTRANSFER_IBC_EVENT, + interceptorFactory, + ); + finisher.useRegistry(bridgeTargetKit.targetRegistry); + await E(transferBridge).initHandler(bridgeTargetKit.bridgeHandler); + + const localBridgeMessages = [] as any[]; + const localchainBridge = makeFakeLocalchainBridge( + rootZone, + obj => localBridgeMessages.push(obj), + makeTestAddress, + ); + const localchain = prepareLocalChainTools( + rootZone.subZone('localchain'), + vowTools, + ).makeLocalChain({ + bankManager, + system: localchainBridge, + transfer: transferMiddleware, + }); + const timer = buildZoeManualTimer(t.log); + const marshaller = makeFakeBoard().getPublishingMarshaller(); + const storage = makeFakeStorageKit( + 'fun', // Fast USDC Node + ); + /** + * Read pure data (CapData that has no slots) from the storage path + * @param path + */ + storage.getDeserialized = (path: string): unknown => + storage.getValues(path).map(defaultSerializer.parse); + + const { portAllocator, setupIBCProtocol, ibcBridge } = setupFakeNetwork( + rootZone.subZone('network'), + { vowTools }, + ); + await setupIBCProtocol(); + + const makeCosmosInterchainService = prepareCosmosInterchainService( + rootZone.subZone('orchestration'), + vowTools, + ); + const cosmosInterchainService = makeCosmosInterchainService({ + portAllocator, + }); + + await registerKnownChains(agoricNamesAdmin, () => {}); + + let ibcSequenceNonce = 0n; + /** simulate incoming message as if the transfer completed over IBC */ + const transmitTransferAck = async () => { + // assume this is called after each outgoing IBC transfer + ibcSequenceNonce += 1n; + // let the promise for the transfer start + await eventLoopIteration(); + const lastMsgTransfer = localBridgeMessages.at(-1).messages[0]; + await E(transferBridge).fromBridge( + buildVTransferEvent({ + receiver: lastMsgTransfer.receiver, + sender: lastMsgTransfer.sender, + target: lastMsgTransfer.sender, + sourceChannel: lastMsgTransfer.sourceChannel, + sequence: ibcSequenceNonce, + denom: lastMsgTransfer.token.denom, + amount: BigInt(lastMsgTransfer.token.amount), + }), + ); + // let the bridge handler finish + await eventLoopIteration(); + }; + + /** A chainHub for Exo tests, distinct from the one a contract makes within `withOrchestration` */ + const chainHub = makeChainHub( + rootZone.subZone('chainHub'), + agoricNames, + vowTools, + { + chainInfoValueShape: CosmosChainInfoShapeV1, + }, + ); + + const chainInfo = harden(() => { + const { agoric, osmosis, noble } = withChainCapabilities(fetchedChainInfo); + const { ethereum, solana } = objectMap(cctpChainInfo, v => ({ + ...v, + // for backwards compatibility with `CosmosChainInfoShapeV1` which expects a `chainId` + chainId: `${v.namespace}:${v.reference}`, + })); + return { + agoric, + osmosis, + noble, + ethereum, + solana, + } as Record; + })(); + + const assetInfo: [Denom, DenomDetail & { brandKey?: string }][] = harden([ + assetOn('uusdc', 'noble'), + [uusdcOnAgoric, agUSDCDetail], + assetOn('uusdc', 'noble', 'osmosis', fetchedChainInfo), + ]); + + return { + bootstrap: { + agoricNames, + agoricNamesAdmin, + bankManager, + timer, + localchain, + cosmosInterchainService, + storage, + }, + brands: { + usdc: usdcSansMint, + }, + mocks: { + ibcBridge, + transferBridge, + }, + commonPrivateArgs: { + agoricNames, + localchain, + orchestrationService: cosmosInterchainService, + storageNode: storage.rootNode, + poolMetricsNode: storage.rootNode.makeChildNode('poolMetrics'), + marshaller, + timerService: timer, + feeConfig: makeTestFeeConfig(usdc), + chainInfo, + assetInfo, + }, + facadeServices: { + agoricNames, + /** A chainHub for Exo tests, distinct from the one a contract makes within `withOrchestration` */ + chainHub, + localchain, + orchestrationService: cosmosInterchainService, + timerService: timer, + }, + utils: { + contractZone: rootZone.subZone('contract'), + pourPayment, + inspectLocalBridge: () => harden([...localBridgeMessages]), + inspectDibcBridge: () => E(ibcBridge).inspectDibcBridge(), + inspectBankBridge: () => harden([...bankBridgeMessages]), + rootZone, + transmitTransferAck, + vowTools, + }, + }; +}; + +export const makeDefaultContext = (contract: Installation) => {}; + +/** + * Reincarnate without relaxDurabilityRules and provide a durable zone in the incarnation. + * @param key + */ +export const provideDurableZone = (key: string): Zone => { + const { fakeVomKit } = reincarnate({ relaxDurabilityRules: false }); + const root = fakeVomKit.cm.provideBaggage(); + const zone = makeDurableZone(root); + return zone.subZone(key); +}; diff --git a/packages/orch-skel/tsconfig.json b/packages/orch-skel/tsconfig.json new file mode 100644 index 00000000000..56bae8d03b0 --- /dev/null +++ b/packages/orch-skel/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": {}, + "include": [ + "src", + "test", + "testing" + ], +} diff --git a/packages/orchestration/src/orchestration-api.ts b/packages/orchestration/src/orchestration-api.ts index 225840d0e95..c05fffd533a 100644 --- a/packages/orchestration/src/orchestration-api.ts +++ b/packages/orchestration/src/orchestration-api.ts @@ -234,10 +234,18 @@ export interface OrchestrationAccountCommon { */ getAddress: () => CosmosChainAddress; - /** @returns an array of amounts for every balance in the account. */ + /** + * @returns an array of amounts for every balance in the account. + * + * @throws when prohibited (see `icqEnabled` in {@link CosmosChainInfo}) + */ getBalances: () => Promise; - /** @returns the balance of a specific denom for the account. */ + /** + * @returns the balance of a specific denom for the account. + * + * @throws when prohibited (see `icqEnabled` in {@link CosmosChainInfo}) + */ getBalance: (denom: DenomArg) => Promise; /** diff --git a/packages/orchestration/src/typeGuards.js b/packages/orchestration/src/typeGuards.js index ec13eb4b9f5..3f2fda69ad6 100644 --- a/packages/orchestration/src/typeGuards.js +++ b/packages/orchestration/src/typeGuards.js @@ -243,11 +243,11 @@ export const AnyNatAmountsRecord = M.and( /** @type {TypedPattern} */ export const OrchestrationPowersShape = { - agoricNames: M.remotable(), - localchain: M.remotable(), - orchestrationService: M.remotable(), - storageNode: M.remotable(), - timerService: M.remotable(), + agoricNames: M.remotable('agoricNames'), + localchain: M.remotable('localchain'), + orchestrationService: M.remotable('orchestrationService'), + storageNode: M.remotable('storageNode'), + timerService: M.remotable('timerService'), }; harden(OrchestrationPowersShape); diff --git a/packages/zoe/tools/setup-zoe.js b/packages/zoe/tools/setup-zoe.js index 244b479a468..8cd6a4e0a3e 100644 --- a/packages/zoe/tools/setup-zoe.js +++ b/packages/zoe/tools/setup-zoe.js @@ -78,7 +78,7 @@ export const setUpZoeForTest = async ({ ); // Copy all the properties so this object can be hardened. const exports = { ...pathOrExports }; - return bundleTestExports(exports); + return /** @type TestBundle */ (bundleTestExports(exports)); } };