Skip to content

Commit

Permalink
10130 excise SharedStateRecord (#10140)
Browse files Browse the repository at this point in the history
closes: #10130


## Description

Provide examples of how a contract can have a shared local account, without the race condition of creating it conditionally in a flow.

In the course of this I corrected the `LocalOrchestrationAccount` method signatures (particularly `deposit` not returning the Amount).

### Security Considerations
none, just example code

### Scaling Considerations

none, example code

### Documentation Considerations
Examples are implicit documentation. Can be mined for docs site tutorials if the need arises.

### Testing Considerations
The upgrade test for `send-anywhere` is disabled, but since this just makes a vow there shouldn't be any upgrade interactions.

### Upgrade Considerations
The examples aren't to be deployed.

The remove of `sharedStateRecord` won't affect anything on chain. Same for changing the return of `deposit`.
  • Loading branch information
mergify[bot] authored Oct 2, 2024
2 parents 69f8e4b + 31e82eb commit 813e54c
Show file tree
Hide file tree
Showing 15 changed files with 152 additions and 125 deletions.
1 change: 0 additions & 1 deletion packages/async-flow/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export * from './src/async-flow.js';
export * from './src/types-index.js';
export { makeSharedStateRecord } from './src/endowments.js';
32 changes: 0 additions & 32 deletions packages/async-flow/src/endowments.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,38 +53,6 @@ export const forwardingMethods = rem => {
return fromEntries(keys.map(makeMethodEntry));
};

/**
* Given a possibly mutable (and therefore unhardened) record, return a
* corresponding state record that acts identically for normal
* gets and sets, but is implemented using accessors, so it will be recognized
* as a state record.
*
* @template { string | number | symbol } K
* @template {Record<K, any>} R
* @param {R} dataRecord
* @returns {R}
*/
export const makeSharedStateRecord = dataRecord =>
harden(
create(
objectPrototype,
fromEntries(
ownKeys(dataRecord).flatMap(key =>
entries(
getOwnPropertyDescriptors({
get [key]() {
return dataRecord[key];
},
set [key](newValue) {
dataRecord[key] = newValue;
},
}),
),
),
),
),
);

/**
* @param {Zone} outerZone
* @param {PreparationOptions} [outerOptions]
Expand Down
25 changes: 16 additions & 9 deletions packages/orchestration/src/examples/send-anywhere.contract.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { makeSharedStateRecord } from '@agoric/async-flow';

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 * as flows from './send-anywhere.flows.js';
import * as sharedFlows from './shared.flows.js';

/**
* @import {Vow} from '@agoric/vow';
Expand Down Expand Up @@ -38,23 +37,31 @@ export const contract = async (
zone,
{ chainHub, orchestrateAll, vowTools, zoeTools },
) => {
const contractState = makeSharedStateRecord(
/** @type {{ account: OrchestrationAccount<any> | undefined }} */ {
localAccount: undefined,
},
);

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, {
contractState,
log,
sharedLocalAccountP,
zoeTools,
});

Expand Down
23 changes: 12 additions & 11 deletions packages/orchestration/src/examples/send-anywhere.flows.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import { M, mustMatch } from '@endo/patterns';
/**
* @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, LocalAccountMethods, OrchestrationAccountI, OrchestrationFlow} from '../types.js';
* @import {Orchestrator, OrchestrationFlow, LocalAccountMethods} from '../types.js';
*/

const { entries } = Object;
Expand All @@ -18,15 +19,15 @@ const { entries } = Object;
* @satisfies {OrchestrationFlow}
* @param {Orchestrator} orch
* @param {object} ctx
* @param {{ localAccount?: OrchestrationAccountI & LocalAccountMethods }} ctx.contractState
* @param {Promise<GuestInterface<LocalOrchestrationAccountKit['holder']>>} ctx.sharedLocalAccountP
* @param {GuestInterface<ZoeTools>} ctx.zoeTools
* @param {GuestOf<(msg: string) => Vow<void>>} ctx.log
* @param {ZCFSeat} seat
* @param {{ chainName: string; destAddr: string }} offerArgs
*/
export const sendIt = async (
orch,
{ contractState, log, zoeTools: { localTransfer, withdrawToSeat } },
{ sharedLocalAccountP, log, zoeTools: { localTransfer, withdrawToSeat } },
seat,
offerArgs,
) => {
Expand All @@ -44,23 +45,23 @@ export const sendIt = async (
`${amt.brand} not registered in vbank`,
);

// FIXME racy
if (!contractState.localAccount) {
contractState.localAccount = await agoric.makeAccount();
}

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

await localTransfer(seat, contractState.localAccount, give);
/**
* @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`);

try {
await contractState.localAccount.transfer(
await sharedLocalAccount.transfer(
{
value: destAddr,
encoding: 'bech32',
Expand All @@ -70,7 +71,7 @@ export const sendIt = async (
);
void log(`completed transfer to ${destAddr}`);
} catch (e) {
await withdrawToSeat(contractState.localAccount, seat, give);
await withdrawToSeat(sharedLocalAccount, seat, give);
const errorMsg = `IBC Transfer failed ${q(e)}`;
void log(`ERROR: ${errorMsg}`);
seat.exit(errorMsg);
Expand Down
21 changes: 21 additions & 0 deletions packages/orchestration/src/examples/shared.flows.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* @file Flows shared by multiple examples
*
* A module with flows can be used be reused across multiple contracts. They are
* bound to a particular contract's context via orchestrateAll. See
* ./send-anywhere.contract.js for example usage.
*/
/**
* @import {Orchestrator, OrchestrationFlow, LocalAccountMethods} from '../types.js';
*/

/**
* @satisfies {OrchestrationFlow}
* @param {Orchestrator} orch
* @returns {Promise<LocalAccountMethods>}
*/
export const makeLocalAccount = async orch => {
const agoricChain = await orch.getChain('agoric');
return agoricChain.makeAccount();
};
harden(makeLocalAccount);
2 changes: 2 additions & 0 deletions packages/orchestration/src/examples/stake-bld.contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ export const start = async (zcf, privateArgs, baggage) => {
const { give } = seat.getProposal();
trace('makeStakeBldInvitation', give);
const { holder } = await makeLocalAccountKit();
/** @type {Record<string, Payment<'nat'>>} */
// @ts-expect-error XXX PaymentPKeywordRecord throught deeplyFulfilled will be a PaymnentKeywordRecord
const { In } = await deeplyFulfilled(
withdrawFromSeat(zcf, seat, give),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
* The primary offer result is a power for invitation makers that can perform
* actions with an ICA account.
*/
import { makeSharedStateRecord } from '@agoric/async-flow';
import { AmountShape } from '@agoric/ertp';
import { M } from '@endo/patterns';
import { prepareCombineInvitationMakers } from '../exos/combine-invitation-makers.js';
import { CosmosOrchestrationInvitationMakersI } from '../exos/cosmos-orchestration-account.js';
import { ChainAddressShape, DelegationShape } from '../typeGuards.js';
import { withOrchestration } from '../utils/start-helper.js';
import * as flows from './staking-combinations.flows.js';
import * as sharedFlows from './shared.flows.js';
import { prepareChainHubAdmin } from '../exos/chain-hub-admin.js';

/**
Expand Down Expand Up @@ -46,16 +46,6 @@ const contract = async (
zone,
{ orchestrateAll, zoeTools, chainHub },
) => {
const contractState = makeSharedStateRecord(
/**
* @type {{
* account: (OrchestrationAccount<any> & LocalAccountMethods) | undefined;
* }}
*/ {
localAccount: undefined,
},
);

const StakingCombinationsInvitationMakersI = M.interface(
'StakingCombinationsInvitationMakersI',
{
Expand Down Expand Up @@ -128,8 +118,22 @@ const contract = async (
StakingCombinationsInvitationMakersI,
);

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(),
);

const orchFns = orchestrateAll(flows, {
contractState,
sharedLocalAccountP,
makeCombineInvitationMakers,
makeExtraInvitationMaker,
flows,
Expand Down
21 changes: 12 additions & 9 deletions packages/orchestration/src/examples/staking-combinations.flows.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* @import {GuestInterface} from '@agoric/async-flow';
* @import {Orchestrator, OrchestrationFlow, AmountArg, CosmosValidatorAddress, ChainAddress, LocalAccountMethods, OrchestrationAccountI} from '../types.js'
* @import {ContinuingOfferResult, InvitationMakers} from '@agoric/smart-wallet/src/types.js';
* @import {LocalOrchestrationAccountKit} from '../exos/local-orchestration-account.js';
* @import {MakeCombineInvitationMakers} from '../exos/combine-invitation-makers.js';
* @import {CosmosOrchestrationAccount} from '../exos/cosmos-orchestration-account.js';
* @import {ResolvedContinuingOfferResult, ZoeTools} from '../utils/zoe-tools.js';
Expand Down Expand Up @@ -47,7 +48,7 @@ harden(makeAccount);
* @satisfies {OrchestrationFlow}
* @param {Orchestrator} orch
* @param {object} ctx
* @param {{ localAccount?: OrchestrationAccountI & LocalAccountMethods }} ctx.contractState
* @param {Promise<GuestInterface<LocalOrchestrationAccountKit['holder']>>} ctx.sharedLocalAccountP
* @param {GuestInterface<ZoeTools>} ctx.zoeTools
* @param {GuestInterface<CosmosOrchestrationAccount>} account
* @param {ZCFSeat} seat
Expand All @@ -56,26 +57,28 @@ harden(makeAccount);
*/
export const depositAndDelegate = async (
orch,
{ contractState, zoeTools },
{ sharedLocalAccountP, zoeTools },
account,
seat,
validator,
) => {
await null;
trace('depositAndDelegate', account, seat, validator);
mustMatch(validator, ChainAddressShape);
if (!contractState.localAccount) {
const agoricChain = await orch.getChain('agoric');
contractState.localAccount = await agoricChain.makeAccount();
}

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

const address = account.getAddress();
try {
await contractState.localAccount.transfer(address, give.Stake);
await sharedLocalAccount.transfer(address, give.Stake);
} catch (cause) {
await zoeTools.withdrawToSeat(contractState.localAccount, seat, give);
await zoeTools.withdrawToSeat(sharedLocalAccount, seat, give);
const errMsg = makeError(`ibc transfer failed ${q(cause)}`);
seat.exit(errMsg);
throw errMsg;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { coerceCoin, coerceDenomAmount } from '../utils/amounts.js';
/**
* @import {HostOf} from '@agoric/async-flow';
* @import {LocalChain, LocalChainAccount} from '@agoric/vats/src/localchain.js';
* @import {AmountArg, ChainAddress, DenomAmount, IBCMsgTransferOptions, IBCConnectionInfo, OrchestrationAccountI} from '@agoric/orchestration';
* @import {AmountArg, ChainAddress, DenomAmount, IBCMsgTransferOptions, IBCConnectionInfo, OrchestrationAccountI, LocalAccountMethods} from '@agoric/orchestration';
* @import {RecorderKit, MakeRecorderKit} from '@agoric/zoe/src/contractSupport/recorder.js'.
* @import {Zone} from '@agoric/zone';
* @import {Remote} from '@agoric/internal';
Expand Down Expand Up @@ -613,14 +613,14 @@ export const prepareLocalOrchestrationAccountKit = (
* updater will get a special notification that the account is being
* transferred.
*/
/** @type {HostOf<LocalChainAccount['deposit']>} */
/** @type {HostOf<LocalAccountMethods['deposit']>} */
deposit(payment) {
return watch(
E(this.state.account).deposit(payment),
this.facets.returnVoidWatcher,
);
},
/** @type {HostOf<LocalChainAccount['withdraw']>} */
/** @type {HostOf<LocalAccountMethods['withdraw']>} */
withdraw(amount) {
return watch(E(this.state.account).withdraw(amount));
},
Expand Down Expand Up @@ -733,7 +733,7 @@ export const prepareLocalOrchestrationAccountKit = (
matchFirstPacket(patternV) {
return watch(E(this.state.packetTools).matchFirstPacket(patternV));
},
/** @type {HostOf<LocalChainAccount['monitorTransfers']>} */
/** @type {HostOf<LocalAccountMethods['monitorTransfers']>} */
monitorTransfers(tap) {
return watch(E(this.state.packetTools).monitorTransfers(tap));
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,30 @@ Generated by [AVA](https://avajs.dev).
FunctionUnwrapper_singleton: 'Alleged: FunctionUnwrapper',
LogStore_kindHandle: 'Alleged: kind',
StateUnwrapper_kindHandle: 'Alleged: kind',
asyncFuncEagerWakers: [],
asyncFuncEagerWakers: [
Object @Alleged: asyncFlow flow {},
],
asyncFuncFailures: {},
flowForOutcomeVow: {},
flowForOutcomeVow: {
'Alleged: VowInternalsKit vowV0': 'Alleged: asyncFlow flow',
},
unwrapMap: 'Alleged: weakMapStore',
},
chainHub: {
ChainHub_kindHandle: 'Alleged: kind',
ChainHub_singleton: 'Alleged: ChainHub',
brandDenom: {},
chainInfos: {},
chainInfos: {
agoric: {
chainId: 'agoric-3',
icqEnabled: false,
stakingTokens: [
{
denom: 'ubld',
},
],
},
},
connectionInfos: {},
denom: {},
lookupChainInfo_kindHandle: 'Alleged: kind',
Expand All @@ -40,13 +54,15 @@ Generated by [AVA](https://avajs.dev).
'ChainHub Admin_singleton': 'Alleged: ChainHub Admin',
'Send PF_kindHandle': 'Alleged: kind',
'Send PF_singleton': 'Alleged: Send PF',
localAccount: 'Vow',
orchestration: {
makeLocalAccount: {
asyncFlow_kindHandle: 'Alleged: kind',
},
sendIt: {
asyncFlow_kindHandle: 'Alleged: kind',
endowments: {
0: {
contractState_kindHandle: 'Alleged: kind',
contractState_singleton: 'Alleged: contractState',
log_kindHandle: 'Alleged: kind',
log_singleton: 'Alleged: log',
zoeTools: {
Expand Down
Binary file not shown.
Loading

0 comments on commit 813e54c

Please sign in to comment.