Skip to content

Commit

Permalink
feat: feed access controls
Browse files Browse the repository at this point in the history
  • Loading branch information
turadg committed Nov 14, 2024
1 parent 6f743c6 commit 8f4a66d
Show file tree
Hide file tree
Showing 7 changed files with 260 additions and 19 deletions.
115 changes: 115 additions & 0 deletions packages/fast-usdc/src/exos/operator-kit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { makeTracer } from '@agoric/internal';
import { Fail } from '@endo/errors';
import { M } from '@endo/patterns';
import { CctpTxEvidenceShape } from '../typeGuards.js';

const trace = makeTracer('TxOperator');

/**
* @import {Zone} from '@agoric/zone';
* @import {CctpTxEvidence} from '../types.js';
*/

/**
* @typedef {object} OperatorStatus
* @property {boolean} [disabled]
* @property {string} operatorId
*/
/**
* @typedef {Readonly<{ operatorId: string }> & {disabled: boolean}} State
*/

const OperatorKitI = {
admin: M.interface('Admin', {
disable: M.call().returns(),
}),

invitationMakers: M.interface('InvitationMakers', {
SubmitEvidence: M.call(CctpTxEvidenceShape).returns(M.promise()),
}),

operator: M.interface('Operator', {
submitEvidence: M.call(CctpTxEvidenceShape).returns(M.promise()),
getStatus: M.call().returns(M.record()),
}),
};

/**
* @param {Zone} zone
* @param {{handleEvidence: Function, makeInertInvitation: Function}} powers
*/
export const prepareOperatorKit = (zone, powers) =>
zone.exoClassKit(
'Operator Kit',
OperatorKitI,
/**
* @param {string} operatorId
* @returns {State}
*/
operatorId => {
return {
operatorId,
disabled: false,
};
},
{
admin: {
disable() {
trace(`operator ${this.state.operatorId} disabled`);
this.state.disabled = true;
},
},
/**
* NB: when this kit is an offer result, the smart-wallet will detect the `invitationMakers`
* key and save it for future offers.
*/
invitationMakers: {
/**
* Provide an API call in the form of an invitation maker, so that the
* capability is available in the smart-wallet bridge.
*
* NB: The `Invitation` object is evidence that the operation took
* place, rather than as a means of performing it as in the
* fluxAggregator contract used for price oracles.
*
* @param {CctpTxEvidence} evidence
* @returns {Promise<Invitation>}
*/
async SubmitEvidence(evidence) {
const { operator } = this.facets;
await operator.submitEvidence(evidence);
return powers.makeInertInvitation(
'evidence was pushed in the invitation maker call',
);
},
},
operator: {
/**
* submit evidence from this operator
*
* @param {CctpTxEvidence} evidence
*/
async submitEvidence(evidence) {
const { state } = this;
!state.disabled || Fail`submitEvidence for disabled operator`;
const result = await powers.handleEvidence(
{
operatorId: state.operatorId,
},
evidence,
);
return result;
},
/** @returns {OperatorStatus} */
getStatus() {
const { state } = this;
return {
operatorId: state.operatorId,
disabled: state.disabled,
};
},
},
},
);

/** @typedef {ReturnType<ReturnType<typeof prepareOperatorKit>>} OperatorKit */
103 changes: 88 additions & 15 deletions packages/fast-usdc/src/exos/transaction-feed.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,39 @@ import { makeTracer } from '@agoric/internal';
import { prepareDurablePublishKit } from '@agoric/notifier';
import { M } from '@endo/patterns';
import { CctpTxEvidenceShape } from '../typeGuards.js';
import { prepareOperatorKit } from './operator-kit.js';
import { defineInertInvitation } from '../utils/zoe.js';

/**
* @import {Zone} from '@agoric/zone';
* @import {OperatorKit} from './operator-kit.js';
* @import {CctpTxEvidence} from '../types.js';
*/

const trace = makeTracer('TxFeed', true);

export const INVITATION_MAKERS_DESC = 'transaction oracle invitation';
/** Name in the invitation purse (keyed also by this contract instance) */
export const INVITATION_MAKERS_DESC = 'oracle operator invitation';

const TransactionFeedKitI = harden({
admin: M.interface('Transaction Feed Admin', {
submitEvidence: M.call(CctpTxEvidenceShape).returns(),
initOperator: M.call(M.string()).returns(M.promise()),
}),
creator: M.interface('Transaction Feed Creator', {
makeOperatorInvitation: M.call(M.string()).returns(M.promise()),
removeOperator: M.call(M.string()).returns(),
}),
public: M.interface('Transaction Feed Public', {
getEvidenceStream: M.call().returns(M.remotable()),
getEvidenceSubscriber: M.call().returns(M.remotable()),
}),
});

/**
* @param {Zone} zone
* @param {ZCF} zcf
*/
export const prepareTransactionFeedKit = zone => {
export const prepareTransactionFeedKit = (zone, zcf) => {
const kinds = zone.mapStore('Kinds');
const makeDurablePublishKit = prepareDurablePublishKit(
kinds,
Expand All @@ -33,20 +43,83 @@ export const prepareTransactionFeedKit = zone => {
/** @type {PublishKit<CctpTxEvidence>} */
const { publisher, subscriber } = makeDurablePublishKit();

return zone.exoClassKit('Fast USDC Feed', TransactionFeedKitI, () => ({}), {
admin: {
/** @param {CctpTxEvidence } evidence */
submitEvidence: evidence => {
trace('TEMPORARY: Add evidence:', evidence);
// TODO decentralize
// TODO validate that it's valid to publish
publisher.publish(evidence);
},
},
public: {
getEvidenceStream: () => subscriber,
const makeInertInvitation = defineInertInvitation(zcf, 'submitting evidence');

const makeOperatorKit = prepareOperatorKit(zone, {
handleEvidence: (operatorId, evidence) => {
trace('handleEvidence', operatorId, evidence);
},
makeInertInvitation,
});

return zone.exoClassKit(
'Fast USDC Feed',
TransactionFeedKitI,
() => {
/** @type {MapStore<string, OperatorKit>} */
const operators = zone.mapStore('operators', {
durable: true,
});
return { operators };
},
{
creator: {
/**
* An "operator invitation" is an invitation to be an operator in the
* oracle netowrk, with the able to submit data to submit evidence of
* CCTP transactions.
*
* @param {string} operatorId unique per contract instance
* @returns {Promise<Invitation<OperatorKit>>}
*/
makeOperatorInvitation(operatorId) {
const { admin } = this.facets;
trace('makeOperatorInvitation', operatorId);

return zcf.makeInvitation(
/** @type {OfferHandler<OperatorKit>} */
seat => {
seat.exit();
return admin.initOperator(operatorId);
},
INVITATION_MAKERS_DESC,
);
},
/** @param {string} operatorId */
async removeOperator(operatorId) {
const { operators: oracles } = this.state;
trace('removeOperator', operatorId);
const kit = oracles.get(operatorId);
kit.admin.disable();
oracles.delete(operatorId);
},
},

admin: {
/** @param {string} operatorId */
async initOperator(operatorId) {
const { operators: oracles } = this.state;
trace('initOperator', operatorId);

const oracleKit = makeOperatorKit(operatorId);
oracles.init(operatorId, oracleKit);

return oracleKit;
},

/** @param {CctpTxEvidence } evidence */
submitEvidence: evidence => {
trace('TEMPORARY: Add evidence:', evidence);
// TODO decentralize
// TODO validate that it's valid to publish
publisher.publish(evidence);
},
},
public: {
getEvidenceSubscriber: () => subscriber,
},
},
);
};
harden(prepareTransactionFeedKit);

Expand Down
9 changes: 7 additions & 2 deletions packages/fast-usdc/src/fast-usdc.contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const trace = makeTracer('FastUsdc');
/**
* @import {OrchestrationPowers, OrchestrationTools} from '@agoric/orchestration/src/utils/start-helper.js';
* @import {Zone} from '@agoric/zone';
* @import {OperatorKit} from './exos/operator-kit.js';
* @import {CctpTxEvidence} from './types.js';
*/

Expand Down Expand Up @@ -63,15 +64,15 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
statusManager,
vowTools,
});
const makeFeedKit = prepareTransactionFeedKit(zone);
const makeFeedKit = prepareTransactionFeedKit(zone, zcf);
assertAllDefined({ makeFeedKit, makeAdvancer, makeSettler, statusManager });
const feedKit = makeFeedKit();
const advancer = makeAdvancer(
// @ts-expect-error FIXME
{},
);
// Connect evidence stream to advancer
void observeIteration(subscribeEach(feedKit.public.getEvidenceStream()), {
void observeIteration(subscribeEach(feedKit.public.getEvidenceSubscriber()), {
updateState(evidence) {
try {
advancer.handleTransactionEvent(evidence);
Expand All @@ -93,6 +94,10 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
);

const creatorFacet = zone.exo('Fast USDC Creator', undefined, {
/** @type {(operatorId: string) => Promise<Invitation<OperatorKit>>} */
async makeOperatorInvitation(operatorId) {
return feedKit.creator.makeOperatorInvitation(operatorId);
},
simulateFeesFromAdvance(amount, payment) {
console.log('🚧🚧 UNTIL: advance fees are implemented 🚧🚧');
// eslint-disable-next-line no-use-before-define
Expand Down
4 changes: 3 additions & 1 deletion packages/fast-usdc/test/exos/transaction-feed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js';
import { makeHeapZone } from '@agoric/zone';
import { prepareTransactionFeedKit } from '../../src/exos/transaction-feed.js';

const nullZcf = null as any;

test('basics', t => {
const zone = makeHeapZone();
const kit = prepareTransactionFeedKit(zone);
const kit = prepareTransactionFeedKit(zone, nullZcf);
t.deepEqual(Object.values(kit), []);
});
46 changes: 45 additions & 1 deletion packages/fast-usdc/test/fast-usdc.contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,18 @@ import { commonSetup } from './supports.js';

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

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

const getInvitationProperties = async (
zoe: ZoeService,
invitation: Invitation,
) => {
const invitationIssuer = E(zoe).getInvitationIssuer();
const amount = await E(invitationIssuer).getAmountOf(invitation);
return amount.value[0];
};

const startContract = async (
common: Pick<
Awaited<ReturnType<typeof commonSetup>>,
Expand Down Expand Up @@ -70,6 +78,42 @@ test('start', async t => {
);
});

test('oracle operators have closely-held rights to submit evidence of CCTP transactions', async t => {
const common = await commonSetup(t);
const { creatorFacet, zoe } = await startContract(common);

const operatorId = 'operator-1';

const opInv = await E(creatorFacet).makeOperatorInvitation(operatorId);

t.like(await getInvitationProperties(zoe, opInv), {
description: 'oracle operator invitation',
});

const operatorKit = await E(E(zoe).offer(opInv)).getOfferResult();

t.deepEqual(Object.keys(operatorKit), [
'admin',
'invitationMakers',
'operator',
]);

const e1 = MockCctpTxEvidences.AGORIC_NO_PARAMS();

{
const inv = await E(operatorKit.invitationMakers).SubmitEvidence(e1);
const res = await E(E(zoe).offer(inv)).getOfferResult();
t.is(res, 'inert; nothing should be expected from this offer');
}

// what removeOperator will do
await E(operatorKit.admin).disable();

await t.throwsAsync(E(operatorKit.invitationMakers).SubmitEvidence(e1), {
message: 'submitEvidence for disabled operator',
});
});

const logAmt = amt => [
Number(amt.value),
// numberWithCommas(Number(amt.value)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,13 @@ Generated by [AVA](https://avajs.dev).
withdrawHandler: Object @Alleged: Liquidity Pool withdrawHandler {},
},
'Liquidity Pool_kindHandle': 'Alleged: kind',
'Operator Kit_kindHandle': 'Alleged: kind',
PendingTxs: {},
SeenTxs: [],
mint: {
PoolShare: 'Alleged: zcfMint',
},
operators: {},
orchestration: {},
vstorage: {
'Durable Publish Kit_kindHandle': 'Alleged: kind',
Expand Down
Binary file modified packages/fast-usdc/test/snapshots/fast-usdc.contract.test.ts.snap
Binary file not shown.

0 comments on commit 8f4a66d

Please sign in to comment.