-
Notifications
You must be signed in to change notification settings - Fork 217
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
test: make a scripted oracle that can be used in tests (#1999)
* 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
1 parent
d25709f
commit 570c92c
Showing
3 changed files
with
326 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}); | ||
} |