-
Notifications
You must be signed in to change notification settings - Fork 245
docs: orchestration contract package skeleton #11323
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
base: master
Are you sure you want to change the base?
Changes from all commits
959f352
fc00e20
e70125d
e7bfe84
7f3b282
a89e338
f5f3298
2f5acc2
90a549f
167b603
4d6c1f8
bab4e4a
75637f5
b3eee19
9a2350c
5d12e4c
98f4771
c586fdb
9f8ab32
2c0606f
72958c9
204433c
53b03a7
d0b7e25
954902f
5f5c743
4b84cac
10ebbce
1cb70e7
0d21ee5
6fc135b
a748a22
a26f486
3dbd7bd
be9e42c
8788939
e55f982
4445598
bb45c59
a525ef0
aa072de
fb1f245
84f86c0
8a774e2
890fe52
8f3b65a
674bbf0
b9e6d53
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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:*", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in a new repo, |
||
"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", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @turadg when I use a new repository, I get a bunch of...
I guess I could use |
||
"@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" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<void>(); | ||
const poolAccountTransferPK = makePromiseKit<void>(); | ||
const settleAccountTransferPK = makePromiseKit<void>(); | ||
const settleAccountSendPK = makePromiseKit<void>(); | ||
const intermediateAccountTransferPK = makePromiseKit<void>(); | ||
const intermediateAccountDepositForBurnPK = makePromiseKit<void>(); | ||
|
||
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<typeof makeTestLogger>; | ||
|
||
export const makeTestFeeConfig = (usdc: Omit<AmountUtils, 'mint'>): FeeConfig => | ||
harden({ | ||
flat: usdc.make(1n), | ||
variableRate: makeRatio(2n, usdc.brand), | ||
contractRate: makeRatio(20n, usdc.brand), | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<StartFn> = | ||
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' })); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in a new repo,
yarn test
says: