diff --git a/lib/ag-solo/vats/lib-wallet.js b/lib/ag-solo/vats/lib-wallet.js index e36d0d974d3..56c9a020754 100644 --- a/lib/ag-solo/vats/lib-wallet.js +++ b/lib/ag-solo/vats/lib-wallet.js @@ -50,6 +50,52 @@ export async function makeWallet( inboxStateChangeHandler(getInboxState()); } + function makeObservablePurse(purse, onFulfilled) { + return { + getName() { + return E(purse).getName(); + }, + getAssay() { + return E(purse).getAssay(); + }, + getBalance() { + return E(purse).getBalance(); + }, + depositExactly(...args) { + return E(purse) + .depositExactly(...args) + .then(result => { + onFulfilled(); + return result; + }); + }, + depositAll(...args) { + return E(purse) + .depositAll(...args) + .then(result => { + onFulfilled(); + return result; + }); + }, + withdraw(...args) { + return E(purse) + .withdraw(...args) + .then(result => { + onFulfilled(); + return result; + }); + }, + withdrawAll(...args) { + return E(purse) + .withdrawAll(...args) + .then(result => { + onFulfilled(); + return result; + }); + }, + }; + } + function checkOrder(a0, a1, b0, b1) { if (a0 === b0 && a1 === b1) { return true; @@ -63,30 +109,14 @@ export async function makeWallet( } async function makeOffer(date) { - let offerOk = false; - const { meta: { purseName0, purseName1, instanceId }, offerRules, } = dateToOfferRec.get(date); - // TODO balance check - // use unit/ops purse balance doen't include units - // return if purse balance is not > amount to withdraw - // if (!unitOps.includes(purse.getBalance(), unitsToWithdraw) return - // unitOps = assay.getUnitOps() - // if (purse0.getBalance() < extent0) return; // todo message - - const instanceHandle = await E(registrar).get(instanceId); - - // Get the assays in the contract. - // Get the contract instance. - const { - terms: { assays: contractAssays }, - instance, - } = await E(zoe).getInstance(instanceHandle); + const purse0 = nameToPurse.get(purseName0); + const purse1 = nameToPurse.get(purseName1); - // Extract extents and assayIds from the offer rules skeleton. const { payoutRules: [ { @@ -101,73 +131,128 @@ export async function makeWallet( ], } = offerRules; - // Find the correcponsing assays by id in the registrar. - const registryAssays = await Promise.all([ - E(registrar).get(assayId0), - E(registrar).get(assayId1), + const instanceHandleP = E(registrar).get(instanceId); + const regAssay0P = E(registrar).get(assayId0); + const regAssay1P = E(registrar).get(assayId1); + const purseAssay0P = E(purse0).getAssay(); + const purseAssay1P = E(purse1).getAssay(); + const purseUnit0P = E(purseAssay0P).makeUnits(extent0 || 0); + const purseUnit1P = E(purseAssay0P).makeUnits(extent1 || 0); + + const [ + instanceHandle, + regAssay0, + regAssay1, + purseAssay0, + purseAssay1, + purseUnit0, + purseUnit1, + ] = await Promise.all([ + instanceHandleP, + regAssay0P, + regAssay1P, + purseAssay0P, + purseAssay1P, + purseUnit0P, + purseUnit1P, ]); - // Reconstruct the units. - const units = await Promise.all([ - E(registryAssays[0]).makeUnits(extent0 || 0), // use from registry - E(registryAssays[1]).makeUnits(extent1 || 0), // use from registry - E(contractAssays[2]).makeUnits(extent2 || 0), // default from contract + // ===================== + // === AWAITING TURN === + // ===================== + + const { + terms: { + assays: [contractAssay0, contractAssay1, contractAssay2], + }, + instance, + } = await E(zoe).getInstance(instanceHandle); + + // ===================== + // === AWAITING TURN === + // ===================== + + insist(contractAssay0 === regAssay0 && regAssay1 === contractAssay1); + + // Check whether we sell on contract assay 0 or 1. + const normal = checkOrder( + purseAssay0, + purseAssay1, + contractAssay0, + contractAssay1, + ); + + const payment0P = E(purse0).withdraw(normal ? purseUnit0 : purseUnit1); + const contractUnit0P = E(contractAssay0).makeUnits(extent0 || 0); + const contractUnit1P = E(contractAssay1).makeUnits(extent1 || 0); + const contractUnit2P = E(contractAssay2).makeUnits(extent2 || 0); + + const [ + payment0, + contractUnit0, + contractUnit1, + contractUnit2, + ] = await Promise.all([ + payment0P, + contractUnit0P, + contractUnit1P, + contractUnit2P, ]); - // Clone the offer rules to make them writable. + // ===================== + // === AWAITING TURN === + // ===================== + + // Clone the offer rules to have a writable object. const newOfferRules = JSON.parse(JSON.stringify(offerRules)); - // Hydrate by deconstruction. - [ - newOfferRules.payoutRules[0].units, - newOfferRules.payoutRules[1].units, - newOfferRules.payoutRules[2].units, - ] = units; + // Hydrate with the resolved units. + newOfferRules.payoutRules[0].units = contractUnit0; + newOfferRules.payoutRules[1].units = contractUnit1; + newOfferRules.payoutRules[2].units = contractUnit2; harden(newOfferRules); - const purse0 = nameToPurse.get(purseName0); - const purse1 = nameToPurse.get(purseName1); - const purseAssays = await Promise.all([ - E(purse0).getAssay(), - E(purse1).getAssay(), + const payment = normal + ? [payment0, undefined, undefined] + : [undefined, payment0, undefined]; + + const { escrowReceipt, payout: payoutP } = await E(zoe).escrow( + newOfferRules, + payment, + ); + + // ===================== + // === AWAITING TURN === + // ===================== + + // IMPORTANT: payout will resolve only once makeOffer() + // resolves, so we technically only need to await on + // payout. For readability of the code, and to ease any + // eventual debugging, we await on both: it is not + // obvious that both are joined internally, and stumbling + // over a naked non-awaited invocation of E() would appear + // as an error. + + const [offerOk, payout] = await Promise.all([ + E(instance).makeOffer(escrowReceipt), + payoutP, ]); - try { - const normal = checkOrder( - purseAssays[0], - purseAssays[1], - registryAssays[0], - registryAssays[1], - ); - - const payment0 = await E(purse0).withdraw(normal ? units[0] : units[1]); - const payments = [ - normal ? payment0 : undefined, - normal ? undefined : payment0, - ]; - - const { escrowReceipt, payout } = await E(zoe).escrow( - newOfferRules, - payments, - ); - - offerOk = await E(instance).makeOffer(escrowReceipt); - - if (offerOk) { - const [payout0, payout1] = await payout; - await Promise.all([ - E(purse0).depositAll(normal ? payout0 : payout1), - E(purse1).depositAll(normal ? payout1 : payout0), - ]); - } - } catch (e) { - // if balance > empty payment has not been claimed. - const recoveryPayment = purseAssays[0].claimAll(); - await E(purse0).depositAll(recoveryPayment); - } + // ===================== + // === AWAITING TURN === + // ===================== - updatePursesState(purseName0, purse0); - updatePursesState(purseName1, purse1); + const deposit0P = E(purse0).depositAll(payout[normal ? 0 : 1]); + const deposit1P = E(purse1).depositAll(payout[normal ? 1 : 0]); + + await Promise.all([deposit0P, deposit1P]); + + // ===================== + // === AWAITING TURN === + // ===================== + + // updatePursesState(purseName0, purse0); + // updatePursesState(purseName1, purse1); return offerOk; } @@ -175,41 +260,68 @@ export async function makeWallet( // === API async function addPurse(purse) { - const purseName = await E(purse).getName(); + const purseNameP = E(purse).getName(); + const assayP = E(purse).getAssay(); + const labelP = E(assayP).getLabel(); + + const [purseName, assay, { allegedName }] = await Promise.all([ + purseNameP, + assayP, + labelP, + ]); + + // ===================== + // === AWAITING TURN === + // ===================== + insist(!nameToPurse.has(purseName))`Purse name already used in wallet.`; - const assay = await E(purse).getAssay(); - const { allegedName } = await E(assay).getLabel(); const assayId = await E(registrar).register(allegedName, assay); - nameToPurse.set(purseName, purse); + // ===================== + // === AWAITING TURN === + // ===================== + + // IMPORTANT: once wrapped, the original purse shoudl never + // be used otherwise the UI state will be out of sync. + + const observablePurse = makeObservablePurse(purse, () => + updatePursesState(purseName, purse), + ); + + nameToPurse.set(purseName, observablePurse); nameToAssayId.set(purseName, assayId); - await updatePursesState(purseName, purse); + updatePursesState(purseName, purse); } function getPurses() { return harden([...nameToPurse.values()]); } - async function addOffer(offerRec) { + function addOffer(offerRec) { const { meta: { date }, } = offerRec; dateToOfferRec.set(date, offerRec); - await updateInboxState(date, offerRec); + updateInboxState(date, offerRec); } function declineOffer(date) { const { meta } = dateToOfferRec.get(date); // Update status, drop the offerRules - const declineedOfferRec = { meta: { ...meta, status: 'decline' } }; - dateToOfferRec.set(date, declineedOfferRec); - updateInboxState(date, declineedOfferRec); + const declinedOfferRec = { meta: { ...meta, status: 'decline' } }; + dateToOfferRec.set(date, declinedOfferRec); + updateInboxState(date, declinedOfferRec); } async function acceptOffer(date) { const offerOk = await makeOffer(date); + + // ===================== + // === AWAITING TURN === + // ===================== + if (!offerOk) return; const { meta } = dateToOfferRec.get(date);