Skip to content

Commit

Permalink
test: make a scripted oracle that can be used in tests (#1999)
Browse files Browse the repository at this point in the history
* test: make a scripted oracle that can be used in tests

Also a rudimentary bounty contract that pays a bounty if a condition
is certified by an oracle.

* chore: clarify the contract's behavior in the comment.
  • Loading branch information
Chris-Hibbert authored Nov 10, 2020
1 parent d25709f commit 570c92c
Show file tree
Hide file tree
Showing 3 changed files with 326 additions and 0 deletions.
84 changes: 84 additions & 0 deletions packages/zoe/test/unitTests/bounty.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { E } from '@agoric/eventual-send';
import { assert, details } from '@agoric/assert';

/**
* This contract lets a funder endow a bounty that will pay out if an Oracle
* reports an event at a deadline. To make a simple contract for a test the
* contract only pays attention to the event that occurs at the requested
* deadline. A realistic contract might look more than once, or might accept any
* positive response before the deadline.
*
* @type {ContractStartFn}
*/
import { assertProposalShape } from '../../src/contractSupport';

const start = async zcf => {
const { oracle, deadline, condition, timer, fee } = zcf.getTerms();
const {
maths: { Fee: feeMath, Bounty: bountyMath },
} = zcf.getTerms();

/** @type {OfferHandler} */
function funder(funderSeat) {
const endowBounty = harden({
give: { Bounty: null },
});
assertProposalShape(funderSeat, endowBounty);

function payOffBounty(seat) {
zcf.reallocate(
funderSeat.stage({ Bounty: bountyMath.getEmpty() }),
seat.stage({ Bounty: funderSeat.getCurrentAllocation().Bounty }),
);
seat.exit();
funderSeat.exit();
zcf.shutdown('bounty was paid');
}

function refundBounty(seat) {
// funds are already allocated.
seat.exit();
funderSeat.exit();
zcf.shutdown('The bounty was not earned');
}

/** @type {OfferHandler} */
function beneficiary(bountySeat) {
const feeProposal = harden({
give: { Fee: null },
});
assertProposalShape(bountySeat, feeProposal);
const feeAmount = bountySeat.getCurrentAllocation().Fee;
assert(
feeMath.isGTE(feeAmount, fee),
details`Fee was required to be at least ${fee}`,
);

// The funder gets the fee regardless of the outcome.
zcf.reallocate(
funderSeat.stage({ Fee: feeAmount }),
bountySeat.stage({ Fee: feeMath.getEmpty() }),
);

const wakeHandler = harden({
wake: async () => {
const reply = await E(oracle).query('state');
if (reply.event === condition) {
payOffBounty(bountySeat);
} else {
refundBounty(bountySeat);
}
},
});
timer.setWakeup(deadline, wakeHandler);
}

return zcf.makeInvitation(beneficiary, 'pay to be the beneficiary');
}

const creatorInvitation = zcf.makeInvitation(funder, 'fund a bounty');
return harden({ creatorInvitation });
};

harden(start);
export { start };
186 changes: 186 additions & 0 deletions packages/zoe/test/unitTests/test-scriptedOracle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
// @ts-check
// eslint-disable-next-line import/no-extraneous-dependencies
import '@agoric/install-ses';
// eslint-disable-next-line import/no-extraneous-dependencies
import test from 'ava';
import bundleSource from '@agoric/bundle-source';

import { E } from '@agoric/eventual-send';

import { makeFakeVatAdmin } from '../../src/contractFacet/fakeVatAdmin';
import { makeZoe } from '../..';

import '../../exported';
import '../../src/contracts/exported';
import buildManualTimer from '../../tools/manualTimer';
import { setup } from './setupBasicMints';
import { assertPayoutAmount } from '../zoeTestHelpers';
import { makeScriptedOracle } from '../../tools/scriptedOracle';

// This test shows how to set up a fake oracle and use it in a contract.

const oracleContractPath = `${__dirname}/../../src/contracts/oracle`;
const bountyContractPath = `${__dirname}/bounty`;

test.before(
'setup oracle',
/** @param {ExecutionContext} ot */ async ot => {
// Outside of tests, we should use the long-lived Zoe on the
// testnet. In this test, we must create a new Zoe.
const zoe = makeZoe(makeFakeVatAdmin().admin);

const oracleContractBundle = await bundleSource(oracleContractPath);
const bountyContractBundle = await bundleSource(bountyContractPath);

// Install the contracts on Zoe, getting installations. We use these
// installations to instantiate the contracts.
const oracleInstallation = await E(zoe).install(oracleContractBundle);
const bountyInstallation = await E(zoe).install(bountyContractBundle);
const { moolaIssuer, moolaMint, moola } = setup();

ot.context.zoe = zoe;
ot.context.oracleInstallation = oracleInstallation;
ot.context.bountyInstallation = bountyInstallation;
ot.context.moolaMint = moolaMint;
ot.context.moolaIssuer = moolaIssuer;
ot.context.moola = moola;
},
);

test('pay bounty', async t => {
const { zoe, oracleInstallation, bountyInstallation } = t.context;
// The timer is not build in test.before(), because each test needs its own.
const timer = buildManualTimer(console.log);
const { moolaIssuer, moolaMint, moola } = t.context;
const script = { 0: 'Nothing', 1: 'Nothing', 2: 'Nothing', 3: 'Succeeded' };

const oracle = await makeScriptedOracle(
script,
oracleInstallation,
timer,
zoe,
t.context.moolaIssuer,
);
const { publicFacet } = oracle;

const { creatorInvitation: funderInvitation } = await E(zoe).startInstance(
bountyInstallation,
{ Bounty: moolaIssuer, Fee: moolaIssuer },
{
oracle: publicFacet,
deadline: 3,
condition: 'Succeeded',
timer,
fee: moola(50),
},
);

// Alice funds a bounty
const funderSeat = await E(zoe).offer(
funderInvitation,
harden({
give: { Bounty: moola(200) },
want: { Fee: moola(0) },
}),
harden({
Bounty: moolaMint.mintPayment(moola(200)),
}),
);
const bountyInvitation = await funderSeat.getOfferResult();
assertPayoutAmount(t, moolaIssuer, funderSeat.getPayout('Fee'), moola(50));
assertPayoutAmount(t, moolaIssuer, funderSeat.getPayout('Bounty'), moola(0));

// Bob buys the bounty invitation
const bountySeat = await E(zoe).offer(
bountyInvitation,
harden({
give: { Fee: moola(50) },
want: { Bounty: moola(0) },
}),
harden({
Fee: moolaMint.mintPayment(moola(50)),
}),
);
assertPayoutAmount(t, moolaIssuer, bountySeat.getPayout('Fee'), moola(0));
assertPayoutAmount(
t,
moolaIssuer,
bountySeat.getPayout('Bounty'),
moola(200),
);

await E(timer).tick();
await E(timer).tick();
await E(timer).tick();
await E(timer).tick();
});

test('pay no bounty', async t => {
const { zoe, oracleInstallation, bountyInstallation } = t.context;
// The timer is not build in test.before(), because each test needs its own.
const timer = buildManualTimer(console.log);
const { moolaIssuer, moolaMint, moola } = t.context;
const script = { 0: 'Nothing', 1: 'Nothing', 2: 'Nothing', 3: 'Nothing' };

const oracle = await makeScriptedOracle(
script,
oracleInstallation,
timer,
zoe,
t.context.moolaIssuer,
);
const { publicFacet } = oracle;

const { creatorInvitation: funderInvitation } = await E(zoe).startInstance(
bountyInstallation,
{ Bounty: moolaIssuer, Fee: moolaIssuer },
{
oracle: publicFacet,
deadline: 3,
condition: 'Succeeded',
timer,
fee: moola(50),
},
);

// Alice funds a bounty
const funderSeat = await E(zoe).offer(
funderInvitation,
harden({
give: { Bounty: moola(200) },
want: { Fee: moola(0) },
}),
harden({
Bounty: moolaMint.mintPayment(moola(200)),
}),
);
const bountyInvitation = await funderSeat.getOfferResult();
assertPayoutAmount(t, moolaIssuer, funderSeat.getPayout('Fee'), moola(50));
// Alice gets the funds back.
assertPayoutAmount(
t,
moolaIssuer,
funderSeat.getPayout('Bounty'),
moola(200),
);

// Bob buys the bounty invitation
const bountySeat = await E(zoe).offer(
bountyInvitation,
harden({
give: { Fee: moola(50) },
want: { Bounty: moola(0) },
}),
harden({
Fee: moolaMint.mintPayment(moola(50)),
}),
);
assertPayoutAmount(t, moolaIssuer, bountySeat.getPayout('Fee'), moola(0));
// Bob doesn't receive the bounty
assertPayoutAmount(t, moolaIssuer, bountySeat.getPayout('Bounty'), moola(0));

await E(timer).tick();
await E(timer).tick();
await E(timer).tick();
await E(timer).tick();
});
56 changes: 56 additions & 0 deletions packages/zoe/tools/scriptedOracle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { E } from '@agoric/eventual-send';

/**
* Start an instance of an Oracle that follows a script. The Oracle has access
* to a timer, and looks in the script for events indexed by the current time.
* It responds to onQuery() by reporting any current event. This is intended to
* be useful for tests.
* The queries return an object that has fields { event, time, query }. event is
* whatever was in the script indexed by the time. The query is returned in the
* result, but has no impact on the result. If the script has no entry for the
* time, the event is 'nothing to report'.
*
* @param {Array} script
* @param {Installation} oracleInstallation
* @param {Timer} timer
* @param {ZoeService} zoe
* @param {Issuer} feeIssuer
* @returns {Promise<unknown[]>}
*/

export async function makeScriptedOracle(
script,
oracleInstallation,
timer,
zoe,
feeIssuer,
) {
/** @type {OracleHandler} */
const oracleHandler = harden({
async onQuery(query) {
const time = await E(timer).getCurrentTimestamp();
const event = script[time] || 'Nothing to report';
const reply = { event, time, query };
return harden({ reply });
},
async onError(_query, _reason) {
// do nothing
},
async onReply(_query, _reply, _fee) {
// do nothing
},
});

/** @type {OracleStartFnResult} */
const startResult = await E(zoe).startInstance(oracleInstallation, {
Fee: feeIssuer,
});
const creatorFacet = await E(startResult.creatorFacet).initialize({
oracleHandler,
});

return harden({
publicFacet: startResult.publicFacet,
creatorFacet,
});
}

0 comments on commit 570c92c

Please sign in to comment.