/ contract / test / test-p2p.js
test-p2p.js
  1  // @ts-check
  2  
  3  /* eslint-disable import/order -- https://github.com/endojs/endo/issues/1235 */
  4  import { countKeys, describe } from './prepare-riteway.js';
  5  import path from 'path';
  6  
  7  import bundleSource from '@endo/bundle-source';
  8  
  9  import { E } from '@endo/eventual-send';
 10  import { makeFakeVatAdmin } from '@agoric/zoe/tools/fakeVatAdmin.js';
 11  import { AmountMath, makeIssuerKit } from '@agoric/ertp';
 12  import { compose, getHoursInSeconds } from '../src/shared/utils/general.js';
 13  import { Far, passStyleOf } from '@endo/marshal';
 14  import { assert as agAssert } from '@agoric/assert';
 15  import { makeZoeKitForTest } from '@agoric/zoe/tools/setup-zoe.js';
 16  import { makeFakeStorageKit } from '@agoric/internal/src/storage-test-utils.js';
 17  import { makeScalarBigMapStore } from '@agoric/vat-data';
 18  import { eventLoopIteration } from '../src/tools/eventLoopIteration.js';
 19  import buildManualTimer from '../src/tools/manualTimer.js';
 20  import { makeFakeMarshaller } from '@agoric/notifier/tools/testSupports.js';
 21  
 22  import { Nat, isNat } from '@endo/nat';
 23  import { setupContract } from './services.js';
 24  import { ONE_DAY } from '../src/shared/utils/time.js';
 25  import { subscriptionTracker } from './test-metrics.js';
 26  const isTruthy = x => !!x;
 27  
 28  const multiplyByEuler = (rate, time) => p => p * Math.pow(Math.E, rate * time);
 29  
 30  const assertIsRemotable = facet => agAssert(passStyleOf(facet) === 'remotable');
 31  
 32  const filename = new URL(import.meta.url).pathname;
 33  const dirname = path.dirname(filename);
 34  
 35  const contractPath = `${dirname}/../src/p2pLendingMarket/p2p.js`;
 36  const utilsContractPath = `${dirname}/../src/utilityContract/index.js`;
 37  const makeKit = name => makeIssuerKit(name);
 38  
 39  const setupIssuers = (issuers = ['ATOM', 'IST', 'BLD', 'USDC']) =>
 40    issuers.map(makeKit);
 41  
 42  const issuers = setupIssuers();
 43  const makeAmounts =
 44    ({ brand }) =>
 45      (x = 0n) =>
 46        AmountMath.make(brand, x);
 47  const [atomKit, istKit, bldKit, USDCKit] = issuers;
 48  const [atoms, ist, bld, usdc] = issuers.map(makeAmounts);
 49  
 50  const makePurse = ({ issuer }) => issuer.makeEmptyPurse();
 51  
 52  describe('TimerService API', async assert => {
 53    const { contractInstance, zoe, fakeVatAdmin, utilsInstance } =
 54      await setupContract();
 55  
 56    const liquidationWaker = Far('Liquidation Waker', {
 57      wake(time) {
 58        console.log(`The clock has struck ${time}. Now, we must say goodbye.`);
 59      }
 60    });
 61  
 62    /**
 63     * @type {import('@agoric/swingset-vat/tools/manual-timer.js').TimerService}
 64     */
 65    const timer = buildManualTimer(console.log, 6000n, {
 66      timeStep: 5n,
 67      eventLoopIteration
 68    });
 69  
 70    const timerClock = await E(timer).getClock();
 71  
 72    await E(timer).setWakeup(10_000n, liquidationWaker);
 73  
 74    await E(timer).tickN(5n);
 75    assert({
 76      given: 'E(timer).getClock()',
 77      should: 'create a clock ',
 78      actual: await E(timerClock).getTimerBrand(),
 79      expected: await E(timer).getTimerBrand()
 80    });
 81  
 82    // const currentTime = await E(timeUtilsFacet).getCurrentTimeMs();
 83    // assert({
 84    //   given: 'E(dateFacet).getCurrentTimeMs()',
 85    //   should: 'return a timestamp',
 86    //   actual: isNat(currentTime),
 87    //   expected: true
 88    // });
 89  });
 90  
 91  const callFacet =
 92    facet =>
 93      (method, args = {}) =>
 94        E(facet)[method](args);
 95  
 96  const facetMethods = {
 97    public: {
 98      GET_PUBLIC_TOPICS: 'getPublicTopics',
 99      SAVE_ISSUERS: 'saveIssuers',
100      GET_ISSUERS: 'getIssuers',
101      UNDERWRITE_LOAN: 'underwriteLoan',
102      GET_FUNDED_LOANS: 'getFundedLoans'
103    }
104  };
105  
106  describe('p2p loan:: sad paths', async (assert, done) => {
107    const KEYWORDS = {
108      IST: 'IST',
109      ATOM: 'ATOM'
110    };
111  
112    const { IST: istKeyword, ATOM: atomKeyword } = KEYWORDS;
113  
114    const { contractInstance, zoe, fakeVatAdmin, utilsInstance } =
115      await setupContract();
116  
117    const zoeIssuer = await E(zoe).getInvitationIssuer();
118  
119    const callCreatorFacet = callFacet(contractInstance.creatorFacet);
120    const callPublicFacet = callFacet(contractInstance.publicFacet);
121  
122    const {
123      public: {
124        GET_PUBLIC_TOPICS,
125        GET_ISSUERS,
126        GET_FUNDED_LOANS,
127        SAVE_ISSUERS,
128        UNDERWRITE_LOAN
129      }
130    } = facetMethods;
131  
132    await assert({
133      unitLabel: 'E(creatorFacet).saveIssuers()',
134      given: 'an issuer keyword record',
135      should: 'return a success message',
136      actual: await callCreatorFacet(SAVE_ISSUERS, {
137        ATOM: atomKit.issuer,
138        IST: istKit.issuer,
139        BLD: bldKit.issuer,
140        USDC: USDCKit.issuer
141      }),
142      expected: 'Add issuers success!'
143    });
144  
145    const getIssuers = () => callPublicFacet(GET_ISSUERS);
146    const getLength = ({ length }) => length;
147    await assert({
148      unitLabel: 'publicFacet',
149      given: 'contract',
150      should: 'contain an instance',
151      actual: getLength(await getIssuers()),
152      expected: 4
153    });
154  
155    const fakeAtomKit = makeIssuerKit('not atom');
156    const fakeUsdcKit = makeIssuerKit('not usdc');
157  
158    const aliceLoanTerms = {
159      collateralKey: 'ATOM',
160      collateralBrand: atomKit.brand,
161      debtKey: 'IST',
162      debtBrand: istKit.brand,
163      expiry: 1209600000n,
164      loanToValue: 70n,
165      interestRate: 5n
166    };
167  
168    const [debtBrandError, collateralBrandError] = await Promise.all([
169      callPublicFacet(UNDERWRITE_LOAN, {
170        ...aliceLoanTerms,
171        debtBrand: fakeUsdcKit.brand
172      }),
173      callPublicFacet(UNDERWRITE_LOAN, {
174        ...aliceLoanTerms,
175        collateralBrand: fakeAtomKit.brand
176      })
177    ]);
178    const fakeErrorObj = (brandName = 'test brand') => ({
179      displayMessage: 'Error validating inputs',
180      error: new Error(
181        '[object Alleged: ' + brandName + ' brand] is not a recognized brand.'
182      )
183    });
184  
185    await assert({
186      given: 'an offer containing an invalid debt brand',
187      should: 'gracefully handle the error',
188      actual: debtBrandError,
189      expected: fakeErrorObj('not usdc')
190    });
191  
192    await assert({
193      given: 'an offer containing an invalid collateral brand',
194      should: 'gracefully handle the error',
195      actual: collateralBrandError,
196      expected: fakeErrorObj('not atom')
197    });
198  });
199  
200  describe('p2p loan', async (assert, done) => {
201    const KEYWORDS = {
202      IST: 'IST',
203      ATOM: 'ATOM'
204    };
205  
206    const { IST: istKeyword, ATOM: atomKeyword } = KEYWORDS;
207  
208    const { contractInstance, zoe, fakeVatAdmin, utilsInstance } =
209      await setupContract();
210  
211    const zoeIssuer = await E(zoe).getInvitationIssuer();
212  
213    const callCreatorFacet = callFacet(contractInstance.creatorFacet);
214    const callPublicFacet = callFacet(contractInstance.publicFacet);
215  
216    const {
217      public: {
218        GET_PUBLIC_TOPICS,
219        GET_ISSUERS,
220        GET_FUNDED_LOANS,
221        SAVE_ISSUERS,
222        UNDERWRITE_LOAN
223      }
224    } = facetMethods;
225    const { metrics } = await callPublicFacet(GET_PUBLIC_TOPICS);
226  
227    await assert({
228      unitLabel: 'publicFacet',
229      given: 'getPublicTopics',
230      should: 'return metrics object with 3 keys.',
231      actual: countKeys(metrics),
232      expected: 3
233    });
234  
235    await assert({
236      unitLabel: 'publicFacet',
237      given: 'getPublicTopics',
238      should: 'return metrics object with the correct descriptin.',
239      actual: metrics.description,
240      expected: 'P2P Lending Marketplace Metrics'
241    });
242  
243    await assert({
244      unitLabel: 'E(creatorFacet).saveIssuers()',
245      given: 'an issuer keyword record',
246      should: 'return a success message',
247      actual: await callCreatorFacet(SAVE_ISSUERS, {
248        ATOM: atomKit.issuer,
249        IST: istKit.issuer,
250        BLD: bldKit.issuer,
251        USDC: USDCKit.issuer
252      }),
253      expected: 'Add issuers success!'
254    });
255  
256    const getIssuers = () => callPublicFacet(GET_ISSUERS);
257    const getLength = ({ length }) => length;
258    await assert({
259      unitLabel: 'publicFacet',
260      given: 'contract',
261      should: 'contain an instance',
262      actual: getLength(await getIssuers()),
263      expected: 4
264    });
265  
266    const aliceLoanTerms = {
267      collateralKey: 'ATOM',
268      collateralBrand: atomKit.brand,
269      debtKey: 'IST',
270      debtBrand: istKit.brand,
271      expiry: 1209600000n,
272      loanToValue: 70n,
273      interestRate: 5n
274    };
275  
276    const aliceLoanInvitation = await callPublicFacet(
277      UNDERWRITE_LOAN,
278      aliceLoanTerms
279    );
280  
281    assert({
282      unitLabel: 'publicFacet',
283      given: 'underwriteLoan',
284      should: 'prepare the loanAgreeentReducer state with the terms',
285      actual: await zoeIssuer.isLive(aliceLoanInvitation),
286      expected: true
287    });
288  
289    const aliceLoanDetails = await E(zoe).getInvitationDetails(
290      aliceLoanInvitation
291    );
292  
293    const firstLoanId = 'loan-0';
294  
295    await assert({
296      unitLabel: 'aliceLoanInvitation',
297      given: 'getInvitationDetails',
298      should: 'reflect in terms specified upon the creation of the loan',
299      actual: aliceLoanDetails.customDetails,
300      expected: { ...aliceLoanTerms, id: firstLoanId }
301    });
302  
303    const sevenThousandIST = ist(7000n);
304  
305    const createZoeOffer = proposal => payment => (invitation) => E(zoe).offer(
306      invitation,
307      proposal,
308      payment
309    )
310  
311    const offerOne = createZoeOffer({ give: { IST: sevenThousandIST } })(istKit.mint.mintPayment(sevenThousandIST))
312  
313  
314  
315    const aliceLoanSeat = await E(zoe).offer(
316      aliceLoanInvitation,
317      {
318        give: { IST: sevenThousandIST },
319        want: {}
320      },
321      { IST: istKit.mint.mintPayment(sevenThousandIST) }
322    );
323  
324  
325    const aliceLoanExo = await E(aliceLoanSeat).getOfferResult();
326  
327    const alicesSecondLoanInvitation = await callPublicFacet(UNDERWRITE_LOAN, {
328      ...aliceLoanTerms,
329      expiry: ONE_DAY
330    });
331    const aliceSecondLoanDetails = await E(zoe).getInvitationDetails(
332      alicesSecondLoanInvitation
333    );
334  
335  
336    await assert({
337      unitLabel: 'aliceSecondLoanInvitation',
338      given: 'isLive check',
339      should: 'return true',
340      actual: await zoeIssuer.isLive(alicesSecondLoanInvitation),
341      expected: true
342    })
343  
344  
345    await assert({
346      unitLabel: 'aliceSecondLoanInvitation',
347      given: 'getInvitationDetails',
348      should: 'reflect in terms specified upon the creation of the loan',
349      actual: aliceSecondLoanDetails.customDetails,
350      expected: { ...aliceLoanTerms, id: 'loan-1', expiry: ONE_DAY }
351    });
352  
353  
354    const getSeats = ({ debtSeat, lenderSeat }) => ({
355      debtSeat,
356      lenderSeat
357    });
358  
359  
360    const aliceSecondLoanSeat = await E(zoe).offer(
361      alicesSecondLoanInvitation,
362      {
363        give: { IST: ist(25_000n) },
364        want: {}
365      },
366      { IST: istKit.mint.mintPayment(ist(25_000n)) }
367    );
368  
369  
370    await E(aliceSecondLoanSeat).getOfferResult();
371  
372    const [fundedLoan, secondLoan] = await callPublicFacet(GET_FUNDED_LOANS).then(res => {
373      console.log({ res });
374      return [...res.entries()].map(([key, value]) => {
375        return { key, value };
376      });
377    });
378  
379    await assert({
380      unitLabel: 'fundedLoans Map',
381      given: '.entries()',
382      should:
383        'return an array containing a fundedLoan exo object which corresponds to alices newly funded loan.',
384      actual: fundedLoan.key,
385      expected: firstLoanId
386    });
387  
388  
389    const secondLoanBalances = secondLoan.value.public.getSeatBalances();
390    await assert({
391      unitLabel: 'aliceSecondLoanSeat',
392      given: 'getOfferResult',
393      should:
394        'return a loan object.',
395      actual: secondLoanBalances.debtSeat['IST'],
396      expected: ist(25_000n)
397    });
398  
399    await assert({
400      unitLabel: 'fundedLoans Map',
401      given: 'two funded loans',
402      should:
403        'hold references to both loan.',
404      actual: secondLoan.key,
405      expected: 'loan-1'
406    });
407    const loan0Seats = await fundedLoan.value.public.getSeatBalances();
408  
409    const { debtSeat, lenderSeat } = await getSeats(loan0Seats);
410  
411    await assert({
412      unitLabel: 'fundedLoans Map',
413      given: 'a fundedLoan',
414      should:
415        'expose a method to check the total assets allocated to that particular loan.',
416      actual: debtSeat[istKeyword],
417      expected: sevenThousandIST
418    });
419  
420    const borrowInvitation = fundedLoan.value.invitationMakers.makeFulfillLoan();
421  
422    await assert({
423      unitLabel: 'fundedLoans Map',
424      given: 'a fundedLoan',
425      should:
426        'expose a method that returns an invitation for entering into a loan.',
427      actual: await zoeIssuer.isLive(borrowInvitation),
428      expected: true
429    });
430  
431    const bobSeat = await E(zoe).offer(
432      borrowInvitation,
433      {
434        give: { ATOM: atoms(1000n) },
435        want: { IST: AmountMath.make(istKit.brand, 7000n) }
436      },
437      { ATOM: atomKit.mint.mintPayment(atoms(1000n)) }
438    );
439  
440    const borrowOfferResult = await E(bobSeat).getPayout('IST');
441  
442    await assert({
443      unitLabel: 'BorrowBobSeat',
444      given: 'a live borrowInvitation',
445      should:
446        'successfully exchange his collateral in exchange for the request IST.',
447      actual: await istKit.issuer.getAmountOf(borrowOfferResult),
448      expected: sevenThousandIST
449    });
450  
451    const { loan: loanTopic } = fundedLoan.value.holder.getPublicTopics();
452  
453    const makeStoragePath = rootPath => childPath => `${rootPath}.${childPath}`;
454  
455    const loansStoragePath = makeStoragePath('p2pMetrics.loans');
456  
457    assert({
458      unitLabel: 'fundedLoan Exo',
459      given: 'holder.getPublicTopics()',
460      should: 'return a topic object with the correct storagePath.',
461      actual: await loanTopic.storagePath,
462      expected: loansStoragePath(firstLoanId)
463    });
464  
465    assert({
466      unitLabel: 'fundedLoan Exo',
467      given: 'holder.getPublicTopics()',
468      should: 'return a topic object with the correct description.',
469      actual: loanTopic.description,
470      expected: 'Loan Holder status'
471    });
472  
473    console.log({ loanTopic });
474  
475    // [TG] todo add tests for issuer kit notifications
476    // assert({
477    //   unitLabel: 'fundedLoan Exo',
478    //   given: 'holder.getPublicTopics()',
479    //   should: 'return a topic object with a subscriber object..',
480    //   actual: loanTopic.subscriber,
481    //   expected: 'Loan Holder status'
482    // });
483  
484    await done;
485  });
486  
487  describe('p2p loan - expiration handler - lender claims collateral', async (assert, done) => {
488    const KEYWORDS = {
489      IST: 'IST',
490      ATOM: 'ATOM'
491    };
492  
493    const LOAN_EXPIRY = 5n;
494  
495    const { IST: istKeyword, ATOM: atomKeyword } = KEYWORDS;
496  
497    const { contractInstance, zoe, timerService } = await setupContract(
498      contractPath
499    );
500  
501    const zoeIssuer = await E(zoe).getInvitationIssuer();
502  
503    const callCreatorFacet = callFacet(contractInstance.creatorFacet);
504    const callPublicFacet = callFacet(contractInstance.publicFacet);
505  
506    const {
507      public: {
508        GET_PUBLIC_TOPICS,
509        GET_ISSUERS,
510        GET_FUNDED_LOANS,
511        SAVE_ISSUERS,
512        UNDERWRITE_LOAN
513      }
514    } = facetMethods;
515    const { metrics } = await callPublicFacet(GET_PUBLIC_TOPICS);
516  
517    await assert({
518      unitLabel: 'publicFacet',
519      given: 'getPublicTopics',
520      should: 'return metrics object with 3 keys.',
521      actual: countKeys(metrics),
522      expected: 3
523    });
524  
525    await assert({
526      unitLabel: 'publicFacet',
527      given: 'getPublicTopics',
528      should: 'return metrics object with the correct descriptin.',
529      actual: metrics.description,
530      expected: 'P2P Lending Marketplace Metrics'
531    });
532  
533    await assert({
534      unitLabel: 'E(creatorFacet).saveIssuers()',
535      given: 'an issuer keyword record',
536      should: 'return a success message',
537      actual: await callCreatorFacet(SAVE_ISSUERS, {
538        ATOM: atomKit.issuer,
539        IST: istKit.issuer,
540        BLD: bldKit.issuer,
541        USDC: USDCKit.issuer
542      }),
543      expected: 'Add issuers success!'
544    });
545  
546    const getIssuers = () => callPublicFacet(GET_ISSUERS);
547    const getLength = ({ length }) => length;
548    await assert({
549      unitLabel: 'publicFacet',
550      given: 'contract',
551      should: 'contain an instance',
552      actual: getLength(await getIssuers()),
553      expected: 4
554    });
555  
556    const aliceLoanTerms = {
557      collateralKey: 'ATOM',
558      collateralBrand: atomKit.brand,
559      debtKey: 'IST',
560      debtBrand: istKit.brand,
561      expiry: LOAN_EXPIRY,
562      loanToValue: 70n,
563      interestRate: 5n
564    };
565  
566    const aliceLoanInvitation = await callPublicFacet(
567      UNDERWRITE_LOAN,
568      aliceLoanTerms
569    );
570  
571    assert({
572      unitLabel: 'publicFacet',
573      given: 'underwriteLoan',
574      should: 'prepare the loanAgreeentReducer state with the terms',
575      actual: await zoeIssuer.isLive(aliceLoanInvitation),
576      expected: true
577    });
578  
579    const aliceLoanDetails = await E(zoe).getInvitationDetails(
580      aliceLoanInvitation
581    );
582  
583    const firstLoanId = 'loan-0';
584  
585    await assert({
586      unitLabel: 'aliceLoanInvitation',
587      given: 'getInvitationDetails',
588      should: 'reflect in terms specified upon the creation of the loan',
589      actual: aliceLoanDetails.customDetails,
590      expected: { ...aliceLoanTerms, id: firstLoanId }
591    });
592  
593    const sevenThousandIST = ist(7000n);
594    const aliceLoanSeat = await E(zoe).offer(
595      aliceLoanInvitation,
596      {
597        give: { IST: sevenThousandIST },
598        want: {}
599      },
600      { IST: istKit.mint.mintPayment(sevenThousandIST) }
601    );
602  
603    await E(aliceLoanSeat).getOfferResult();
604  
605    const alicesSecondLoanInvitation = await callPublicFacet(UNDERWRITE_LOAN, {
606      ...aliceLoanTerms,
607      expiry: ONE_DAY
608    });
609    const aliceSecondLoanDetails = await E(zoe).getInvitationDetails(
610      alicesSecondLoanInvitation
611    );
612  
613    await assert({
614      unitLabel: 'aliceSecondLoanInvitation',
615      given: 'getInvitationDetails',
616      should: 'reflect in terms specified upon the creation of the loan',
617      actual: aliceSecondLoanDetails.customDetails,
618      expected: { ...aliceLoanTerms, id: 'loan-1', expiry: ONE_DAY }
619    });
620  
621    const [fundedLoan] = await callPublicFacet(GET_FUNDED_LOANS).then(res => {
622      return [...res.entries()].map(([key, value]) => {
623        return { key, value };
624      });
625    });
626    const borrowInvitation = fundedLoan.value.invitationMakers.makeFulfillLoan();
627  
628    await assert({
629      unitLabel: 'fundedLoans Map',
630      given: 'a fundedLoan',
631      should:
632        'expose a method that returns an invitation for entering into a loan.',
633      actual: await zoeIssuer.isLive(borrowInvitation),
634      expected: true
635    });
636  
637    const bobSeat = await E(zoe).offer(
638      borrowInvitation,
639      {
640        give: { ATOM: atoms(1000n) },
641        want: { IST: AmountMath.make(istKit.brand, 7000n) }
642      },
643      { ATOM: atomKit.mint.mintPayment(atoms(1000n)) }
644    );
645  
646    const borrowOfferResult = await E(bobSeat).getPayout('IST');
647  
648    await assert({
649      unitLabel: 'BorrowBobSeat',
650      given: 'a live borrowInvitation',
651      should:
652        'successfully exchange his collateral in exchange for the request IST.',
653      actual: await istKit.issuer.getAmountOf(borrowOfferResult),
654      expected: sevenThousandIST
655    });
656  
657    // fast forward, to when the loan has expired
658    timerService.tickN(LOAN_EXPIRY);
659    await assert({
660      unitLabel: 'aliceLoanSeeat',
661      given: 'an expired loan',
662      should: 'receive Collateral from the Collateral seat',
663      actual: await atomKit.issuer.getAmountOf(
664        (
665          await E(aliceLoanSeat).getPayouts()
666        ).ATOM
667      ),
668      expected: atoms(1000n)
669    });
670  
671    await assert({
672      given: 'E(bobSeat).hasExited()',
673      should: 'return true following loan expiration.',
674      actual: await E(bobSeat).hasExited(),
675      expected: true
676    });
677  
678    await assert({
679      given: 'E(aliceLoanSeat).hasExited()',
680      should: 'return true following loan expiration.',
681      actual: await E(aliceLoanSeat).hasExited(),
682      expected: true
683    });
684  
685    const bobAtomPayout = await E(bobSeat).getPayout('ATOM');
686  
687    await assert({
688      unitLabel: 'bobLoanSeat',
689      given: 'an expired loan',
690      should: 'be unable to claim his initial collateral.',
691      actual: await atomKit.issuer.getAmountOf(bobAtomPayout),
692      expected: atoms(0n)
693    });
694  
695    await done;
696  });