From a04f8e625eae3c76fd2704e092ca7a9c08f11916 Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Sat, 9 Nov 2024 12:08:31 -0800 Subject: [PATCH] feat(vats): Mock ERTP face on mock orch assets --- .../mock-ertp/internal-typeGuards.js | 60 ++++ .../mock-ertp/internal-types.ts | 48 +++ .../src/mock-orch-ertp/mock-ertp/mock-ertp.js | 297 ++++++++++++++++++ .../mock-orch-ertp/mock-ertp/typeGuards.js | 59 ++++ .../src/mock-orch-ertp/mock-ertp/types.ts | 111 +++++++ .../mock-orch/internal-typeGuards.js | 55 ++++ .../mock-orch/internal-types.ts | 43 +++ .../src/mock-orch-ertp/mock-orch/mock-orch.js | 236 ++++++++++++++ .../mock-orch-ertp/mock-orch/typeGuards.js | 65 ++++ .../src/mock-orch-ertp/mock-orch/types.ts | 61 ++++ .../src/mock-orch-ertp/notes.txt | 43 +++ .../mock-orch/mock-orch.test.js | 53 ++++ 12 files changed, 1131 insertions(+) create mode 100644 packages/orchestration/src/mock-orch-ertp/mock-ertp/internal-typeGuards.js create mode 100644 packages/orchestration/src/mock-orch-ertp/mock-ertp/internal-types.ts create mode 100644 packages/orchestration/src/mock-orch-ertp/mock-ertp/mock-ertp.js create mode 100644 packages/orchestration/src/mock-orch-ertp/mock-ertp/typeGuards.js create mode 100644 packages/orchestration/src/mock-orch-ertp/mock-ertp/types.ts create mode 100644 packages/orchestration/src/mock-orch-ertp/mock-orch/internal-typeGuards.js create mode 100644 packages/orchestration/src/mock-orch-ertp/mock-orch/internal-types.ts create mode 100644 packages/orchestration/src/mock-orch-ertp/mock-orch/mock-orch.js create mode 100644 packages/orchestration/src/mock-orch-ertp/mock-orch/typeGuards.js create mode 100644 packages/orchestration/src/mock-orch-ertp/mock-orch/types.ts create mode 100644 packages/orchestration/src/mock-orch-ertp/notes.txt create mode 100644 packages/orchestration/test/mock-orch-ertp/mock-orch/mock-orch.test.js diff --git a/packages/orchestration/src/mock-orch-ertp/mock-ertp/internal-typeGuards.js b/packages/orchestration/src/mock-orch-ertp/mock-ertp/internal-typeGuards.js new file mode 100644 index 00000000000..9d1915e5485 --- /dev/null +++ b/packages/orchestration/src/mock-orch-ertp/mock-ertp/internal-typeGuards.js @@ -0,0 +1,60 @@ +import { M } from '@endo/patterns'; + +import { AmountShape, PaymentShape } from '@agoric/ertp'; +import { AnyNatAmountShape } from '../../typeGuards.js'; +import { mockIssuerInterfaces } from './typeGuards.js'; +import { MockOrchAccountShape } from '../mock-orch/typeGuards.js'; + +export const RecoverySetShape = M.remotable('RecoverySet'); +export const RecoveryFacetShape = M.remotable('RecoverFacet'); + +export const PaymentLedgerEntryShape = harden({ + keyShape: PaymentShape, + valueShape: AnyNatAmountShape, +}); + +export const PaymentRecoveryEntryShape = harden({ + keyShape: PaymentShape, + valueShape: RecoveryFacetShape, +}); + +/** + * @param {Pattern} [brandShape] + * @param {Pattern} [assetKindShape] + * @param {Pattern} [amountShape] + */ +export const mockIssuerInterfacesPlus = ( + brandShape = undefined, + assetKindShape = undefined, + amountShape = AmountShape, +) => { + const { + IssuerI, + MintI, + PaymentI, + PurseIKit: MockPurseIKit, + } = mockIssuerInterfaces(brandShape, assetKindShape, amountShape); + + const RecoveryFacetI = M.interface('RecoveryFacet', { + initPayment: M.call(PaymentShape).returns(), + deletePayment: M.call(PaymentShape).returns(), + getRecoverySetStore: M.call().returns(RecoverySetShape), + getCurrentEncumberedBalance: M.call().returns(amountShape), + encumber: M.call(amountShape).returns(), + unencumber: M.call(amountShape).returns(), + getOrchAcct: M.call().returns(MockOrchAccountShape), + }); + + const MockPurseIKitPlus = { + ...MockPurseIKit, + recoveryFacet: RecoveryFacetI, + }; + + return harden({ + IssuerI, + MintI, + PaymentI, + PurseIKit: MockPurseIKitPlus, + }); +}; +harden(mockIssuerInterfacesPlus); diff --git a/packages/orchestration/src/mock-orch-ertp/mock-ertp/internal-types.ts b/packages/orchestration/src/mock-orch-ertp/mock-ertp/internal-types.ts new file mode 100644 index 00000000000..155c37f36ae --- /dev/null +++ b/packages/orchestration/src/mock-orch-ertp/mock-ertp/internal-types.ts @@ -0,0 +1,48 @@ +import type { WeakMapStore, SetStore } from '@agoric/store'; + +import type { Payment, Amount } from '@agoric/ertp'; +import type { MockOrchAccount } from '../mock-orch/types.ts'; + +export type PaymentLedgerMap = WeakMapStore; + +export type RecoverySet = SetStore; + +export type RecoveryFacet = { + /** + * + */ + initPayment: (payment: Payment) => void; + + /** + * + */ + deletePayment: (payment: Payment) => void; + + /** + * Awkward name because `getRecoverSet` is already a method of purse that + * return a copySet. + */ + getRecoverySetStore: () => RecoverySet; + + /** + * Get the amount contained all payments still in the recoverySet at this moment + */ + getCurrentEncumberedBalance: () => Amount; + + /** + * + */ + encumber: (amount: Amount) => void; + + /** + * + */ + unencumber: (amount: Amount) => void; + + /** + * + */ + getOrchAcct: () => MockOrchAccount; +}; + +export type PaymentRecoveryMap = WeakMapStore; diff --git a/packages/orchestration/src/mock-orch-ertp/mock-ertp/mock-ertp.js b/packages/orchestration/src/mock-orch-ertp/mock-ertp/mock-ertp.js new file mode 100644 index 00000000000..34e223318f3 --- /dev/null +++ b/packages/orchestration/src/mock-orch-ertp/mock-ertp/mock-ertp.js @@ -0,0 +1,297 @@ +import { Fail } from '@endo/errors'; +import { mustMatch } from '@endo/patterns'; + +import { AmountMath, BrandI, PaymentShape } from '@agoric/ertp'; +import { AnyNatAmountShape } from '../../typeGuards.js'; +import { + mockIssuerInterfacesPlus, + PaymentLedgerEntryShape, + PaymentRecoveryEntryShape, +} from './internal-typeGuards.js'; + +/** + * @import {Amplify} from '@endo/exo' + * @import {Zone} from '@agoric/zone' + * @import {PaymentLedgerMap} from './internal-types.js' + */ + +/** + * @param {Zone} zone + */ +export const prepareMockIssuerKit = zone => { + const makeWeakMapStore = zone.detached().weakMapStore; + const makeSetStore = zone.detached().setStore; + + const { + IssuerI, + MintI, + PaymentI, + PurseIKit: MockPurseIKitPlus, + } = mockIssuerInterfacesPlus(); + + const mockPayment = zone.exoClass( + 'MockPayment', + PaymentI, + brand => ({ brand }), + { + getAllegedBrand() { + return this.state.brand; + }, + }, + ); + + /** @type {Amplify} */ + let purseKitAmp; + + const mockPurseKit = zone.exoClassKit( + 'MockPurse', + MockPurseIKitPlus, + (orchAcct, denom, brand, paymentLedger, paymentRecoveryFacets) => { + const recoverySet = makeSetStore('recoverySet', { + keyShape: PaymentShape, + }); + return { + orchAcct, + denom, + brand, + paymentLedger, + paymentRecoveryFacets, + recoverySet, + currentEncumberedBalance: harden({ + brand, + value: 0n, + }), + }; + }, + { + purse: { + getAllegedBrand() { + return this.state.brand; + }, + async getCurrentFullBalance() { + const { orchAcct, denom, brand } = this.state; + const { value } = await orchAcct.getBalance(denom); + return harden({ brand, value }); + }, + getCurrentEncumberedBalance() { + return this.state.currentEncumberedBalance; + }, + async getCurrentUnencumberedBalance() { + const { purse } = this.facets; + const fullBalance = await purse.getCurrentFullBalance(); + const encBalance = purse.getCurrentEncumberedBalance(); + AmountMath.isGTE(fullBalance, encBalance) || + Fail`unencumbered balance is negative`; + return AmountMath.subtract(fullBalance, encBalance); + }, + // alias for getCurrentUnencumberedBalance + async getCurrentAmount() { + return this.facets.purse.getCurrentUnencumberedBalance(); + }, + async getCurrentAmountNotifier() { + Fail`Mock ertp does not implement amount/balance notifiers`; + }, + async deposit(_payment, _optAmountShape = undefined) { + // TODO + }, + async withdraw(amount) { + // No overdrawn prevention on withdraw. Only deposit. + // Surprising, but probably correct! + const { brand, paymentLedger } = this.state; + const { recoveryFacet } = this.facets; + + const payment = mockPayment(brand); + if (!AmountMath.isEmpty(amount)) { + recoveryFacet.initPayment(payment); + recoveryFacet.encumber(amount); + } + paymentLedger.init(payment, amount); + return payment; + }, + getDepositFacet() { + return this.facets.depositFacet; + }, + getRecoverySet() { + return this.state.recoverySet.snapshot(); + }, + recoverAll() { + const { + brand, + recoverySet, + currentEncumberedBalance: value, + } = this.state; + const { recoveryFacet } = this.facets; + for (const payment of recoverySet.keys()) { + recoveryFacet.deletePayment(payment); + } + this.state.currentEncumberedBalance = harden({ + brand, + value: 0n, + }); + return harden({ brand, value }); + }, + }, + depositFacet: { + async receive(payment, optAmountShape) { + return this.facets.purse.deposit(payment, optAmountShape); + }, + }, + recoveryFacet: { + initPayment(payment) { + const { paymentRecoveryFacets, recoverySet } = this.state; + const { recoveryFacet } = this.facets; + + recoverySet.add(payment); + paymentRecoveryFacets.init(payment, recoveryFacet); + }, + deletePayment(payment) { + const { paymentRecoveryFacets, recoverySet } = this.state; + + paymentRecoveryFacets.delete(payment); + recoverySet.delete(payment); + }, + getRecoverySetStore() { + return this.state.recoverySet; + }, + getCurrentEncumberedBalance() { + return this.state.currentEncumberedBalance; + }, + encumber(amount) { + const { currentEncumberedBalance: oldEncBalance } = this.state; + this.state.currentEncumberedBalance = AmountMath.add( + oldEncBalance, + amount, + ); + }, + unencumber(amount) { + const { currentEncumberedBalance: oldEncBalance } = this.state; + this.state.currentEncumberedBalance = AmountMath.subtract( + oldEncBalance, + amount, + ); + }, + getOrchAcct() { + return this.state.orchAcct; + }, + }, + }, + { + receiveAmplifier(a) { + purseKitAmp = a; + }, + }, + ); + + // @ts-expect-error TS thinks it is used before assigned, which is a hazard + // TS is correct to bring to our attention, since there is not enough static + // into to infer otherwise. + assert(purseKitAmp !== undefined); + + const mockIssuerKit = zone.exoClassKit( + 'MockIssuerKit', + { + brand: BrandI, + issuer: IssuerI, + mint: MintI, + }, + (orchAcct, denom) => { + // TODO The issuerKit should not be associated with an orchAcct + /** @type {PaymentLedgerMap} */ + const paymentLedger = makeWeakMapStore( + 'paymentLedger', + PaymentLedgerEntryShape, + ); + const paymentRecoveryFacets = makeWeakMapStore( + 'paymentRecoverFacets', + PaymentRecoveryEntryShape, + ); + return { + orchAcct, + denom, + paymentLedger, + paymentRecoveryFacets, + }; + }, + { + brand: { + isMyIssuer(allegedIssuer) { + return this.facets.issuer === allegedIssuer; + }, + getAllegedName() { + return this.state.denom; + }, + getAmountShape() { + return AnyNatAmountShape; + }, + }, + issuer: { + getBrand() { + return this.facets.brand; + }, + getAllegedName() { + return this.state.denom; + }, + getAssetKind() { + return 'nat'; + }, + getDisplayInfo() { + Fail`mock ertp does not implement deprecated getDisplayInfo`; + }, + makeEmptyPurse() { + // TODO each purse should somehow be associated with its own explicit + // orchAcct + const { orchAcct, denom, paymentLedger, paymentRecoveryFacets } = + this.state; + const { brand } = this.facets; + const { purse } = mockPurseKit( + orchAcct, + denom, + brand, + paymentLedger, + paymentRecoveryFacets, + ); + return purse; + }, + isLive(payment) { + return this.state.paymentLedger.has(payment); + }, + getAmountOf(payment) { + return this.state.paymentLedger.get(payment); + }, + burn(payment, optAmountShape = undefined) { + const { paymentLedger, paymentRecoveryFacets } = this.state; + + const amount = paymentLedger.get(payment); + if (optAmountShape !== undefined) { + mustMatch(amount, optAmountShape); + } + if (paymentRecoveryFacets.has(payment)) { + const paymentRecoveryFacet = paymentRecoveryFacets.get(payment); + paymentRecoveryFacet.unencumber(amount); + paymentRecoveryFacet.deletePayment(payment); + // TODO must all burn the same orch denom assets + // so the fullBalance goes down and the unencumbered balance + // eventually stays the same. + // Without this, the unencumbered balance will go up, which is + // completely broken. + } + }, + }, + mint: { + getIssuer() { + return this.facets.issuer; + }, + mintPayment(newAmount) { + // The mock implementation has no mintRecoveryPurse or any recovery + // facet associated with minted payments. + const { paymentLedger } = this.state; + const payment = mockPayment(newAmount.brand); + paymentLedger.init(payment, newAmount); + return payment; + }, + }, + }, + ); + return mockIssuerKit; +}; +harden(prepareMockIssuerKit); diff --git a/packages/orchestration/src/mock-orch-ertp/mock-ertp/typeGuards.js b/packages/orchestration/src/mock-orch-ertp/mock-ertp/typeGuards.js new file mode 100644 index 00000000000..518bd5a8694 --- /dev/null +++ b/packages/orchestration/src/mock-orch-ertp/mock-ertp/typeGuards.js @@ -0,0 +1,59 @@ +// @jessie-check + +import { M, getInterfaceGuardPayload } from '@endo/patterns'; + +import { + AmountPatternShape, + AmountShape, + makeIssuerInterfaces, + PaymentShape, +} from '@agoric/ertp'; + +/** + * @param {Pattern} [brandShape] + * @param {Pattern} [assetKindShape] + * @param {Pattern} [amountShape] + */ +export const mockIssuerInterfaces = ( + brandShape = undefined, + assetKindShape = undefined, + amountShape = AmountShape, +) => { + const { + IssuerI, + MintI, + PaymentI, + PurseIKit: { purse: OriginalPurseI }, + } = makeIssuerInterfaces(brandShape, assetKindShape, amountShape); + + const { methodGuards: originalPurseMethodGuards } = + getInterfaceGuardPayload(OriginalPurseI); + + const MockPurseI = M.interface('MockPurse', { + ...originalPurseMethodGuards, + getCurrentFullBalance: M.call().returns(M.eref(amountShape)), + getCurrentEncumberedBalance: M.call().returns(amountShape), + getCurrentUnencumberedBalance: M.call().returns(M.eref(amountShape)), + getCurrentAmount: M.call().returns(M.eref(amountShape)), + deposit: M.call(PaymentShape) + .optional(AmountPatternShape) + .returns(M.eref(amountShape)), + }); + + const MockDepositFacetI = M.interface('MockDepositFacet', { + receive: getInterfaceGuardPayload(MockPurseI).methodGuards.deposit, + }); + + const MockPurseIKit = harden({ + purse: MockPurseI, + depositFacet: MockDepositFacetI, + }); + + return harden({ + IssuerI, + MintI, + PaymentI, + PurseIKit: MockPurseIKit, + }); +}; +harden(mockIssuerInterfaces); diff --git a/packages/orchestration/src/mock-orch-ertp/mock-ertp/types.ts b/packages/orchestration/src/mock-orch-ertp/mock-ertp/types.ts new file mode 100644 index 00000000000..038f1b3dbe6 --- /dev/null +++ b/packages/orchestration/src/mock-orch-ertp/mock-ertp/types.ts @@ -0,0 +1,111 @@ +import type { ERef } from '@endo/far'; +import type { CopySet, Key, Pattern } from '@endo/patterns'; +import type { LatestTopic } from '@agoric/notifier'; + +import type { Amount, Brand, Payment } from '@agoric/ertp'; + +export type MockDepositFacetReceive = ( + payment: Payment, + optAmountShape?: Pattern, +) => ERef; + +export type MockDepositFacet = { + /** + * Deposit all the contents of payment + * into the purse that made this facet, returning the amount. If the optional + * argument `optAmount` does not equal the amount of digital assets in the + * payment, throw an error. + * + * If payment is a promise, throw an error. + */ + receive: MockDepositFacetReceive; +}; + +/** + * Purses hold amount of + * digital assets of the same brand, but unlike Payments, they are not meant + * to be sent to others. To transfer digital assets, a Payment should be + * withdrawn from a Purse. The amount of digital assets in a purse can change + * through the action of deposit() and withdraw(). + */ +export type MockPurse = { + /** + * Get the alleged Brand for this + * Purse + */ + getAllegedBrand: () => Brand; + + /** + * Get the amount contained in + * this purse + all payments still in the recoverySet at this moment + */ + getCurrentFullBalance: () => ERef; + + /** + * Get the amount contained all payments still in the recoverySet at this moment + */ + getCurrentEncumberedBalance: () => Amount; + + /** + * Get the amount contained in + * this purse. + */ + getCurrentUnencumberedBalance: () => ERef; + + /** + * Get the amount contained in + * this purse. + */ + getCurrentAmount: () => ERef; + + /** + * Get a + * lossy notifier for changes to this purse's balance. + */ + getCurrentAmountNotifier: () => LatestTopic; + + /** + * Deposit all the contents of payment into this purse, returning the amount. If + * the optional argument `optAmount` does not equal the amount of digital + * assets in the payment, throw an error. + * + * If payment is a promise, throw an error. + */ + deposit: (payment: Payment, optAmountShape?: Pattern) => ERef; + + /** + * Return an object whose + * `receive` method deposits to the current Purse. + */ + getDepositFacet: () => MockDepositFacet; + + /** + * Withdraw amount + * from this purse into a new Payment. + */ + withdraw: (amount: Amount) => Payment; + + /** + * The set of payments + * withdrawn from this purse that are still live. These are the payments that + * can still be recovered in emergencies by, for example, depositing into this + * purse. Such a deposit action is like canceling an outstanding check because + * you're tired of waiting for it. Once your cancellation is acknowledged, you + * can spend the assets at stake on other things. Afterwards, if the recipient + * of the original check finally gets around to depositing it, their deposit + * fails. + * + * Returns an empty set if this issuer does not support recovery sets. + */ + getRecoverySet: () => CopySet; + + /** + * For use in emergencies, such as + * coming back from a traumatic crash and upgrade. This deposits all the + * payments in this purse's recovery set into the purse itself, returning the + * total amount of assets recovered. + * + * Returns an empty amount if this issuer does not support recovery sets. + */ + recoverAll: () => Amount; +}; diff --git a/packages/orchestration/src/mock-orch-ertp/mock-orch/internal-typeGuards.js b/packages/orchestration/src/mock-orch-ertp/mock-orch/internal-typeGuards.js new file mode 100644 index 00000000000..9867c8a6773 --- /dev/null +++ b/packages/orchestration/src/mock-orch-ertp/mock-orch/internal-typeGuards.js @@ -0,0 +1,55 @@ +import { M } from '@endo/patterns'; +import { DenomAmountShape, DenomShape } from '../../typeGuards.js'; +import { + AcctAddrValueShape, + ChainNameShape, + MockChainAcctAddrShape, + MockChainShape, + MockDenomInfoShape, + MockOrchAccountShape, +} from './typeGuards.js'; + +export const MockDenomMintShape = M.remotable('MockDenomMint'); +export const MockOrchAdminShape = M.remotable('MockOrchAdmin'); +export const BalancesMapShape = M.remotable('balancesMap'); +export const AccountsMapShape = M.remotable('accountsMap'); +export const ChainsMapShape = M.remotable('chainsMap'); +export const DenomsMapShape = M.remotable('denomsMap'); + +export const MockDenomMintI = M.interface('MockDenomMint', { + mintTo: M.call(MockChainAcctAddrShape, DenomAmountShape).returns( + M.eref(M.undefined()), + ), +}); + +export const MockOrchAdminI = M.interface('MockOrchAdmin', { + makeDenom: M.call(DenomShape, MockChainShape).returns( + M.eref(MockDenomMintShape), + ), +}); + +export const BalancesEntryShape = harden({ + keyShape: DenomShape, + valueShape: DenomAmountShape, +}); + +export const AccountsEntryShape = { + keyShape: AcctAddrValueShape, + valueShape: { + account: MockOrchAccountShape, + balances: BalancesMapShape, + }, +}; + +export const ChainsEntryShape = { + keyShape: ChainNameShape, + valueShape: { + chain: MockChainShape, + accounts: AccountsMapShape, + }, +}; + +export const DenomsEntryShape = { + keyShape: DenomShape, + valueShape: MockDenomInfoShape, +}; diff --git a/packages/orchestration/src/mock-orch-ertp/mock-orch/internal-types.ts b/packages/orchestration/src/mock-orch-ertp/mock-orch/internal-types.ts new file mode 100644 index 00000000000..1bedb4d4e6c --- /dev/null +++ b/packages/orchestration/src/mock-orch-ertp/mock-orch/internal-types.ts @@ -0,0 +1,43 @@ +import type { ERef } from '@endo/far'; +import type { MapStore } from '@agoric/store'; +import type { Denom, DenomAmount } from '../../orchestration-api.ts'; +import type { + MockChain, + MockDenomInfo, + MockChainAcctAddr, + AcctAddrValue, + MockOrchAccount, + ChainName, +} from './types.ts'; + +export type MockDenomMint = { + // like transfer, but with no src account + mintTo(destAddr: MockChainAcctAddr, denomAmount: DenomAmount): ERef; +}; + +/** + * Essentially, minting authority for mock testing purposes. + */ +export type MockOrchestratorAdmin = { + makeDenom(denom: Denom, chain: MockChain): ERef; +}; + +export type BalancesMap = MapStore; + +export type AccountsMap = MapStore< + AcctAddrValue, + { + account: MockOrchAccount; + balances: BalancesMap; + } +>; + +export type ChainsMap = MapStore< + ChainName, + { + chain: MockChain; + accounts: AccountsMap; + } +>; + +export type DenomsMap = MapStore; diff --git a/packages/orchestration/src/mock-orch-ertp/mock-orch/mock-orch.js b/packages/orchestration/src/mock-orch-ertp/mock-orch/mock-orch.js new file mode 100644 index 00000000000..595f7d960cd --- /dev/null +++ b/packages/orchestration/src/mock-orch-ertp/mock-orch/mock-orch.js @@ -0,0 +1,236 @@ +import { Fail, q } from '@endo/errors'; +import { provideLazy } from '@agoric/store'; + +import { + MockChainI, + MockOrchAccountI, + MockOrchestratorI, +} from './typeGuards.js'; +import { + AccountsEntryShape, + BalancesEntryShape, + ChainsEntryShape, + DenomsEntryShape, + MockDenomMintI, + MockOrchAdminI, +} from './internal-typeGuards.js'; + +// TODO declare less. infer more. + +/** + * @import {Zone} from '@agoric/base-zone' + * + * @import {Denom} from '../../orchestration-api.js' + * @import {AcctAddrValue, ChainName, MockChain, MockOrchAccount} from './types.js' + * @import {AccountsMap, ChainsMap, DenomsMap} from './internal-types.js'; + */ + +const decrBalance = (myBalances, denom, deltaValue) => { + const { value: myOldBalanceValue } = myBalances.get(denom); + // Would be really good to do this source check synchronously + // and in order. + // TODO is this realistic for the real orch? + myOldBalanceValue >= deltaValue || + Fail`overdrawn ${myOldBalanceValue} - ${deltaValue}`; + const myNewBalanceValue = myOldBalanceValue - deltaValue; + + // Would be really good to do this source update synchronously, in order + // TODO is this realistic for the real orch? + myBalances.set(denom, harden({ denom, value: myNewBalanceValue })); +}; + +const getDestBalances = (chains, destAddr) => { + const { chainId: destChainId, value: destAcctAddrValue } = destAddr; + const { accounts: destChainAccounts } = chains.get(destChainId); + const { balances: dBalances } = destChainAccounts.get(destAcctAddrValue); + return dBalances; +}; + +const restoreBalance = (myBalances, denom, deltaValue) => { + const { value: myNextOldBalanceValue } = myBalances.get(denom); + const myNextNewBalanceValue = myNextOldBalanceValue + deltaValue; + myBalances.set(denom, harden({ denom, value: myNextNewBalanceValue })); +}; + +const incrBalance = (destBalances, denom, deltaValue) => { + if (destBalances.has(denom)) { + const { value: destOldBalanceValue } = destBalances.get(denom); + const destNewBalanceValue = destOldBalanceValue + deltaValue; + destBalances.set(denom, harden({ denom, value: destNewBalanceValue })); + } else { + destBalances.init(denom, harden({ denom, value: deltaValue })); + } +}; + +/** + * @param {Zone} zone + */ +export const prepareMockOrchestratorKit = zone => { + const makeMapStore = zone.detached().mapStore; + + /** + * @type {( + * chains: ChainsMap, + * accounts: AccountsMap, + * chainId: ChainName, + * chain: MockChain, + * ) => MockOrchAccount} + */ + const mockOrchAccount = zone.exoClass( + 'MockOrchAccount', + MockOrchAccountI, + /** + * @param {ChainsMap} chains + * @param {AccountsMap} accounts + * @param {ChainName} chainId + * @param {MockChain} chain + */ + (chains, accounts, chainId, chain) => { + /** @type {AcctAddrValue} */ + const acctAddrValue = `${accounts.getSize()}`; + return { chains, accounts, chainId, chain, acctAddrValue }; + }, + { + getAddress() { + const { chainId, acctAddrValue } = this.state; + return harden({ chainId, value: acctAddrValue }); + }, + async getBalances() { + const { accounts, acctAddrValue } = this.state; + const { balances } = accounts.get(acctAddrValue); + return [...balances.values()]; + }, + async getBalance(denom) { + const { accounts, acctAddrValue } = this.state; + const { balances } = accounts.get(acctAddrValue); + return balances.get(denom); + }, + async transfer(destAddr, denomAmount) { + const { chains, accounts, acctAddrValue } = this.state; + const { balances: myBalances } = accounts.get(acctAddrValue); + const { denom, value: deltaValue } = denomAmount; + + decrBalance(myBalances, denom, deltaValue); + + await null; // sometime later... + + let destBalances; + try { + destBalances = getDestBalances(chains, destAddr); + } catch (err) { + // A failure at this point means the `denomAmount` is not yet spent + // at dest, and so should be restored to `myBalances`. + restoreBalance(myBalances, denom, deltaValue); + throw err; + } + incrBalance(destBalances, denom, deltaValue); + }, + }, + ); + + const makeMockChain = zone.exoClass( + 'MockChain', + MockChainI, + /** + * @param {ChainsMap} chains + * @param {DenomsMap} denoms + * @param {ChainName} chainName + */ + (chains, denoms, chainName) => ({ + chains, + denoms, + chainId: chainName, + }), + { + getChainInfo() { + const { chainId } = this.state; + return harden({ chainId }); + }, + async makeAccount() { + const { self } = this; + const { chains, chainId } = this.state; + const { accounts } = chains.get(chainId); + + const account = mockOrchAccount(chains, accounts, chainId, self); + const { value: acctAddrValue } = account.getAddress(); + accounts.init( + acctAddrValue, + harden({ + account, + balances: makeMapStore('balances', BalancesEntryShape), + }), + ); + return account; + }, + }, + ); + + const mockDenomMint = zone.exoClass( + 'MockDenomMint', + MockDenomMintI, + /** + * @param {ChainsMap} chains + * @param {DenomsMap} denoms + * @param {Denom} denom + */ + (chains, denoms, denom) => ({ + chains, + denoms, + denom, + }), + { + mintTo(destAddr, denomAmount) { + const { chains, denom } = this.state; + const { denom: sameDenom, value: deltaValue } = denomAmount; + denom === sameDenom || + Fail`${q(denom)} should be same as ${q(sameDenom)}`; + + const destBalances = getDestBalances(chains, destAddr); + incrBalance(destBalances, denom, deltaValue); + }, + }, + ); + + const mockOrchestratorKit = zone.exoClassKit( + 'MockOrchestrator', + { + orchestrator: MockOrchestratorI, + admin: MockOrchAdminI, + }, + () => { + /** @type {ChainsMap} */ + const chains = makeMapStore('chains', ChainsEntryShape); + /** @type {DenomsMap} */ + const denoms = makeMapStore('denoms', DenomsEntryShape); + return { chains, denoms }; + }, + { + orchestrator: { + getChain(chainName) { + const { chains, denoms } = this.state; + // @ts-expect-error typing problems with provideLazy + const { chain } = provideLazy(chains, chainName, cName => + harden({ + chain: makeMockChain(chains, denoms, cName), + accounts: makeMapStore('accounts', AccountsEntryShape), + }), + ); + return chain; + }, + getDenomInfo(denom) { + return this.state.denoms.get(denom); + }, + }, + admin: { + makeDenom(denom, chain) { + const { chains, denoms } = this.state; + // init will throw if `denom` name is already taken + denoms.init(denom, harden({ chain })); + return mockDenomMint(chains, denoms, denom); + }, + }, + }, + ); + return mockOrchestratorKit; +}; +harden(prepareMockOrchestratorKit); diff --git a/packages/orchestration/src/mock-orch-ertp/mock-orch/typeGuards.js b/packages/orchestration/src/mock-orch-ertp/mock-orch/typeGuards.js new file mode 100644 index 00000000000..524b76bfb61 --- /dev/null +++ b/packages/orchestration/src/mock-orch-ertp/mock-orch/typeGuards.js @@ -0,0 +1,65 @@ +// @jessie-check +import { M } from '@endo/patterns'; +import { + ChainInfoShape, + DenomAmountShape, + DenomShape, +} from '../../typeGuards.js'; + +export const MockOrchAccountShape = M.remotable('MockOrchAccount'); +export const MockChainShape = M.remotable('MockChain'); +export const MockOrchestratorShape = M.remotable('MockOrchestrator'); + +// copied from portfolio-holder-kit.js +export const ChainNameShape = M.string(); + +export const AcctAddrValueShape = M.string(); + +/** + * @see {ChainAddressShape} + * @see {MockChainAcctAddr} + */ +export const MockChainAcctAddrShape = harden({ + chainId: ChainNameShape, + value: AcctAddrValueShape, // acctAddrValue +}); + +/** + * @see {orchestrationAccountMethods} + * @see {MockOrchAccount} + */ +export const MockOrchAccountI = M.interface('MockOrchAccount', { + getAddress: M.call().returns(MockChainAcctAddrShape), + getBalances: M.call().returns(M.eref(M.arrayOf(DenomAmountShape))), + getBalance: M.call(DenomShape).returns(M.eref(DenomAmountShape)), + transfer: M.call(MockChainAcctAddrShape, DenomAmountShape).returns( + M.eref(M.undefined()), + ), +}); + +/** + * @see {DenomInfoShape} + * @see {MockDenomInfo} + */ +export const MockDenomInfoShape = harden({ + chain: MockChainShape, +}); + +/** + * @see {chainFacadeMethods} + * @see {MockChain} + * @see {OrchestratorI} + */ +export const MockChainI = M.interface('MockChain', { + getChainInfo: M.call().returns(M.eref(ChainInfoShape)), + makeAccount: M.call().returns(M.eref(MockOrchAccountShape)), +}); + +/** + * @see {OrchestratorI} + * @see {MockOrchestrator} + */ +export const MockOrchestratorI = M.interface('MockOrchestrator', { + getChain: M.call(ChainNameShape).returns(M.eref(MockChainShape)), + getDenomInfo: M.call(DenomShape).returns(MockDenomInfoShape), +}); diff --git a/packages/orchestration/src/mock-orch-ertp/mock-orch/types.ts b/packages/orchestration/src/mock-orch-ertp/mock-orch/types.ts new file mode 100644 index 00000000000..d626f1fac31 --- /dev/null +++ b/packages/orchestration/src/mock-orch-ertp/mock-orch/types.ts @@ -0,0 +1,61 @@ +import type { ERef } from '@endo/far'; + +import type { Denom, DenomAmount, ChainAddress } from '@agoric/orchestration'; + +export type ChainName = string; +export type AcctAddrValue = string; + +/** + * @see {ChainAddress}, which actually names an account on a chain. + * Like a widely-held deposit-facet. + */ +export type MockChainAcctAddr = { + chainId: ChainName; + value: AcctAddrValue; // acctAddrValue +}; + +/** + * @see {OrchestrationAccount} + */ +export type MockOrchAccount = { + getAddress(): MockChainAcctAddr; + getBalances(): ERef; + getBalance(denom: Denom): ERef; + transfer(destAddr: MockChainAcctAddr, denomAmount: DenomAmount): ERef; +}; + +/** + * @see {ChainInfo}, which is in the intersection of the existing + * `ChainInfo` possibilities. + */ +export type MockChainInfo = { + chainId: ChainName; +}; + +/** + * @see {DenomInfo} + */ +export type MockDenomInfo = { + // Omitting "brand" from DenomInfo in order to get clearer layering + // brand: Brand; + // eslint-disable-next-line no-use-before-define + chain: MockChain; +}; + +/** + * @see {Chain} + */ +export type MockChain = { + getChainInfo(): ERef; + makeAccount(): ERef; +}; + +/** + * @see {Orchestrator} + */ +export type MockOrchestrator = { + getChain(chainName: ChainName): ERef; + getDenomInfo(denom: Denom): MockDenomInfo; + // Omitting "asAmount" from Orchestrator in order to get clearer layering + // asAmount(denomAmount: DenomAmount): Amount; +}; diff --git a/packages/orchestration/src/mock-orch-ertp/notes.txt b/packages/orchestration/src/mock-orch-ertp/notes.txt new file mode 100644 index 00000000000..539b7a9e584 --- /dev/null +++ b/packages/orchestration/src/mock-orch-ertp/notes.txt @@ -0,0 +1,43 @@ +ub = unencumbered balance (purse "currentAmount") +eb = encumbered balance (total payments in recovery set) +fb = full balance = eb + ub (orch acct balance) + +s = source +d = dest + +ertp withdraw(a) + ub -= a + eb += a + fb unchanged + +ertp deposit(a) + sub unchanged + seb -= a + sfb -= a + once transfer succeeds + dub += a + deb unchanged + dfb += a + once transfer fails + sub += a + seb unchanged from above + sfb += a (net sfb unchanged) + +ertp burn(a) (like a successful deposit to dev/null) + ub unchanged + eb -= a + fb -= a + +ertp reclaim(a), like a successful self.deposit(a) + ub += a + eb -= a + fb unchanged + +mintTo(d, a), like succeed case of transfer + dub += a + deb unchanged + dfb += a + +if orch account can be independently spent from + ub can go negative, if fb < eb + payments can fail to be deposited diff --git a/packages/orchestration/test/mock-orch-ertp/mock-orch/mock-orch.test.js b/packages/orchestration/test/mock-orch-ertp/mock-orch/mock-orch.test.js new file mode 100644 index 00000000000..907fbb1a2cd --- /dev/null +++ b/packages/orchestration/test/mock-orch-ertp/mock-orch/mock-orch.test.js @@ -0,0 +1,53 @@ +import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; + +import { makeScalarBigMapStore } from '@agoric/vat-data'; +import { makeDurableZone } from '@agoric/zone/durable.js'; +import { prepareMockOrchestratorKit } from '../../../src/mock-orch-ertp/mock-orch/mock-orch.js'; + +test('mock orch chain', async t => { + const baggage = makeScalarBigMapStore('test-baggage', { durable: true }); + const zone = makeDurableZone(baggage); + const makeMockOrchestratorKit = prepareMockOrchestratorKit(zone); + const { orchestrator, admin } = makeMockOrchestratorKit(); + + const aChain = orchestrator.getChain('a'); + const { chainId: aChainId } = await aChain.getChainInfo(); + t.is(aChainId, 'a'); + const aliceAcct = await aChain.makeAccount(); + const { chainId: aliceChainId, value: aliceAcctAddrValue } = + aliceAcct.getAddress(); + t.is(aliceChainId, 'a'); + t.is(aliceAcctAddrValue, '0'); + const aliceBalances = await aliceAcct.getBalances(); + t.is(aliceBalances.length, 0); + + const bChain = orchestrator.getChain('b'); + const { chainId: bChainId } = await bChain.getChainInfo(); + t.is(bChainId, 'b'); + const bobAcct = await bChain.makeAccount(); + const { chainId: bobChainId, value: bobAcctAddrValue } = bobAcct.getAddress(); + t.is(bobChainId, 'b'); + t.is(bobAcctAddrValue, '0'); + const bobBalances = await bobAcct.getBalances(); + t.is(bobBalances.length, 0); + + const denomMint = admin.makeDenom('bucks', aChain); + await denomMint.mintTo( + aliceAcct.getAddress(), + harden({ + denom: 'bucks', + value: 300n, + }), + ); + t.deepEqual(await aliceAcct.getBalances(), [{ denom: 'bucks', value: 300n }]); + + await aliceAcct.transfer( + bobAcct.getAddress(), + harden({ + denom: 'bucks', + value: 100n, + }), + ); + t.deepEqual(await aliceAcct.getBalances(), [{ denom: 'bucks', value: 200n }]); + t.deepEqual(await bobAcct.getBalances(), [{ denom: 'bucks', value: 100n }]); +});