/ contract / src / p2pLendingMarket / loanKit.js
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 };