loanKit.js
1 import { AmountShape } from '@agoric/ertp'; 2 import { M, prepareExoClassKit } from '@agoric/vat-data'; 3 import { 4 TopicsRecordShape, 5 atomicTransfer 6 } from '@agoric/zoe/src/contractSupport/index.js'; 7 import { Fn } from '../../data.types.js'; 8 import { E } from '@endo/eventual-send'; 9 import { Far } from '@endo/marshal'; 10 import { TimeMath } from '@agoric/time'; 11 import { borrowOfferHandler } from './p2p.js'; 12 const makeBorrowInvitationDescription = (debtKey, collateralKey) => 13 `Borrow ${debtKey} in exchange for suppliying ${collateralKey}`; 14 15 const setExpirationWakeup = (timer, expiry, fn) => 16 E(timer).setWakeup( 17 expiry, 18 Far('waker', { 19 wake: timestamp => { 20 console.log('timer has awoke!'); 21 console.log('time to repay lender::', { timestamp }); 22 fn(timestamp); 23 } 24 }) 25 ); 26 27 /** 28 * initState function for durably held state properties. 29 * @function initDurableState 30 * @param {DurableLoanState} 31 * @returns {DurableLoanState} 32 */ 33 const initDurableState = ({ 34 loanNode, 35 lenderSeat, 36 debtSeat, 37 collateralSeat 38 }) => ({ 39 loanNode, 40 lenderSeat, 41 debtSeat, 42 collateralSeat 43 }); 44 45 /** 46 * initState function for ephemeral state properties. 47 * @function initEphemeralState 48 * @param {EphemeralLoanState} 49 * @returns {EphemeralLoanState} 50 */ 51 const initEphemeralState = ({ 52 debtKey, 53 collateralKey, 54 debtBrand, 55 collateralBrand, 56 expiry, 57 endTime = 0n, 58 startTime = 0n, 59 interestRate, 60 id, 61 expirationP 62 }) => ({ 63 debtKey, 64 collateralKey, 65 debtBrand, 66 endTime, 67 startTime, 68 collateralBrand, 69 expiry, 70 expirationP, 71 interestRate, 72 id 73 }); 74 75 /** 76 * @typedef {DurableLoanState & EphemeralLoanState} LoanState 77 */ 78 79 /** 80 * @function initLoanState 81 * @param {object} obj 82 * @returns {LoanState} 83 */ 84 const initLoanState = obj => ({ 85 ...initEphemeralState(obj), 86 ...initDurableState(obj) 87 }); 88 89 const prepareLoanFromAccount = (zcf, baggage, makeRecorder) => { 90 return prepareExoClassKit('Bo'); 91 }; 92 93 // [TG] TODO 94 // have holder facet reflect the shape of interface below 95 const HolderI = M.interface('holder', { 96 getCollateralAmount: M.call().returns(AmountShape), 97 getCurrentDebt: M.call().returns(AmountShape), 98 getNormalizedDebt: M.call().returns(AmountShape), 99 getPublicTopics: M.call().returns(TopicsRecordShape), 100 makeAdjustBalancesInvitation: M.call().returns(M.promise()), 101 makeCloseInvitation: M.call().returns(M.promise()), 102 makeTransferInvitation: M.call().returns(M.promise()) 103 }); 104 105 /** @type {{ [name: string]: [ description: string, valueShape: Pattern ] }} */ 106 const PUBLIC_TOPICS = { 107 loan: ['Loan Holder status', M.any()] 108 }; 109 110 const createLoanExo = (zcf, baggage, makeRecorderKit) => { 111 const newLoan = prepareExoClassKit( 112 baggage, 113 'LenderAccount Exo Object', 114 undefined, 115 ({ terms, lenderSeat, debtSeat, loanNode, collateralSeat }) => { 116 return { 117 ...initLoanState({ 118 ...terms, 119 lenderSeat, 120 debtSeat, 121 loanNode, 122 collateralSeat 123 }), 124 topicKit: makeRecorderKit(loanNode, PUBLIC_TOPICS.loan[1]) 125 }; 126 }, 127 { 128 invitationMakers: { 129 makeFulfillLoan() { 130 return zcf.makeInvitation( 131 seat => this.facets.self.enterLoan(seat), 132 makeBorrowInvitationDescription(this.state), 133 harden(initEphemeralState(this.state)) 134 ); 135 } 136 }, 137 public: { 138 getSeatBalances() { 139 return { 140 lenderSeat: this.state.lenderSeat.getCurrentAllocation(), 141 debtSeat: this.state.debtSeat.getCurrentAllocation() 142 }; 143 } 144 }, 145 helper: { 146 async calculateExpiryTimes() { 147 const { timerService } = zcf.getTerms(); 148 return Fn.of( 149 TimeMath.absValue(await E(timerService).getCurrentTimestamp()) 150 ) 151 .chain(ts => 152 Fn(x => ({ ...x, startTime: ts, endTime: ts + x.expiry })) 153 ) 154 .map(x => { 155 this.state.startTime = x.startTime; 156 this.state.endTime = x.endTime; 157 return x; 158 }) 159 .run({ 160 expiry: this.state.expiry 161 }); 162 }, 163 getUpdater() { 164 return this.state.topicKit.recorder; 165 } 166 }, 167 self: { 168 getId() { 169 console.log({ 170 state: this.state, 171 id: this.state.id, 172 loanId: this.state.loanId 173 }); 174 return this.state.id; 175 }, 176 async getCurrentInterest() {}, 177 setExpirationWakeup() { 178 const { timerService } = zcf.getTerms(); 179 const { lenderSeat, collateralSeat, collateralBrand, collateralKey } = 180 this.state; 181 /** 182 * @description transfer collateral to lenderSeat and exit 183 * @todo 184 * 1. change loan state to COMPLETE, or indicate collateral has been claimed 185 * 2. cancel expirationP - do we need to do this? It will just be resolved, 186 * with a success or error message as the res? 187 * 3. ... garbage collect exo object? a zcf.shutdown() equiv? 188 * @returns {void} */ 189 const expirationHandler = _timestamp => { 190 atomicTransfer(zcf, collateralSeat, lenderSeat, { 191 [collateralKey]: collateralSeat.getAmountAllocated( 192 collateralKey, 193 collateralBrand 194 ) 195 }); 196 lenderSeat.exit( 197 'Loan expired without repayment. Collateral now claimable.' 198 ); 199 }; 200 201 this.state.expirationP = setExpirationWakeup( 202 timerService, 203 this.state.endTime, 204 expirationHandler 205 ); 206 }, 207 async enterLoan(borrowSeat) { 208 await this.facets.helper 209 .calculateExpiryTimes() 210 .then(() => this.facets.self.setExpirationWakeup()); 211 return borrowOfferHandler({ 212 ...this.state, 213 zcf 214 })(borrowSeat); 215 } 216 }, 217 holder: { 218 getExpirationDetails() { 219 const { endTime, startTime } = this.state; 220 return harden({ 221 startTime, 222 endTime 223 }); 224 }, 225 getPublicTopics() { 226 return harden({ 227 loan: { 228 description: PUBLIC_TOPICS.loan[0], 229 subscriber: this.state.topicKit.subscriber, 230 storagePath: this.state.topicKit.recorder.getStoragePath() 231 } 232 }); 233 } 234 } 235 } 236 ); 237 return newLoan; 238 }; 239 240 harden(createLoanExo); 241 242 export { createLoanExo };