;
4 |
--------------------------------------------------------------------------------
/components/LoanForm/LoanFormDisclosure/LoanFormDisclosure.tsx:
--------------------------------------------------------------------------------
1 | import { DisclosureContent } from 'reakit/Disclosure';
2 | import { useDisclosureState } from 'reakit/Disclosure';
3 | import { DisclosureButton } from 'components/Button';
4 |
5 | type LoanFormDisclosure = React.PropsWithChildren<{
6 | title: string;
7 | className?: string;
8 | }>;
9 |
10 | export function LoanFormDisclosure({
11 | title,
12 | children,
13 | className,
14 | }: LoanFormDisclosure) {
15 | const disclosure = useDisclosureState({ visible: false });
16 | return (
17 | <>
18 |
19 |
20 | {title}
21 |
22 |
23 |
24 | {(_props) => disclosure.visible && children}
25 |
26 | >
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/components/LoanForm/LoanFormDisclosure/index.ts:
--------------------------------------------------------------------------------
1 | export { LoanFormDisclosure } from './LoanFormDisclosure';
2 |
--------------------------------------------------------------------------------
/components/LoanForm/LoanFormEarlyClosure/index.ts:
--------------------------------------------------------------------------------
1 | export { LoanFormEarlyClosure } from './LoanFormEarlyClosure';
2 |
--------------------------------------------------------------------------------
/components/LoanForm/LoanFormRepay/Explainer.tsx:
--------------------------------------------------------------------------------
1 | import { Explainer as ExplainerWrapper } from 'components/Explainer';
2 |
3 | type ExplainerProps = {
4 | top: number;
5 | };
6 |
7 | export const Explainer = ({ top }: ExplainerProps) => {
8 | return (
9 |
10 |
11 |
12 | );
13 | };
14 |
15 | function Repay() {
16 | return (
17 |
18 | ”Repay and claim” will pay this amount, close the loan, and transfer the
19 | collateral to your wallet.
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/components/LoanForm/LoanFormRepay/index.ts:
--------------------------------------------------------------------------------
1 | export { LoanFormRepay } from './LoanFormRepay';
2 |
--------------------------------------------------------------------------------
/components/LoanForm/LoanFormSeizeCollateral/Explainer.tsx:
--------------------------------------------------------------------------------
1 | import { Explainer as ExplainerWrapper } from 'components/Explainer';
2 |
3 | type ExplainerProps = {
4 | top: number;
5 | };
6 |
7 | export const Explainer = ({ top }: ExplainerProps) => {
8 | return (
9 |
10 |
11 |
12 | );
13 | };
14 |
15 | function SeizeNFT() {
16 | return (
17 |
18 | Because the Borrower has not repaid the loan, as the Lender you have the
19 | opportunity to seize the collateral NFT. If you choose to wait, the
20 | Borrower could still repay, and another Lender could still buy you out.
21 | Seizing the NFT will close the loan.
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/components/LoanForm/LoanFormSeizeCollateral/index.ts:
--------------------------------------------------------------------------------
1 | export { LoanFormSeizeCollateral } from './LoanFormSeizeCollateral';
2 |
--------------------------------------------------------------------------------
/components/LoanForm/LoanOfferBetterTermsDisclosure/LoanOfferBetterTermsDisclosure.tsx:
--------------------------------------------------------------------------------
1 | import { Disclosure, DisclosureContent } from 'reakit/Disclosure';
2 | import { useDisclosureState } from 'reakit/Disclosure';
3 |
4 | type LoanOfferBetterTermsDisclosure = React.PropsWithChildren<{
5 | textWrapperClassName: string;
6 | disclosureTextClassName: string;
7 | className?: string;
8 | }>;
9 |
10 | export function LoanOfferBetterTermsDisclosure({
11 | textWrapperClassName,
12 | disclosureTextClassName,
13 | children,
14 | }: LoanOfferBetterTermsDisclosure) {
15 | const disclosure = useDisclosureState({ visible: false });
16 | return (
17 | <>
18 |
19 | 🎉 You are the current lender! You can still{' '}
20 |
25 | update the loan terms
26 |
27 | , which will reset the loan duration.
28 |
29 |
30 | {(_props) => disclosure.visible && children}
31 |
32 | >
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/components/LoanForm/LoanOfferBetterTermsDisclosure/index.ts:
--------------------------------------------------------------------------------
1 | export { LoanOfferBetterTermsDisclosure } from './LoanOfferBetterTermsDisclosure';
2 |
--------------------------------------------------------------------------------
/components/LoanForm/index.ts:
--------------------------------------------------------------------------------
1 | export { LoanForm } from './LoanForm';
2 |
--------------------------------------------------------------------------------
/components/LoanForm/loanPageFormSchema.ts:
--------------------------------------------------------------------------------
1 | import { MIN_RATE } from 'lib/constants';
2 | import * as Yup from 'yup';
3 |
4 | type LoanPageFormSchemaParams = {
5 | duration: number;
6 | interestRate: number;
7 | loanAmount: number;
8 | };
9 | export const loanPageFormSchema = ({
10 | loanAmount,
11 | interestRate,
12 | duration,
13 | }: LoanPageFormSchemaParams) =>
14 | Yup.object({
15 | loanAmount: Yup.number()
16 | .min(
17 | loanAmount,
18 | `Loan amount must be at least the current term of ${loanAmount}.`,
19 | )
20 | .required(),
21 | interestRate: Yup.number()
22 | .min(MIN_RATE)
23 | .max(
24 | interestRate,
25 | `Interest rate must be no greater than the current term of ${interestRate}.`,
26 | )
27 | .required(),
28 | duration: Yup.number()
29 | .min(
30 | duration,
31 | `Duration must be at least the current term of ${duration} days.`,
32 | )
33 | .required(),
34 | });
35 |
--------------------------------------------------------------------------------
/components/LoanForm/strings.ts:
--------------------------------------------------------------------------------
1 | export const BETTER_TERMS_LABEL = 'Lend at better terms';
2 | export const LEND_LABEL = 'Lend';
3 |
--------------------------------------------------------------------------------
/components/LoanGalleryLoadMore/LoanGalleryLoadMore.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | grid-column: span 12;
3 | display: flex;
4 | align-items: center;
5 | justify-content: center;
6 | margin-bottom: var(--gap);
7 | }
8 |
9 | .container > button {
10 | max-width: 350px;
11 | }
12 |
--------------------------------------------------------------------------------
/components/LoanGalleryLoadMore/LoanGalleryLoadMore.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from 'components/Button';
2 | import React from 'react';
3 | import styles from './LoanGalleryLoadMore.module.css';
4 |
5 | type LoanGalleryLoadMoreProps = {
6 | isLoadingMore?: boolean;
7 | isReachingEnd?: boolean;
8 | loadMore: () => void;
9 | };
10 |
11 | export function LoanGalleryLoadMore({
12 | isLoadingMore,
13 | isReachingEnd,
14 | loadMore,
15 | }: LoanGalleryLoadMoreProps) {
16 | if (isLoadingMore) {
17 | return (
18 |
19 | Loading...
20 |
21 | );
22 | }
23 |
24 | if (isReachingEnd) {
25 | return (
26 |
27 |
28 | That's all, folks
29 |
30 |
31 | );
32 | }
33 |
34 | return (
35 |
36 | Load More
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/components/LoanGalleryLoadMore/index.ts:
--------------------------------------------------------------------------------
1 | export { LoanGalleryLoadMore } from './LoanGalleryLoadMore';
2 |
--------------------------------------------------------------------------------
/components/LoanHeader/LoanHeader.module.css:
--------------------------------------------------------------------------------
1 | .loan-header {
2 | display: flex;
3 | width: 100%;
4 | background: var(--background-white);
5 | justify-content: center;
6 | padding-bottom: 90px;
7 | padding-top: calc(var(--gap) / 2);
8 | }
9 |
10 | .loan-header > div {
11 | max-width: var(--max-width);
12 | }
13 |
14 | .stack {
15 | display: flex;
16 | flex-direction: column;
17 | line-height: 28px;
18 | padding-bottom: 5px;
19 | }
20 |
21 | .conversion {
22 | font-size: var(--font-small);
23 | line-height: var(--font-small);
24 | color: var(--highlight-success-100);
25 | }
26 |
27 | .red {
28 | color: var(--highlight-alert-100);
29 | }
30 |
31 | .media {
32 | grid-column: col-start / span 4;
33 | display: flex;
34 | align-items: flex-start;
35 | justify-content: center;
36 | }
37 |
38 | .form {
39 | grid-column: col-start 5 / -1;
40 | }
41 |
42 | @media screen and (max-width: 700px) {
43 | .media,
44 | .form {
45 | grid-column: span 12;
46 | }
47 |
48 | .loan-header {
49 | padding: calc(var(--gap) / 4) 0;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/components/LoanHeader/index.ts:
--------------------------------------------------------------------------------
1 | export { LoanHeader } from './LoanHeader';
2 |
--------------------------------------------------------------------------------
/components/LoanInfo/LoanInfo.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | display: flex;
3 | justify-content: center;
4 | width: 100%;
5 | position: relative;
6 | padding: 40px 0;
7 | flex-grow: 1;
8 | }
9 |
10 | .wrapper > div:last-child {
11 | max-width: var(--max-width);
12 | z-index: 1;
13 | }
14 |
15 | .mask {
16 | background: var(--background-radial-gradient);
17 | opacity: 0.7;
18 | position: absolute;
19 | top: 0;
20 | bottom: 0;
21 | left: 0;
22 | right: 0;
23 | display: flex;
24 | z-index: 0;
25 | }
26 |
27 | .column {
28 | display: flex;
29 | flex-direction: column;
30 | width: 100%;
31 | gap: var(--gap);
32 | }
33 |
34 | .left-column {
35 | composes: column;
36 | grid-column: col-start / span 4;
37 | }
38 |
39 | .right-column {
40 | composes: column;
41 | grid-column: col-start 5 / -1;
42 | }
43 |
44 | @media screen and (max-width: 700px) {
45 | .left-column,
46 | .right-column {
47 | grid-column: span 12;
48 | gap: 20px;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/components/LoanInfo/index.ts:
--------------------------------------------------------------------------------
1 | export { LoanInfo } from './LoanInfo';
2 |
--------------------------------------------------------------------------------
/components/LoanTable/LoanTable.module.css:
--------------------------------------------------------------------------------
1 | .table {
2 | width: 100%;
3 | background: var(--background-white);
4 | border-collapse: collapse;
5 | grid-column: 1 / -1;
6 | box-shadow: var(--box-shadow);
7 | }
8 |
9 | .header th {
10 | text-align: left;
11 | font-weight: normal;
12 | text-transform: uppercase;
13 | font-size: var(--font-small);
14 | padding: 0.5rem 0;
15 | }
16 |
17 | .table tr {
18 | border: 10px solid #fff;
19 | }
20 |
21 | .table > tbody > tr:hover {
22 | background-color: var(--highlight-clickable-5);
23 | }
24 |
25 | .name-container {
26 | display: grid;
27 | grid-template-columns: 75px 1fr;
28 | grid-template-rows: 75px;
29 | gap: 1rem;
30 | color: var(--neutral-100);
31 | }
32 |
33 | .name-container:visited {
34 | color: var(--neutral-100);
35 | }
36 |
37 | .field-and-subfield {
38 | display: flex;
39 | flex-direction: column;
40 | justify-content: center;
41 | }
42 |
43 | .field-and-subfield > span:last-child {
44 | text-transform: uppercase;
45 | font-size: var(--font-small);
46 | }
47 |
48 | .right,
49 | th.right {
50 | text-align: right;
51 | }
52 |
53 | .rate,
54 | th.rate {
55 | text-align: right;
56 | padding-right: 1rem;
57 | }
58 |
--------------------------------------------------------------------------------
/components/LoanTable/index.ts:
--------------------------------------------------------------------------------
1 | export { LoanTable } from './LoanTable';
2 |
--------------------------------------------------------------------------------
/components/LoanTermsDisclosure/LoanTermsDisclosure.module.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/components/LoanTermsDisclosure/LoanTermsDisclosure.module.css
--------------------------------------------------------------------------------
/components/LoanTermsDisclosure/index.ts:
--------------------------------------------------------------------------------
1 | export { LoanTermsDisclosure } from './LoanTermsDisclosure';
2 |
--------------------------------------------------------------------------------
/components/LoanTickets/LoanTickets.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | display: grid;
3 | grid-auto-flow: column;
4 | grid-column-gap: var(--gap);
5 | grid-auto-columns: minmax(0, 1fr);
6 | }
7 |
8 | .column {
9 | display: flex;
10 | flex-direction: column;
11 | }
12 |
13 | .column > a {
14 | margin-top: 1rem;
15 | }
16 |
--------------------------------------------------------------------------------
/components/LoanTickets/index.ts:
--------------------------------------------------------------------------------
1 | export { LoanTickets } from './LoanTickets';
2 |
--------------------------------------------------------------------------------
/components/Logo/Logo.module.css:
--------------------------------------------------------------------------------
1 | .image {
2 | height: 70px;
3 | width: 70px;
4 | image-rendering: pixelated;
5 | image-rendering: -moz-crisp-edges;
6 | image-rendering: crisp-edges;
7 | }
8 |
--------------------------------------------------------------------------------
/components/Logo/borked-bunny.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/components/Logo/borked-bunny.png
--------------------------------------------------------------------------------
/components/Logo/index.ts:
--------------------------------------------------------------------------------
1 | export { Logo } from './Logo';
2 |
--------------------------------------------------------------------------------
/components/Logo/pepe-bunny-line.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/components/Logo/pepe-bunny-line.png
--------------------------------------------------------------------------------
/components/Marquee/Marquee.module.css:
--------------------------------------------------------------------------------
1 | /* https://reneroth.xyz/marquees-in-css/ */
2 | .container {
3 | overflow: hidden;
4 | white-space: nowrap;
5 | align-items: stretch;
6 | position: absolute;
7 | left: 0;
8 | right: 0;
9 | }
10 |
11 | .scrolling {
12 | text-align: center;
13 | animation: marquee 15s linear infinite;
14 | display: inline-block;
15 | width: 100%;
16 | background: linear-gradient(
17 | 180deg,
18 | rgba(0, 0, 0, 0) calc(50% - 1px),
19 | var(--neutral-100) calc(50%),
20 | rgba(0, 0, 0, 0) calc(50% + 1px)
21 | );
22 | }
23 |
24 | @keyframes marquee {
25 | from {
26 | transform: translateX(0);
27 | }
28 | to {
29 | transform: translateX(-100%);
30 | }
31 | }
32 |
33 | .paused {
34 | animation-play-state: paused;
35 | }
36 |
37 | .wrapper > * {
38 | background: var(--neutral-5);
39 | margin: 0 2rem;
40 | padding: 0 0.25rem;
41 | font-size: var(--font-large);
42 | }
43 |
--------------------------------------------------------------------------------
/components/Marquee/index.ts:
--------------------------------------------------------------------------------
1 | export { Marquee } from './Marquee';
2 |
--------------------------------------------------------------------------------
/components/Media/Fallback.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useMemo, useState } from 'react';
2 | import styles from './Media.module.css';
3 |
4 | const PAWN_SHOP_ITEMS = ['🎸', '💸', '🔭', '🎥', '🛵', '🏺', '💎', '💍'];
5 | const getRandomItem = () => {
6 | return PAWN_SHOP_ITEMS[Math.floor(Math.random() * PAWN_SHOP_ITEMS.length)];
7 | };
8 |
9 | type FallbackProps = {
10 | small?: boolean;
11 | animated?: boolean;
12 | };
13 |
14 | export const Fallback = ({ animated = true, small }: FallbackProps) => {
15 | const [item, setItem] = useState('');
16 | useEffect(() => {
17 | setItem(getRandomItem());
18 | }, [setItem]);
19 |
20 | const className = useMemo(() => {
21 | if (small) {
22 | return styles.smallback;
23 | }
24 |
25 | if (animated) {
26 | return styles['fallback-animated'];
27 | }
28 |
29 | return styles.fallback;
30 | }, [animated, small]);
31 | return (
32 |
33 | {item}
34 |
35 | );
36 | };
37 |
--------------------------------------------------------------------------------
/components/Media/Media.module.css:
--------------------------------------------------------------------------------
1 | .media-content {
2 | max-width: 100%;
3 | }
4 |
5 | .fallback {
6 | width: 100%;
7 | aspect-ratio: 1/1;
8 | display: flex;
9 | align-items: center;
10 | justify-content: center;
11 | font-size: min(5vw, calc(var(--max-width) / 20));
12 | background: #f7f3f1;
13 | }
14 |
15 | .fallback-animated {
16 | composes: fallback;
17 | background-image: linear-gradient(
18 | 110deg,
19 | #f7f3f1 0%,
20 | #f7f3f1 30%,
21 | #fefaf8 50%,
22 | #f7f3f1 70%,
23 | #f7f3f1 100%
24 | );
25 | background-repeat: no-repeat;
26 | background-size: 600px 600px;
27 | position: relative;
28 | animation-duration: 1600ms;
29 | animation-fill-mode: forwards;
30 | animation-iteration-count: infinite;
31 | animation-name: placeholderShimmer;
32 | animation-timing-function: linear;
33 | }
34 |
35 | .smallback {
36 | composes: fallback;
37 | font-size: calc(var(--max-width) / 40);
38 | }
39 |
40 | .fallback > span {
41 | opacity: 20%;
42 | }
43 |
44 | @keyframes placeholderShimmer {
45 | 0% {
46 | background-position: -500px 0;
47 | }
48 |
49 | 100% {
50 | background-position: 500px 0;
51 | }
52 | }
53 |
54 | .image {
55 | width: 100%;
56 | height: 100%;
57 | justify-self: center;
58 | object-fit: contain;
59 | object-position: top;
60 | }
61 |
--------------------------------------------------------------------------------
/components/Media/NFTMedia.tsx:
--------------------------------------------------------------------------------
1 | import { Media } from 'components/Media';
2 | import { Fallback } from './Fallback';
3 | import { MaybeNFTMetadata } from 'hooks/useTokenMetadata';
4 |
5 | interface NFTMediaProps {
6 | nftInfo: MaybeNFTMetadata;
7 | small?: boolean;
8 | }
9 |
10 | export function NFTMedia({ nftInfo, small = false }: NFTMediaProps) {
11 | const { isLoading, metadata } = nftInfo;
12 |
13 | if (!metadata) {
14 | return ;
15 | }
16 |
17 | return (
18 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/components/Media/index.ts:
--------------------------------------------------------------------------------
1 | export { Media } from './Media';
2 |
--------------------------------------------------------------------------------
/components/Modal/index.ts:
--------------------------------------------------------------------------------
1 | export { Modal } from './Modal';
2 |
--------------------------------------------------------------------------------
/components/NFTCollateralPicker/NFTCollateralPicker.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: center;
5 | gap: 1rem;
6 | }
7 |
8 | .nft-group {
9 | display: flex;
10 | flex-direction: column;
11 | width: 100%;
12 | }
13 |
14 | .nft-group-header {
15 | display: flex;
16 | width: 100%;
17 | padding: 18px 22px;
18 | background: var(--neutral-5);
19 | border-radius: var(--border-radius-large);
20 | align-items: center;
21 | cursor: pointer;
22 | }
23 |
24 | .nft-group-header-active {
25 | composes: nft-group-header;
26 | background: var(--highlight-active-10);
27 | }
28 |
29 | .nft-count {
30 | margin-left: auto;
31 | }
32 |
33 | @keyframes append-animate {
34 | from {
35 | transform: scaleY(0);
36 | opacity: 0;
37 | }
38 | to {
39 | transform: scaleY(1);
40 | opacity: 1;
41 | }
42 | }
43 |
44 | .nft-list {
45 | display: grid;
46 | grid-template-columns: repeat(3, 1fr);
47 | gap: 1rem;
48 | margin-top: 1rem;
49 | animation: append-animate 0.3s linear;
50 | }
51 |
--------------------------------------------------------------------------------
/components/NFTExchangeLink/index.ts:
--------------------------------------------------------------------------------
1 | export { NFTExchangeAddressLink } from './NFTExchangeLink';
2 |
--------------------------------------------------------------------------------
/components/NetworkSelector/NetworkSelector.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | z-index: 1000;
3 | }
4 |
--------------------------------------------------------------------------------
/components/NetworkSelector/index.ts:
--------------------------------------------------------------------------------
1 | export { NetworkSelector } from './NetworkSelector';
2 |
--------------------------------------------------------------------------------
/components/NotificationsModal/NotificationsModal.module.css:
--------------------------------------------------------------------------------
1 | .button-row {
2 | display: flex;
3 | justify-content: space-between;
4 | }
5 |
6 | .button-row > button {
7 | width: auto;
8 | }
9 |
10 | .error-message {
11 | color: var(--highlight-alert-100);
12 | font-size: var(--font-small);
13 | }
14 |
--------------------------------------------------------------------------------
/components/NotificationsModal/index.ts:
--------------------------------------------------------------------------------
1 | export { NotificationsModal } from './NotificationsModal';
2 |
--------------------------------------------------------------------------------
/components/OpenGraph/index.ts:
--------------------------------------------------------------------------------
1 | export { OpenGraph } from './OpenGraph';
2 |
--------------------------------------------------------------------------------
/components/PawnArt/index.ts:
--------------------------------------------------------------------------------
1 | export { PawnLoanArt, PawnTicketArt } from './PawnArt';
2 |
--------------------------------------------------------------------------------
/components/PendingCommunityTransactions/PendingCommunityTransactions.module.css:
--------------------------------------------------------------------------------
1 | .wrapper > fieldset {
2 | border-radius: var(--border-radius-large);
3 | max-width: 715px;
4 | margin: 30px auto;
5 | }
6 |
7 | .list {
8 | list-style-type: none;
9 | padding: 0;
10 | font-size: var(--font-small);
11 | }
12 |
13 | .list > li {
14 | margin: 14px 0;
15 | }
16 |
17 | .ipfsLink {
18 | max-width: 70%;
19 | white-space: nowrap;
20 | overflow: hidden;
21 | text-overflow: ellipsis;
22 | }
--------------------------------------------------------------------------------
/components/Profile/BorrowerLenderBubble.tsx:
--------------------------------------------------------------------------------
1 | import { ethers } from 'ethers';
2 | import { useMemo } from 'react';
3 | import { useAccount } from 'wagmi';
4 | import styles from './borrowerLenderBubble.module.css';
5 |
6 | type BubblesProps = {
7 | address: string;
8 | borrower: boolean;
9 | };
10 |
11 | export function BorrowerLenderBubble({ address, borrower }: BubblesProps) {
12 | const { address: connectedAddress } = useAccount();
13 | const isConnectedUser = useMemo(
14 | () =>
15 | connectedAddress && connectedAddress === ethers.utils.getAddress(address),
16 | [connectedAddress, address],
17 | );
18 |
19 | return (
20 |
21 | {isConnectedUser && `You are the ${borrower ? 'borrower' : 'lender'}`}
22 | {!isConnectedUser && (
23 | <>
24 | {borrower ? 'Borrower' : 'Lender'}{' '}
25 | {address.substring(0, 7)}
26 | >
27 | )}
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/components/Profile/NextLoanDueCountdown.tsx:
--------------------------------------------------------------------------------
1 | import { formatTimeDigit, getDaysHoursMinutesSeconds } from 'lib/duration';
2 | import { getNextLoanDue } from 'lib/loans/profileHeaderMethods';
3 | import { useCallback, useEffect, useMemo, useState } from 'react';
4 | import { Loan } from 'types/Loan';
5 |
6 | export function NextLoanDueCountdown({ loans }: { loans: Loan[] }) {
7 | const [remainingSeconds, setRemainingSeconds] = useState(
8 | getNextLoanDue(loans),
9 | );
10 |
11 | const refreshTimestamp = useCallback(
12 | (intervalId) => {
13 | if (remainingSeconds <= 0) {
14 | clearInterval(intervalId);
15 | return;
16 | }
17 | setRemainingSeconds((prev) => prev - 1);
18 | },
19 | [remainingSeconds],
20 | );
21 |
22 | useEffect(() => {
23 | const timeOutId: NodeJS.Timeout = setInterval(
24 | () => refreshTimestamp(timeOutId),
25 | 1000,
26 | );
27 | return () => clearInterval(timeOutId);
28 | }, [refreshTimestamp]);
29 |
30 | const { days, hours, minutes, seconds } = useMemo(
31 | () => getDaysHoursMinutesSeconds(remainingSeconds),
32 | [remainingSeconds],
33 | );
34 |
35 | return (
36 |
37 | {days} days + {formatTimeDigit(hours)}:{formatTimeDigit(minutes)}:
38 | {formatTimeDigit(seconds)}
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/components/Profile/ProfileLoans.tsx:
--------------------------------------------------------------------------------
1 | import { TwelveColumn } from 'components/layouts/TwelveColumn';
2 | import { LoanCard } from 'components/LoanCard';
3 | import { Loan } from 'types/Loan';
4 |
5 | type ProfileLoansProps = {
6 | address: string;
7 | loans: Loan[];
8 | };
9 |
10 | export function ProfileLoans({ address, loans }: ProfileLoansProps) {
11 | return (
12 |
13 | {loans
14 | .filter((loan) => !loan.closed)
15 | .map((loan) => (
16 |
21 | ))}
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/components/Profile/borrowerLenderBubble.module.css:
--------------------------------------------------------------------------------
1 | .bubble {
2 | text-transform: uppercase;
3 | color: black;
4 | font-size: var(--font-small);
5 | padding: 4px 10px;
6 | border-radius: var(--border-radius-small);
7 | }
8 |
9 | .bubble > span {
10 | text-transform: lowercase;
11 | }
12 |
13 | .borrowerBubble {
14 | composes: bubble;
15 | background: var(--highlight-active-10);
16 | }
17 |
18 | .lenderBubble {
19 | composes: bubble;
20 | background: var(--neutral-30);
21 | }
22 |
--------------------------------------------------------------------------------
/components/Profile/profile.module.css:
--------------------------------------------------------------------------------
1 | .profile-header-wrapper {
2 | background: var(--background-white);
3 | width: 100%;
4 | display: flex;
5 | align-items: center;
6 | justify-content: center;
7 | padding: var(--gap) 0;
8 | }
9 |
10 | .profile-header-wrapper > div {
11 | max-width: var(--max-width);
12 | }
13 |
14 | .profile-header-wrapper fieldset {
15 | grid-column: span 4;
16 | max-width: 100%;
17 | display: block;
18 | overflow: hidden;
19 | overflow-wrap: break-word;
20 | padding-left: 0;
21 | padding-right: 0;
22 | }
23 |
24 | .profile-header-wrapper fieldset button {
25 | padding: 0;
26 | text-align: left;
27 | }
28 |
29 | .container {
30 | display: grid;
31 | grid-template-columns: minmax(0, 1fr);
32 | gap: 1rem;
33 | }
34 |
35 | @media screen and (max-width: 700px) {
36 | .profile-header-wrapper {
37 | padding: 0;
38 | }
39 | .profile-header-wrapper fieldset {
40 | grid-column: span 12;
41 | padding: 20px 0;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/components/ProfileActivity/ProfileActivity.module.css:
--------------------------------------------------------------------------------
1 | .event-list {
2 | grid-column: span 12;
3 | list-style-type: none;
4 | background-color: var(--background-white);
5 | padding: var(--padding-loan-card);
6 | margin: 0;
7 | display: flex;
8 | flex-direction: column;
9 | gap: 1rem;
10 | }
11 |
12 | .event {
13 | display: grid;
14 | grid-template-columns: 0.35fr 70px 1fr;
15 | gap: var(--gap);
16 | align-items: center;
17 | justify-content: space-between;
18 | }
19 |
20 | .description-wrapper {
21 | display: flex;
22 | flex-direction: column;
23 | }
24 |
25 | .event-link {
26 | display: flex;
27 | flex-direction: column;
28 | }
29 |
30 | .event-link > span {
31 | font-size: var(--font-small);
32 | }
33 |
34 | .highlight-address {
35 | padding: 0.25rem;
36 | background-color: var(--highlight-active-10);
37 | }
38 |
39 | .description {
40 | font-size: var(--font-small);
41 | }
42 |
43 | @media screen and (max-width: 700px) {
44 | .event-list {
45 | gap: 2rem;
46 | }
47 | .event {
48 | grid-template-columns: 1fr;
49 | gap: 10px;
50 | }
51 |
52 | .event:last-of-type {
53 | border-bottom: none;
54 | padding-bottom: 0;
55 | }
56 |
57 | .event > *:nth-child(2) {
58 | display: none;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/components/ProfileActivity/index.ts:
--------------------------------------------------------------------------------
1 | export { ProfileActivity } from './ProfileActivity';
2 |
--------------------------------------------------------------------------------
/components/RepaymentInfo/RepaymentInfo.module.css:
--------------------------------------------------------------------------------
1 | .maturity-date > div {
2 | display: flex;
3 | flex-direction: row;
4 | align-items: center;
5 | }
6 |
7 | .maturity-date img {
8 | cursor: pointer;
9 | margin: 0 10px;
10 | }
11 |
12 | .maturity-date a {
13 | display: flex;
14 | }
--------------------------------------------------------------------------------
/components/RepaymentInfo/index.ts:
--------------------------------------------------------------------------------
1 | export { RepaymentInfo } from './RepaymentInfo';
2 |
--------------------------------------------------------------------------------
/components/Select/index.ts:
--------------------------------------------------------------------------------
1 | export { Select } from './Select';
2 |
--------------------------------------------------------------------------------
/components/TicketHistory/TicketHistory.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: grid;
3 | grid-template-columns: minmax(0, 1fr);
4 | gap: 1rem;
5 | }
6 |
--------------------------------------------------------------------------------
/components/TicketHistory/TicketHistory.tsx:
--------------------------------------------------------------------------------
1 | import { Fieldset } from 'components/Fieldset';
2 | import { ParsedEvent } from './ParsedEvent';
3 | import { Loan } from 'types/Loan';
4 | import styles from './TicketHistory.module.css';
5 | import type { Event } from 'types/Event';
6 | import useSWR from 'swr';
7 | import { toObjectWithBigNumbers } from 'lib/parseSerializedResponse';
8 | import { useMemo } from 'react';
9 | import { useConfig } from 'hooks/useConfig';
10 |
11 | interface TicketHistoryProps {
12 | loan: Loan;
13 | }
14 |
15 | const fetcher = (url: string) => fetch(url).then((res) => res.json());
16 |
17 | export function TicketHistory({ loan }: TicketHistoryProps) {
18 | const { network } = useConfig();
19 | const { data } = useSWR(
20 | `/api/network/${network}/loans/history/${loan.id}`,
21 | fetcher,
22 | );
23 |
24 | const parsedData = useMemo(
25 | () => (data ? (data.map(toObjectWithBigNumbers) as Event[]) : []),
26 | [data],
27 | );
28 |
29 | return (
30 |
31 |
32 | {!!parsedData &&
33 | parsedData.map((e) => (
34 |
35 | ))}
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/components/TicketHistory/index.ts:
--------------------------------------------------------------------------------
1 | export { TicketHistory } from './TicketHistory';
2 |
--------------------------------------------------------------------------------
/components/Toggle/Toggle.module.css:
--------------------------------------------------------------------------------
1 | .selector {
2 | display: flex;
3 | flex-direction: row;
4 | justify-content: center;
5 | align-items: center;
6 | background: var(--background-white);
7 | border-radius: var(--border-radius-large);
8 | cursor: pointer;
9 | }
10 |
11 | .child {
12 | display: flex;
13 | flex-direction: column;
14 | justify-content: center;
15 | align-items: center;
16 | height: 40px;
17 | padding: 0.5rem 1rem;
18 | border-radius: var(--border-radius-large);
19 | color: var(--neutral-50);
20 | font-size: var(--font-large);
21 | }
22 |
23 | .left {
24 | composes: child;
25 | }
26 |
27 | .right {
28 | composes: child;
29 | }
30 |
31 | .selector[aria-checked='true'] > .left {
32 | background-color: var(--highlight-active-10);
33 | color: var(--neutral-100);
34 | }
35 |
36 | .selector[aria-checked='false'] > .right {
37 | background-color: var(--highlight-active-10);
38 | color: var(--neutral-100);
39 | }
40 |
41 | .selector[aria-checked='true'] > .left:hover,
42 | .selector[aria-checked='false'] > .right:hover {
43 | background-color: var(--highlight-active-20);
44 | }
45 |
46 | .selector[aria-checked='true'] > .right:hover {
47 | color: var(--neutral-100);
48 | }
49 | .selector[aria-checked='false'] > .left:hover {
50 | color: var(--neutral-100);
51 | }
52 |
--------------------------------------------------------------------------------
/components/Toggle/Toggle.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import { Checkbox } from 'reakit/Checkbox';
3 | import styles from './Toggle.module.css';
4 |
5 | type ToggleProps = {
6 | handleChange: (checked: boolean) => void;
7 | left: React.ReactNode;
8 | right: React.ReactNode;
9 | initialChecked?: boolean;
10 | };
11 |
12 | export const Toggle = ({
13 | handleChange,
14 | initialChecked = true,
15 | left,
16 | right,
17 | }: ToggleProps) => {
18 | const [checked, setChecked] = React.useState(initialChecked);
19 | const toggle = useCallback(() => {
20 | setChecked(!checked);
21 | handleChange(!checked);
22 | }, [checked, handleChange, setChecked]);
23 | return (
24 |
29 | {left}
30 | {right}
31 |
32 | );
33 | };
34 |
--------------------------------------------------------------------------------
/components/Toggle/index.ts:
--------------------------------------------------------------------------------
1 | export { Toggle } from './Toggle';
2 |
--------------------------------------------------------------------------------
/components/layouts/AppWrapper.module.css:
--------------------------------------------------------------------------------
1 | .app-wrapper {
2 | display: flex;
3 | flex-direction: column;
4 | width: 100%;
5 | min-height: 100vh;
6 | align-items: center;
7 | background: var(--background-radial-gradient);
8 | }
9 |
10 | .app-wrapper-community {
11 | composes: app-wrapper;
12 | }
13 |
14 | .app-wrapper-community > nav {
15 | background-color: transparent;
16 | }
17 |
--------------------------------------------------------------------------------
/components/layouts/AppWrapper.tsx:
--------------------------------------------------------------------------------
1 | import { useCommunityGradient } from 'hooks/useCommunityGradient';
2 | import { useRouter } from 'next/router';
3 | import React, { FunctionComponent, useMemo } from 'react';
4 | import styles from './AppWrapper.module.css';
5 |
6 | function communityGradient(start: string, finish: string) {
7 | return `radial-gradient(
8 | 52.94% 52.36% at 28.53% 25.3%,
9 | ${start} 26.04%,
10 | ${finish} 100%
11 | )`;
12 | }
13 |
14 | export const AppWrapper: FunctionComponent = ({ children }) => {
15 | const { pathname } = useRouter();
16 | const { from, to } = useCommunityGradient();
17 |
18 | const isCommunityPage = useMemo(
19 | () => pathname.startsWith('/community'),
20 | [pathname],
21 | );
22 | const computedStyles = useMemo(() => {
23 | if (isCommunityPage) {
24 | return {
25 | background: communityGradient(from, to),
26 | };
27 | }
28 |
29 | return {};
30 | }, [isCommunityPage, from, to]);
31 |
32 | return (
33 |
40 | {children}
41 |
42 | );
43 | };
44 |
--------------------------------------------------------------------------------
/components/layouts/FormWrapper.module.css:
--------------------------------------------------------------------------------
1 | .form-wrapper {
2 | display: grid;
3 | grid-template-columns: 1fr;
4 | row-gap: 0.75rem;
5 | }
6 |
--------------------------------------------------------------------------------
/components/layouts/FormWrapper.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent } from 'react';
2 | import styles from './FormWrapper.module.css';
3 |
4 | export const FormWrapper: FunctionComponent = ({ children }) => {
5 | return {children}
;
6 | };
7 |
--------------------------------------------------------------------------------
/components/layouts/ThreeColumn.module.css:
--------------------------------------------------------------------------------
1 | .grid {
2 | display: grid;
3 | grid-template-columns: repeat(3, 1fr);
4 | gap: var(--gap);
5 | width: 100%;
6 | }
7 |
8 | @media (max-width: 42em) {
9 | .grid {
10 | /* On mobile devices we only want one column. */
11 | grid-template-columns: 1fr;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/components/layouts/ThreeColumn.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent } from 'react';
2 |
3 | import styles from './ThreeColumn.module.css';
4 |
5 | export const ThreeColumn: FunctionComponent = ({ children }) => (
6 | {children}
7 | );
8 |
--------------------------------------------------------------------------------
/components/layouts/TwelveColumn/TwelveColumn.module.css:
--------------------------------------------------------------------------------
1 | .grid {
2 | display: grid;
3 | grid-template-columns: repeat(12, [col-start] 1fr);
4 | gap: var(--gap);
5 | margin: 0 auto;
6 | width: 100%;
7 | max-width: var(--max-width);
8 | }
9 |
10 | .padded-grid {
11 | composes: grid;
12 | padding: var(--gap) 0;
13 | }
14 |
15 | @media screen and (max-width: 700px) {
16 | .grid {
17 | gap: 0;
18 | row-gap: 20px;
19 | padding: 20px;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/components/layouts/TwelveColumn/TwelveColumn.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './TwelveColumn.module.css';
3 |
4 | type TwelveColumnProps = React.PropsWithChildren<{
5 | padded?: boolean;
6 | }>;
7 |
8 | export const TwelveColumn = React.forwardRef(
9 | ({ children, padded }, ref) => {
10 | return (
11 |
12 | {children}
13 |
14 | );
15 | },
16 | );
17 |
18 | TwelveColumn.displayName = 'TwelveColumn';
19 |
--------------------------------------------------------------------------------
/components/layouts/TwelveColumn/index.ts:
--------------------------------------------------------------------------------
1 | export { TwelveColumn } from './TwelveColumn';
2 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | postgres:
4 | image: 'postgres:12.8'
5 | environment:
6 | - POSTGRES_DB=notifications
7 | - POSTGRES_USER=user
8 | - POSTGRES_HOST=postgres
9 | - POSTGRES_PORT=5432
10 | - POSTGRES_PASSWORD=password
11 | volumes:
12 | - db:/var/lib/postgresql/data
13 | ports:
14 | - 5432:5432
15 | volumes:
16 | db:
17 | driver: local
--------------------------------------------------------------------------------
/global-setup.js:
--------------------------------------------------------------------------------
1 | module.exports = async () => {
2 | process.env.TZ = 'America/New_York';
3 | };
4 |
--------------------------------------------------------------------------------
/graphql/community/accessoriesQuery.graphql:
--------------------------------------------------------------------------------
1 | query accessories {
2 | accessories {
3 | id
4 | name
5 | contractAddress
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/graphql/community/communityAccountQuery.graphql:
--------------------------------------------------------------------------------
1 | query communityAccount($address: ID!) {
2 | account(id: $address) {
3 | id
4 | token {
5 | id
6 | uri
7 | mintedAt
8 | }
9 | categoryScoreChanges {
10 | id
11 | timestamp
12 | blockNumber
13 | category {
14 | id
15 | name
16 | }
17 | newScore
18 | oldScore
19 | ipfsEntryHash
20 | ipfsLink
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/graphql/eip721/queries/useNFTquery.graphql:
--------------------------------------------------------------------------------
1 | query Nfts($address: ID!) {
2 | account(id: $address) {
3 | id
4 | tokens {
5 | id
6 | identifier
7 | uri
8 | registry {
9 | name
10 | }
11 | approvals {
12 | id
13 | approved {
14 | id
15 | }
16 | }
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/graphql/nftLoans/fragments/allLoanProperties.graphql:
--------------------------------------------------------------------------------
1 | fragment allLoanProperties on Loan {
2 | id
3 | loanAssetContractAddress
4 | collateralContractAddress
5 | collateralTokenId
6 | perAnumInterestRate
7 | accumulatedInterest
8 | lastAccumulatedTimestamp
9 | durationSeconds
10 | loanAmount
11 | status
12 | closed
13 | loanAssetDecimal
14 | loanAssetSymbol
15 | lendTicketHolder
16 | borrowTicketHolder
17 | endDateTimestamp
18 | collateralTokenURI
19 | collateralName
20 | createdAtTimestamp
21 | lastUpdatedAtTimestamp
22 | numEvents
23 | allowLoanAmountIncrease
24 | }
25 |
--------------------------------------------------------------------------------
/graphql/nftLoans/fragments/eventProperties.graphql:
--------------------------------------------------------------------------------
1 | fragment buyoutEventProperties on BuyoutEvent {
2 | id
3 | newLender
4 | lendTicketHolder
5 | loanAmount
6 | interestEarned
7 | timestamp
8 | blockNumber
9 | }
10 |
11 | fragment closeEventProperties on CloseEvent {
12 | id
13 | timestamp
14 | blockNumber
15 | closer
16 | }
17 |
18 | fragment collateralSeizureEventProperties on CollateralSeizureEvent {
19 | id
20 | timestamp
21 | blockNumber
22 | lendTicketHolder
23 | borrowTicketHolder
24 | }
25 |
26 | fragment createEventProperties on CreateEvent {
27 | id
28 | timestamp
29 | blockNumber
30 | creator
31 | maxPerAnumInterestRate
32 | minDurationSeconds
33 | minLoanAmount
34 | allowLoanAmountIncrease
35 | }
36 |
37 | fragment lendEventProperties on LendEvent {
38 | id
39 | lender
40 | loanAmount
41 | durationSeconds
42 | perAnumInterestRate
43 | timestamp
44 | blockNumber
45 | borrowTicketHolder
46 | }
47 |
48 | fragment repaymentEventProperties on RepaymentEvent {
49 | id
50 | repayer
51 | loanAmount
52 | interestEarned
53 | timestamp
54 | blockNumber
55 | lendTicketHolder
56 | borrowTicketHolder
57 | }
58 |
--------------------------------------------------------------------------------
/graphql/nftLoans/queries/allEventsQueries.graphql:
--------------------------------------------------------------------------------
1 | query allCreateEvents($where: CreateEvent_filter) {
2 | createEvents(where: $where) {
3 | ...createEventProperties
4 | loan {
5 | ...allLoanProperties
6 | }
7 | }
8 | }
9 |
10 | query allBuyoutEvents($where: BuyoutEvent_filter) {
11 | buyoutEvents(where: $where) {
12 | ...buyoutEventProperties
13 | loan {
14 | ...allLoanProperties
15 | }
16 | }
17 | }
18 |
19 | query allLendEvents($where: LendEvent_filter) {
20 | lendEvents(where: $where) {
21 | ...lendEventProperties
22 | loan {
23 | ...allLoanProperties
24 | }
25 | }
26 | }
27 |
28 | query allRepaymentEvents($where: RepaymentEvent_filter) {
29 | repaymentEvents(where: $where) {
30 | ...repaymentEventProperties
31 | loan {
32 | ...allLoanProperties
33 | }
34 | }
35 | }
36 |
37 | query allCollateralSeizureEvents($where: CollateralSeizureEvent_filter) {
38 | collateralSeizureEvents(where: $where) {
39 | ...collateralSeizureEventProperties
40 | loan {
41 | ...allLoanProperties
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/graphql/nftLoans/queries/allLoansQuery.graphql:
--------------------------------------------------------------------------------
1 | query allLoans(
2 | $where: Loan_filter
3 | $first: Int
4 | $skip: Int
5 | $orderBy: Loan_orderBy
6 | $orderDirection: OrderDirection
7 | ) {
8 | loans(
9 | where: $where
10 | orderBy: $orderBy
11 | orderDirection: $orderDirection
12 | first: $first
13 | skip: $skip
14 | ) {
15 | ...allLoanProperties
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/graphql/nftLoans/queries/eventsForLoan.graphql:
--------------------------------------------------------------------------------
1 | query eventsForLoan($id: ID!) {
2 | loan(id: $id) {
3 | createEvent {
4 | ...createEventProperties
5 | }
6 | lendEvents {
7 | ...lendEventProperties
8 | }
9 | buyoutEvents {
10 | ...buyoutEventProperties
11 | }
12 | repaymentEvent {
13 | ...repaymentEventProperties
14 | }
15 | collateralSeizureEvent {
16 | ...collateralSeizureEventProperties
17 | }
18 | closeEvent {
19 | ...closeEventProperties
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/graphql/nftLoans/queries/homepageSearchQuery.graphql:
--------------------------------------------------------------------------------
1 | query homepageSearch(
2 | $statuses: [LoanStatus!]
3 | $collateralContractAddress: Bytes
4 | $collateralName: String
5 | $loanAssetSymbol: String
6 | $borrowTicketHolder: Bytes
7 | $lendTicketHolder: Bytes
8 | $loanAmountMin: BigInt
9 | $loanAmountMax: BigInt
10 | $perAnumInterestRateMin: BigInt
11 | $perAnumInterestRateMax: BigInt
12 | $durationSecondsMin: BigInt
13 | $durationSecondsMax: BigInt
14 | $selectedSort: Loan_orderBy
15 | $sortDirection: OrderDirection
16 | $first: Int
17 | $skip: Int
18 | ) {
19 | loans(
20 | where: {
21 | status_in: $statuses,
22 | collateralContractAddress_contains: $collateralContractAddress,
23 | collateralName_contains: $collateralName,
24 | loanAssetSymbol_contains: $loanAssetSymbol,
25 | borrowTicketHolder_contains: $borrowTicketHolder,
26 | lendTicketHolder_contains: $lendTicketHolder,
27 | loanAmount_gte: $loanAmountMin,
28 | loanAmount_lt: $loanAmountMax,
29 | perAnumInterestRate_gte: $perAnumInterestRateMin,
30 | perAnumInterestRate_lt: $perAnumInterestRateMax,
31 | durationSeconds_gte: $durationSecondsMin,
32 | durationSeconds_lt: $durationSecondsMax
33 | }
34 | orderBy: $selectedSort
35 | orderDirection: $sortDirection
36 | first: $first
37 | skip: $skip
38 | ) {
39 | ...allLoanProperties
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/graphql/nftLoans/queries/homepageSearchQueryWithoutLender.graphql:
--------------------------------------------------------------------------------
1 | query homepageSearchWithoutLender(
2 | $statuses: [LoanStatus!]
3 | $collateralContractAddress: Bytes
4 | $collateralName: String
5 | $loanAssetSymbol: String
6 | $borrowTicketHolder: Bytes
7 | $loanAmountMin: BigInt
8 | $loanAmountMax: BigInt
9 | $perAnumInterestRateMin: BigInt
10 | $perAnumInterestRateMax: BigInt
11 | $durationSecondsMin: BigInt
12 | $durationSecondsMax: BigInt
13 | $selectedSort: Loan_orderBy
14 | $sortDirection: OrderDirection
15 | $first: Int
16 | $skip: Int
17 | ) {
18 | loans(
19 | where: {
20 | status_in: $statuses,
21 | collateralContractAddress_contains: $collateralContractAddress,
22 | collateralName_contains: $collateralName,
23 | loanAssetSymbol_contains: $loanAssetSymbol,
24 | borrowTicketHolder_contains: $borrowTicketHolder,
25 | loanAmount_gte: $loanAmountMin,
26 | loanAmount_lt: $loanAmountMax,
27 | perAnumInterestRate_gte: $perAnumInterestRateMin,
28 | perAnumInterestRate_lt: $perAnumInterestRateMax,
29 | durationSeconds_gte: $durationSecondsMin,
30 | durationSeconds_lt: $durationSecondsMax
31 | }
32 | orderBy: $selectedSort
33 | orderDirection: $sortDirection
34 | first: $first
35 | skip: $skip
36 | ) {
37 | ...allLoanProperties
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/graphql/nftLoans/queries/loanById.graphql:
--------------------------------------------------------------------------------
1 | query loanById($id: ID!) {
2 | loan(id: $id) {
3 | ...allLoanProperties
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/graphql/nftLoans/queries/notificationEvents.graphql:
--------------------------------------------------------------------------------
1 | query createByTransactionHash($id: ID!) {
2 | createEvent(id: $id) {
3 | ...createEventProperties
4 | loan {
5 | ...allLoanProperties
6 | }
7 | }
8 | }
9 |
10 | query buyoutByTransactionHash($id: ID!) {
11 | buyoutEvent(id: $id) {
12 | ...buyoutEventProperties
13 | loan {
14 | ...allLoanProperties
15 | }
16 | }
17 | }
18 |
19 | query lendByTransactionHash($id: ID!) {
20 | lendEvent(id: $id) {
21 | ...lendEventProperties
22 | loan {
23 | ...allLoanProperties
24 | }
25 | }
26 | }
27 |
28 | query repaymentEventByTransactionHash($id: ID!) {
29 | repaymentEvent(id: $id) {
30 | ...repaymentEventProperties
31 | loan {
32 | ...allLoanProperties
33 | }
34 | }
35 | }
36 |
37 | query collateralSeizureEventByTransactionHash($id: ID!) {
38 | collateralSeizureEvent(id: $id) {
39 | ...collateralSeizureEventProperties
40 | loan {
41 | ...allLoanProperties
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/graphql/nftSales/fragments/allSaleProperties.graphql:
--------------------------------------------------------------------------------
1 | fragment allSaleProperties on Sale {
2 | id
3 | nftContractAddress
4 | nftTokenId
5 | saleType
6 | blockNumber
7 | timestamp
8 | seller
9 | buyer
10 | exchange
11 | paymentToken
12 | price
13 | }
14 |
--------------------------------------------------------------------------------
/graphql/nftSales/queries/salesByAddress.graphql:
--------------------------------------------------------------------------------
1 | query salesByAddress(
2 | $nftContractAddress: Bytes
3 | $nftTokenId: String!
4 | $first: Int!
5 | $orderBy: Sale_orderBy
6 | $orderDirection: OrderDirection
7 | ) {
8 | sales(
9 | where: { nftContractAddress: $nftContractAddress, nftTokenId: $nftTokenId }
10 | first: $first
11 | orderBy: $orderBy
12 | orderDirection: $orderDirection
13 | ) {
14 | ...allSaleProperties
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/hooks/useBalance/index.ts:
--------------------------------------------------------------------------------
1 | export { useBalance } from './useBalance';
2 |
--------------------------------------------------------------------------------
/hooks/useCommunityGradient/index.ts:
--------------------------------------------------------------------------------
1 | export {
2 | CommunityGradientContext,
3 | CommunityGradientProvider,
4 | useCommunityGradient,
5 | } from './useCommunityGradient';
6 |
--------------------------------------------------------------------------------
/hooks/useConfig/index.ts:
--------------------------------------------------------------------------------
1 | export { ConfigProvider, useConfig } from './useConfig';
2 |
--------------------------------------------------------------------------------
/hooks/useConfig/useConfig.tsx:
--------------------------------------------------------------------------------
1 | import { Config, configs, SupportedNetwork } from 'lib/config';
2 | import { useRouter } from 'next/router';
3 | import { createContext, FunctionComponent, useContext, useMemo } from 'react';
4 |
5 | const ConfigContext = createContext(null);
6 |
7 | type ConfigProviderProps = {
8 | network: SupportedNetwork;
9 | };
10 | export const ConfigProvider: FunctionComponent = ({
11 | children,
12 | network,
13 | }) => {
14 | return (
15 |
16 | {children}
17 |
18 | );
19 | };
20 |
21 | export function useConfig(): Config {
22 | const config = useContext(ConfigContext);
23 | const { pathname } = useRouter();
24 | const isCommunityPage = useMemo(
25 | () => pathname.startsWith('/community'),
26 | [pathname],
27 | );
28 |
29 | if (isCommunityPage) {
30 | return configs.optimism;
31 | }
32 |
33 | return config!;
34 | }
35 |
--------------------------------------------------------------------------------
/hooks/useGlobalMessages/index.ts:
--------------------------------------------------------------------------------
1 | export {
2 | useGlobalMessages,
3 | GlobalMessagingProvider,
4 | } from './useGlobalMessages';
5 |
6 | export type { Message } from './useGlobalMessages';
7 |
--------------------------------------------------------------------------------
/hooks/useHasCollapsedHeaderInfo/index.ts:
--------------------------------------------------------------------------------
1 | export {
2 | useHasCollapsedHeaderInfo,
3 | HasCollapsedHeaderInfoProvider,
4 | } from './useHasCollapsedHeaderInfo';
5 |
--------------------------------------------------------------------------------
/hooks/useHasCollapsedHeaderInfo/useHasCollapsedHeaderInfo.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useCallback, useContext, useState } from 'react';
2 |
3 | const hasCollapsedHeaderInfoContext = createContext<{
4 | hasCollapsed: boolean;
5 | setHasCollapsed: (value: boolean) => void;
6 | }>({} as any);
7 |
8 | export function HasCollapsedHeaderInfoProvider({
9 | children,
10 | }: React.PropsWithChildren<{}>) {
11 | const [hasCollapsed, setHasCollapsed] = useState(false);
12 | const { Provider } = hasCollapsedHeaderInfoContext;
13 |
14 | const value = { hasCollapsed, setHasCollapsed };
15 | return {children} ;
16 | }
17 |
18 | export function useHasCollapsedHeaderInfo() {
19 | const { hasCollapsed, setHasCollapsed } = useContext(
20 | hasCollapsedHeaderInfoContext,
21 | );
22 |
23 | const onCollapse = useCallback(
24 | () => setHasCollapsed(true),
25 | [setHasCollapsed],
26 | );
27 |
28 | return { hasCollapsed, onCollapse };
29 | }
30 |
--------------------------------------------------------------------------------
/hooks/useKonami/index.ts:
--------------------------------------------------------------------------------
1 | export { useKonami } from './useKonami';
2 |
--------------------------------------------------------------------------------
/hooks/useKonami/useKonami.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | const konami = [
4 | 'ArrowUp',
5 | 'ArrowUp',
6 | 'ArrowDown',
7 | 'ArrowDown',
8 | 'ArrowLeft',
9 | 'ArrowRight',
10 | 'ArrowLeft',
11 | 'ArrowRight',
12 | 'KeyB',
13 | 'KeyA',
14 | 'Enter',
15 | ];
16 |
17 | export function useKonami() {
18 | const [index, setIndex] = useState(0);
19 | const [codeActive, setCodeActive] = useState(false);
20 |
21 | useEffect(() => {
22 | function handleKeyDown(ev: KeyboardEvent) {
23 | setIndex((prev) => {
24 | if (ev.code === konami[prev]) {
25 | return ++prev;
26 | }
27 | return 0;
28 | });
29 | }
30 | document.addEventListener('keydown', handleKeyDown);
31 |
32 | return () => document.removeEventListener('keydown', handleKeyDown);
33 | }, []);
34 |
35 | useEffect(() => {
36 | if (index >= konami.length) {
37 | setIndex(0);
38 | setCodeActive((prev) => !prev);
39 | }
40 | }, [index]);
41 |
42 | return codeActive;
43 | }
44 |
--------------------------------------------------------------------------------
/hooks/useLTV/index.ts:
--------------------------------------------------------------------------------
1 | export { useLTV } from './useLTV';
2 |
--------------------------------------------------------------------------------
/hooks/useLoanDetails/index.ts:
--------------------------------------------------------------------------------
1 | export { useLoanDetails } from './useLoanDetails';
2 |
--------------------------------------------------------------------------------
/hooks/useLoanUnderwriter/index.ts:
--------------------------------------------------------------------------------
1 | export { useLoanUnderwriter } from './useLoanUnderwriter';
2 |
--------------------------------------------------------------------------------
/hooks/useLoanViewerRole/index.ts:
--------------------------------------------------------------------------------
1 | export { useLoanViewerRole } from './useLoanViewerRole';
2 |
--------------------------------------------------------------------------------
/hooks/useLoanViewerRole/useLoanViewerRole.ts:
--------------------------------------------------------------------------------
1 | import { Loan } from '../../types/Loan';
2 | import { useMemo } from 'react';
3 |
4 | export const useLoanViewerRole = (loan: Loan, account?: string | null) =>
5 | useMemo(() => {
6 | if (!account) {
7 | return null;
8 | } else if (account.toUpperCase() === loan.lender?.toUpperCase()) {
9 | return 'lender';
10 | } else if (account.toUpperCase() === loan.borrower.toUpperCase()) {
11 | return 'borrower';
12 | }
13 | return null;
14 | }, [account, loan.lender, loan.borrower]);
15 |
--------------------------------------------------------------------------------
/hooks/useNFTs/index.ts:
--------------------------------------------------------------------------------
1 | export { useNFTs } from './useNFTs';
2 |
--------------------------------------------------------------------------------
/hooks/useNFTs/useNFTs.ts:
--------------------------------------------------------------------------------
1 | import { captureException } from '@sentry/nextjs';
2 | import { ethers } from 'ethers';
3 | import { useEffect } from 'react';
4 | import {
5 | NftsQuery,
6 | NftsDocument,
7 | } from 'types/generated/graphql/eip721subgraph';
8 | import { NFTEntity } from 'types/NFT';
9 | import { useQuery } from 'urql';
10 |
11 | export const useNFTs = (address: string) => {
12 | const [result] = useQuery({
13 | query: NftsDocument,
14 | variables: {
15 | address: address.toLowerCase(),
16 | },
17 | });
18 |
19 | const { data, fetching, error } = result;
20 |
21 | const rawNfts = data?.account?.tokens || [];
22 |
23 | const nfts: NFTEntity[] = rawNfts.map((nft) => ({
24 | ...nft,
25 | identifier: ethers.BigNumber.from(nft.identifier),
26 | }));
27 |
28 | useEffect(() => {
29 | if (error) {
30 | captureException(error);
31 | }
32 | }, [error]);
33 |
34 | return { fetching, error, nfts };
35 | };
36 |
--------------------------------------------------------------------------------
/hooks/useNetworkSpecificStyles/index.ts:
--------------------------------------------------------------------------------
1 | export { useNetworkSpecificStyles } from './useNetworkSpecificStyles';
2 |
--------------------------------------------------------------------------------
/hooks/useNetworkSpecificStyles/useNetworkSpecificStyles.ts:
--------------------------------------------------------------------------------
1 | import { SupportedNetwork } from 'lib/config';
2 | import { useEffect } from 'react';
3 |
4 | type StyleRule = [variableName: string, style: string];
5 |
6 | const RADIAL_NAME = '--background-radial-gradient';
7 | const RADIAL_DEFAULT = 'radial-gradient(#ffffff, #f6f2f0)';
8 | const BASE_RADIAL: StyleRule = [RADIAL_NAME, RADIAL_DEFAULT];
9 |
10 | export const STYLE_MAP: { [network in SupportedNetwork]: StyleRule[] } = {
11 | ethereum: [BASE_RADIAL],
12 | rinkeby: [BASE_RADIAL],
13 | optimism: [[RADIAL_NAME, 'var(--optimistic-red-10)']],
14 | polygon: [[RADIAL_NAME, 'var(--polygon-purple-10)']],
15 | };
16 |
17 | export function useNetworkSpecificStyles(network: SupportedNetwork) {
18 | useEffect(() => {
19 | const rules = STYLE_MAP[network];
20 | rules.forEach(([variableName, style]) => {
21 | document.documentElement.style.setProperty(variableName, style);
22 | });
23 | }, [network]);
24 | }
25 |
--------------------------------------------------------------------------------
/hooks/useOnClickOutside/index.ts:
--------------------------------------------------------------------------------
1 | export { useOnClickOutside } from './useOnClickOutside';
2 |
--------------------------------------------------------------------------------
/hooks/useOnClickOutside/useOnClickOutside.ts:
--------------------------------------------------------------------------------
1 | import { RefObject, useEffect } from 'react';
2 |
3 | type Handler = (event: MouseEvent) => void;
4 |
5 | export function useOnClickOutside(
6 | ref: RefObject,
7 | handler: Handler,
8 | mouseEvent: 'mousedown' | 'mouseup' = 'mousedown',
9 | ): void {
10 | useEffect(() => {
11 | const listener = (event: MouseEvent) => {
12 | if (!ref.current || ref.current.contains(event.target as Node)) {
13 | return;
14 | }
15 | handler(event);
16 | };
17 | document.addEventListener(mouseEvent, listener);
18 | return () => {
19 | document.removeEventListener(mouseEvent, listener);
20 | };
21 | }, [ref, handler, mouseEvent]);
22 | }
23 |
24 | export default useOnClickOutside;
25 |
--------------------------------------------------------------------------------
/hooks/useOnScreenRef.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, RefObject } from 'react';
2 |
3 | // Hook
4 | export function useOnScreen(ref: RefObject, rootMargin = '0px') {
5 | // State and setter for storing whether element is visible
6 | const [isIntersecting, setIntersecting] = useState(false);
7 | useEffect(() => {
8 | const current = ref.current;
9 | const observer = new IntersectionObserver(
10 | ([entry]) => {
11 | // Update our state when observer callback fires
12 | setIntersecting(entry.isIntersecting);
13 | },
14 | {
15 | rootMargin,
16 | },
17 | );
18 | if (current) {
19 | observer.observe(current);
20 | }
21 | return () => {
22 | if (current) {
23 | observer.unobserve(current);
24 | }
25 | };
26 | }, [ref, rootMargin]);
27 | return isIntersecting;
28 | }
29 |
--------------------------------------------------------------------------------
/hooks/useTimestamp/index.ts:
--------------------------------------------------------------------------------
1 | export { useTimestamp } from './useTimestamp';
2 |
--------------------------------------------------------------------------------
/hooks/useTokenMetadata/index.ts:
--------------------------------------------------------------------------------
1 | export { useTokenMetadata } from './useTokenMetadata';
2 | export type { MaybeNFTMetadata, CollateralSpec } from './useTokenMetadata';
3 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | const nextJest = require('next/jest');
2 |
3 | const createJestConfig = nextJest({
4 | dir: './',
5 | });
6 |
7 | const customJestConfig = {
8 | collectCoverageFrom: [
9 | 'components/**/*.{js,jsx,ts,tsx}',
10 | 'hooks/**/*.{js,jsx,ts,tsx}',
11 | 'lib/**/*.{js,jsx,ts,tsx}',
12 | '!**/index.ts',
13 | '!**/*.d.ts',
14 | '!**/node_modules/**',
15 | ],
16 | coverageReporters: ['json-summary', 'text'],
17 | coverageThreshold: {
18 | global: {
19 | branches: 31,
20 | functions: 37.5,
21 | lines: 46,
22 | statements: 48.5,
23 | },
24 | },
25 | moduleDirectories: ['node_modules', ''],
26 | setupFilesAfterEnv: ['/jest.setup.js'],
27 | testEnvironment: 'jsdom',
28 | testPathIgnorePatterns: ['/pages/'],
29 | globalSetup: '/global-setup.js',
30 | };
31 |
32 | module.exports = createJestConfig(customJestConfig);
33 |
--------------------------------------------------------------------------------
/jest.setup.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import dontenv from 'dotenv';
3 | import '@testing-library/jest-dom/extend-expect';
4 | import fetchMock from 'jest-fetch-mock';
5 | import { TextEncoder, TextDecoder } from 'util';
6 | import { configs } from './lib/config';
7 |
8 | // Minimize console output when testing failure cases
9 | global.console.error = jest.fn();
10 |
11 | dontenv.config({ path: path.resolve(__dirname, './.env.test') });
12 |
13 | global.fetch = fetchMock;
14 | global.TextEncoder = TextEncoder;
15 | global.TextDecoder = TextDecoder;
16 |
17 | jest.mock('./hooks/useConfig', () => ({
18 | ...jest.requireActual('./hooks/useConfig'),
19 | useConfig: jest.fn(() => configs.rinkeby),
20 | }));
21 |
--------------------------------------------------------------------------------
/lib/authorizations/authorizeCurrency.ts:
--------------------------------------------------------------------------------
1 | import { captureException } from '@sentry/nextjs';
2 | import { ethers, Signer } from 'ethers';
3 | import { SupportedNetwork } from 'lib/config';
4 | import { contractDirectory, web3Erc20Contract } from 'lib/contracts';
5 |
6 | type AllowParams = {
7 | callback: () => void;
8 | contractAddress: string;
9 | network: SupportedNetwork;
10 | signer: Signer;
11 | setTxHash: (value: string) => void;
12 | setWaitingForTx: (value: boolean) => void;
13 | };
14 | export async function authorizeCurrency({
15 | callback,
16 | contractAddress,
17 | network,
18 | signer,
19 | setTxHash,
20 | setWaitingForTx,
21 | }: AllowParams) {
22 | const contract = web3Erc20Contract(contractAddress, signer);
23 | const t = await contract.approve(
24 | contractDirectory[network].loanFacilitator,
25 | ethers.BigNumber.from(2).pow(256).sub(1),
26 | );
27 | setWaitingForTx(true);
28 | setTxHash(t.hash);
29 | t.wait()
30 | .then(() => {
31 | callback();
32 | setWaitingForTx(false);
33 | })
34 | .catch((err) => {
35 | setWaitingForTx(false);
36 | captureException(err);
37 | });
38 | }
39 |
--------------------------------------------------------------------------------
/lib/aws/config.ts:
--------------------------------------------------------------------------------
1 | export const awsConfig = {
2 | region: 'us-east-1',
3 | credentials: {
4 | accessKeyId: process.env.AMAZON_WEB_SERVICES_ACCESS_KEY!,
5 | secretAccessKey: process.env.AMAZON_WEB_SERVICES_SECRET_KEY!,
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/lib/coingecko.ts:
--------------------------------------------------------------------------------
1 | import { captureException } from '@sentry/nextjs';
2 | import { SupportedNetwork } from './config';
3 |
4 | // based on output from https://api.coingecko.com/api/v3/asset_platforms
5 | const networkMap = {
6 | optimism: 'optimistic-ethereum',
7 | ethereum: 'ethereum',
8 | polygon: 'polygon-pos',
9 | };
10 |
11 | export async function getUnitPriceForEth(toCurrency: string) {
12 | try {
13 | const res = await fetch(
14 | `https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=${toCurrency}`,
15 | );
16 | const json = await res.json();
17 |
18 | if (json) {
19 | return json.ethereum[toCurrency] as number | undefined;
20 | }
21 | } catch (e) {
22 | captureException(e);
23 | }
24 | return undefined;
25 | }
26 |
27 | export async function getUnitPriceForCoin(
28 | tokenAddress: string,
29 | toCurrency: string,
30 | network?: SupportedNetwork,
31 | ): Promise {
32 | if (network === 'rinkeby') {
33 | return 1.01;
34 | }
35 |
36 | if (network && networkMap[network]) {
37 | const statsRes = await fetch(
38 | `https://api.coingecko.com/api/v3/coins/${networkMap[network]}/contract/${tokenAddress}`,
39 | );
40 |
41 | const json = await statsRes.json();
42 |
43 | return json?.market_data?.current_price[toCurrency];
44 | }
45 |
46 | // unhandled network
47 | return undefined;
48 | }
49 |
--------------------------------------------------------------------------------
/lib/constants.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from 'ethers';
2 |
3 | export const SECONDS_IN_A_DAY = 60 * 60 * 24;
4 | export const SECONDS_IN_AN_HOUR = 60 * 60;
5 | export const SECONDS_IN_A_YEAR = 31_536_000;
6 | export const INTEREST_RATE_PERCENT_DECIMALS = 3;
7 | export const MIN_RATE = 1 / 10 ** (INTEREST_RATE_PERCENT_DECIMALS - 2);
8 |
9 | export const SCALAR = ethers.BigNumber.from(10).pow(
10 | INTEREST_RATE_PERCENT_DECIMALS,
11 | );
12 |
13 | export const DISCORD_URL = 'https://discord.gg/ZCxGuE6Ytk';
14 | export const DISCORD_ERROR_CHANNEL = '#🪲bug-reports';
15 | export const TWITTER_URL = 'https://twitter.com/backed_xyz';
16 | export const GITHUB_URL = 'https://github.com/with-backed';
17 | export const FAQ_URL =
18 | 'https://with-backed.notion.site/FAQ-df65a5002100406eb6c5211fb8e105cf';
19 | export const BUNNY_IMG_URL_MAP = {
20 | ethereum: '/logos/backed-bunny.png',
21 | rinkeby: '/logos/backed-bunny.png',
22 | optimism: '/logos/opbunny.png',
23 | polygon: '/logos/pbunny.png',
24 | };
25 |
26 | export const COMMUNITY_NFT_CONTRACT_ADDRESS =
27 | '0x63a9addF2327A0F4B71BcF9BFa757E333e1B7177';
28 | export const COMMUNITY_NFT_SUBGRAPH =
29 | 'https://api.thegraph.com/subgraphs/name/with-backed/backed-community-nft';
30 |
--------------------------------------------------------------------------------
/lib/eip721Subraph.ts:
--------------------------------------------------------------------------------
1 | import { NFTEntity } from 'types/NFT';
2 | import { contractDirectory } from 'lib/contracts';
3 | import { SupportedNetwork } from './config';
4 |
5 | export function hiddenNFTAddresses(network: SupportedNetwork) {
6 | const directory = contractDirectory[network];
7 | return [directory.borrowTicket, directory.lendTicket].map((a) =>
8 | a.toLowerCase(),
9 | );
10 | }
11 |
12 | export function getNftContractAddress(nft: NFTEntity): string {
13 | return nft.id.substring(0, 42);
14 | }
15 |
16 | export function isNFTApprovedForCollateral(
17 | nft: NFTEntity,
18 | network: SupportedNetwork,
19 | ): boolean {
20 | return (
21 | nft.approvals.filter(
22 | (approval: any) =>
23 | approval.approved.id.toLowerCase() ===
24 | contractDirectory[network].loanFacilitator.toLowerCase(),
25 | ).length > 0
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/lib/erc20Helper.ts:
--------------------------------------------------------------------------------
1 | export type ERC20Amount = {
2 | nominal: string;
3 | symbol: string;
4 | address: string;
5 | };
6 |
--------------------------------------------------------------------------------
/lib/events/consumers/attachmentsHelper.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | export async function getPngBufferFromBase64SVG(
4 | base: string,
5 | contractAddress: string,
6 | ): Promise {
7 | const pngBufferRes = await axios.post(`${process.env.SVG_TO_PNG_URL!}`, {
8 | svg: base,
9 | contractAddress,
10 | });
11 | return pngBufferRes.data.pngBuffer;
12 | }
13 |
--------------------------------------------------------------------------------
/lib/events/consumers/discord/shared.ts:
--------------------------------------------------------------------------------
1 | export enum DiscordMetric {
2 | NUM_LOANS_CREATED = 'numLoansCreated',
3 | NUM_LOANS_LENT_TO = 'numLoansLentTo',
4 | DOLLAR_LOANS_LENT_TO = 'dollarLoansLentTo',
5 | }
6 |
--------------------------------------------------------------------------------
/lib/events/consumers/getNftInfoForAttachment.ts:
--------------------------------------------------------------------------------
1 | import { SupportedNetwork } from 'lib/config';
2 | import { getMedia } from 'lib/getNFTInfo';
3 | import { NFTResponseData } from 'lib/getNFTInfo';
4 |
5 | const JSON_PREFIX = 'data:application/json;base64,';
6 |
7 | export async function getNFTInfoForAttachment(
8 | collateralContractAddress: string,
9 | collateralTokenId: string,
10 | siteUrl: string,
11 | network: SupportedNetwork,
12 | ): Promise {
13 | let NFTInfo: NFTResponseData;
14 |
15 | const tokenURIRes = await fetch(
16 | `${siteUrl}/api/network/${network}/nftInfo/${collateralContractAddress}/${collateralTokenId}`,
17 | );
18 | NFTInfo = await tokenURIRes.json();
19 |
20 | return NFTInfo;
21 | }
22 |
--------------------------------------------------------------------------------
/lib/events/consumers/twitter/api.ts:
--------------------------------------------------------------------------------
1 | import { TwitterClient } from 'twitter-api-client';
2 |
3 | export async function tweet(
4 | content: string,
5 | imageAttachment: string | undefined,
6 | ) {
7 | const client = new TwitterClient({
8 | apiKey: process.env.TWITTER_API_KEY!,
9 | apiSecret: process.env.TWITTER_API_SECRET!,
10 | accessToken: process.env.TWITTER_ACCESS_TOKEN!,
11 | accessTokenSecret: process.env.TWITTER_ACCESS_TOKEN_SECRET!,
12 | });
13 |
14 | const mediaIdRes = await client.media.mediaUpload({
15 | media_data: imageAttachment,
16 | });
17 |
18 | await client.tweetsV2.createTweet({
19 | text: content,
20 | media: {
21 | media_ids: [mediaIdRes.media_id_string],
22 | },
23 | });
24 | }
25 |
--------------------------------------------------------------------------------
/lib/events/consumers/twitter/attachments.ts:
--------------------------------------------------------------------------------
1 | import { NFTResponseData } from 'lib/getNFTInfo';
2 | import fetch from 'node-fetch';
3 | import { getPngBufferFromBase64SVG } from '../attachmentsHelper';
4 |
5 | const SVG_PREFIX = 'data:image/svg+xml;base64,';
6 |
7 | export async function nftResponseDataToImageBuffer(
8 | nftResponseData: NFTResponseData,
9 | contractAddress: string,
10 | ): Promise {
11 | if (nftResponseData?.image?.mediaUrl.startsWith(SVG_PREFIX)) {
12 | return await getPngBufferFromBase64SVG(
13 | nftResponseData!.image!.mediaUrl,
14 | contractAddress,
15 | );
16 | } else {
17 | const imageUrlRes = await fetch(nftResponseData!.image!.mediaUrl);
18 | const arraybuffer = await imageUrlRes.arrayBuffer();
19 | const outputBuffer = Buffer.from(arraybuffer);
20 | return outputBuffer.toString('base64');
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/lib/events/consumers/userNotifications/emails/genericFormatter.ts:
--------------------------------------------------------------------------------
1 | export enum GenericEmailType {
2 | CONFIRMATION = 'confirmation',
3 | }
4 |
5 | export type GenericEmailComponents = {
6 | mainMessage: string;
7 | footer: string;
8 | };
9 |
10 | const throwInvalidGenericEmailType = (_: never): never => {
11 | throw new Error('Invalid generic email type passed');
12 | };
13 |
14 | export function getSubjectForGenericEmail(type: GenericEmailType): string {
15 | switch (type) {
16 | case GenericEmailType.CONFIRMATION:
17 | return 'Backed: Email request received';
18 | default:
19 | return throwInvalidGenericEmailType(type);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/lib/events/consumers/userNotifications/emails/ses.ts:
--------------------------------------------------------------------------------
1 | import aws from 'aws-sdk';
2 | import { awsConfig } from 'lib/aws/config';
3 |
4 | const baseParams: aws.SES.Types.SendEmailRequest = {
5 | Source: process.env.BACKED_NOTIFICATIONS_EMAIL_ADDRESS!,
6 | Destination: {
7 | ToAddresses: [],
8 | },
9 | ReplyToAddresses: [process.env.BACKED_NOTIFICATIONS_EMAIL_ADDRESS!],
10 | Message: {
11 | Body: {
12 | Html: {
13 | Charset: 'UTF-8',
14 | Data: '',
15 | },
16 | },
17 | Subject: {
18 | Charset: 'UTF-8',
19 | Data: '',
20 | },
21 | },
22 | };
23 |
24 | export async function executeEmailSendWithSes(
25 | html: string,
26 | subject: string,
27 | toAddress: string,
28 | ): Promise {
29 | const params: aws.SES.Types.SendEmailRequest = {
30 | ...baseParams,
31 | Destination: {
32 | ToAddresses: [toAddress],
33 | },
34 | };
35 | params.Message.Body.Html!.Data = html;
36 | params.Message.Subject.Data = subject;
37 |
38 | const res = await new aws.SES(awsConfig).sendEmail(params).promise();
39 |
40 | return res.$response.error || null;
41 | }
42 |
--------------------------------------------------------------------------------
/lib/events/consumers/userNotifications/shared.ts:
--------------------------------------------------------------------------------
1 | import { RawEventNameType } from 'types/RawEvent';
2 |
3 | export enum NotificationMethod {
4 | EMAIL = 'email',
5 | }
6 |
7 | export type NotificationTriggerType =
8 | | RawEventNameType
9 | | 'LiquidationOccurring'
10 | | 'LiquidationOccurred'
11 | | 'All';
12 |
--------------------------------------------------------------------------------
/lib/events/sns/helpers.ts:
--------------------------------------------------------------------------------
1 | import { SupportedNetwork } from 'lib/config';
2 | import { NextApiResponse } from 'next';
3 | import { LendEvent as RawSubgraphLendEvent } from 'types/generated/graphql/nftLoans';
4 | import { RawEventNameType, RawSubgraphEvent } from 'types/RawEvent';
5 |
6 | export type EventsSNSMessage = {
7 | eventName: RawEventNameType;
8 | event: RawSubgraphEvent;
9 | network: SupportedNetwork;
10 | mostRecentTermsEvent?: RawSubgraphLendEvent;
11 | };
12 |
13 | export async function confirmTopicSubscription(
14 | body: any,
15 | res: NextApiResponse,
16 | ): Promise {
17 | if ('SubscribeURL' in body) {
18 | try {
19 | await fetch(body['SubscribeURL'], {
20 | method: 'GET',
21 | });
22 | res.status(200).send('subscription successful');
23 | } catch (e) {
24 | res.status(400).send('subscription unsuccessful');
25 | }
26 | return true;
27 | } else {
28 | return false;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/lib/events/sns/push.ts:
--------------------------------------------------------------------------------
1 | import { SNS } from 'aws-sdk';
2 | import { awsConfig } from 'lib/aws/config';
3 | import { EventsSNSMessage } from 'lib/events/sns/helpers';
4 |
5 | export async function pushEventForProcessing({
6 | eventName,
7 | event,
8 | network,
9 | mostRecentTermsEvent,
10 | }: EventsSNSMessage): Promise {
11 | const sns = new SNS(awsConfig);
12 |
13 | const res = await sns
14 | .publish({
15 | TopicArn: process.env.EVENTS_SNS_ARN!,
16 | Message: JSON.stringify({
17 | eventName,
18 | event,
19 | network,
20 | mostRecentTermsEvent,
21 | }),
22 | })
23 | .promise();
24 |
25 | return !res.$response.error;
26 | }
27 |
--------------------------------------------------------------------------------
/lib/events/sqs/helpers.ts:
--------------------------------------------------------------------------------
1 | import { SQS } from 'aws-sdk';
2 | import { awsConfig } from 'lib/aws/config';
3 | import { SupportedNetwork } from 'lib/config';
4 | import { RawEventNameType } from 'types/RawEvent';
5 |
6 | export type FormattedNotificationEventMessageType = {
7 | eventName: RawEventNameType;
8 | txHash: string;
9 | receiptHandle: string;
10 | network: SupportedNetwork;
11 | };
12 |
13 | export async function receiveMessages(): Promise<
14 | FormattedNotificationEventMessageType[] | undefined
15 | > {
16 | const queueUrl = process.env.EVENTS_SQS_URL!;
17 | const sqs = new SQS(awsConfig);
18 |
19 | const response = await sqs.receiveMessage({ QueueUrl: queueUrl }).promise();
20 | return response.Messages?.map((message) => {
21 | const messageBody: {
22 | txHash: string;
23 | eventName: RawEventNameType;
24 | network: SupportedNetwork;
25 | } = JSON.parse(message.Body!);
26 | return {
27 | ...messageBody,
28 | receiptHandle: message.ReceiptHandle!,
29 | };
30 | });
31 | }
32 |
33 | export function deleteMessage(receiptHandle: string) {
34 | const queueUrl = process.env.EVENTS_SQS_URL!;
35 | const sqs = new SQS(awsConfig);
36 |
37 | sqs.deleteMessage(
38 | { QueueUrl: queueUrl, ReceiptHandle: receiptHandle },
39 | (err, _) => {
40 | if (err) {
41 | console.error(err);
42 | }
43 | },
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/lib/fetchWithTimeout.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Wrapper for fetch that will timeout after a set amount of time.
3 | * @param RequestInit takes an optional `timeout` field (in ms)
4 | */
5 | export const fetchWithTimeout: typeof fetch = async (
6 | input: RequestInfo,
7 | init?: RequestInit & { timeout?: number },
8 | ) => {
9 | const { timeout = 8000, ...options } = init || {};
10 |
11 | const controller = new AbortController();
12 | const id = setTimeout(() => controller.abort(), timeout);
13 | const response = await fetch(input, {
14 | ...options,
15 | signal: controller.signal,
16 | });
17 | clearTimeout(id);
18 | return response;
19 | };
20 |
--------------------------------------------------------------------------------
/lib/interest.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from 'ethers';
2 | import { INTEREST_RATE_PERCENT_DECIMALS, SECONDS_IN_A_YEAR } from './constants';
3 |
4 | export const formattedAnnualRate = (perAnumScaledRate: ethers.BigNumber) => {
5 | return ethers.utils.formatUnits(
6 | perAnumScaledRate,
7 | INTEREST_RATE_PERCENT_DECIMALS - 2,
8 | );
9 | };
10 |
11 | export const annualRateToPerSecond = (annualRate: number): string => {
12 | return Math.ceil(
13 | (annualRate / SECONDS_IN_A_YEAR) * Math.pow(10, 8),
14 | ).toString();
15 | };
16 |
--------------------------------------------------------------------------------
/lib/loanAssets.ts:
--------------------------------------------------------------------------------
1 | export const DEFAULT_ASSET_DECIMALS = 18;
2 | export interface LoanAsset {
3 | address: string;
4 | symbol: string;
5 | chainId: number;
6 | }
7 |
--------------------------------------------------------------------------------
/lib/loans/collateralSaleInfo.ts:
--------------------------------------------------------------------------------
1 | import { SupportedNetwork } from 'lib/config';
2 | import {
3 | CollectionStatistics,
4 | getCollectionStats,
5 | } from 'lib/nftCollectionStats';
6 |
7 | export type CollateralSaleInfo = {
8 | recentSale: {
9 | paymentToken: string;
10 | price: number;
11 | } | null;
12 | collectionStats: CollectionStatistics;
13 | };
14 |
15 | export async function getCollateralSaleInfo(
16 | nftContractAddress: string,
17 | tokenId: string,
18 | nftSalesSubgraph: string | null,
19 | network: SupportedNetwork,
20 | jsonRpcProvider: string,
21 | ): Promise {
22 | const recentSale = await getMostRecentSale(
23 | nftContractAddress,
24 | tokenId,
25 | nftSalesSubgraph,
26 | network,
27 | jsonRpcProvider,
28 | );
29 |
30 | const collectionStats = await getCollectionStats(
31 | nftContractAddress,
32 | tokenId,
33 | network,
34 | );
35 |
36 | return {
37 | collectionStats,
38 | recentSale,
39 | };
40 | }
41 |
42 | async function getMostRecentSale(
43 | nftContractAddress: string,
44 | tokenId: string,
45 | _nftSalesSubgraph: string | null, // TODO: deprecate
46 | network: SupportedNetwork,
47 | jsonRpcProvider: string,
48 | ): Promise<{ paymentToken: string; price: number } | null> {
49 | // deprecating most recent sale, return null and UI will handle
50 |
51 | return null;
52 | }
53 |
--------------------------------------------------------------------------------
/lib/loans/loans.ts:
--------------------------------------------------------------------------------
1 | import subgraphLoans from './subgraph/subgraphLoans';
2 | import { parseSubgraphLoan } from './utils';
3 |
4 | export async function loans(nftBackedLoansSubgraph: string) {
5 | const loans = await subgraphLoans(20, nftBackedLoansSubgraph);
6 |
7 | return loans.map((loan) => parseSubgraphLoan(loan));
8 | }
9 |
--------------------------------------------------------------------------------
/lib/loans/subgraph/subgraphLoanById.ts:
--------------------------------------------------------------------------------
1 | import { captureEvent } from '@sentry/nextjs';
2 | import { clientFromUrl } from 'lib/urql';
3 | import {
4 | LoanByIdDocument,
5 | LoanByIdQuery,
6 | } from 'types/generated/graphql/nftLoans';
7 |
8 | export async function subgraphLoanById(
9 | id: string,
10 | nftBackedLoansSubgraph: string,
11 | ) {
12 | const nftBackedLoansClient = clientFromUrl(nftBackedLoansSubgraph);
13 | const { data, error } = await nftBackedLoansClient
14 | .query(LoanByIdDocument, { id })
15 | .toPromise();
16 |
17 | if (error) {
18 | captureEvent(error);
19 | }
20 |
21 | if (data?.loan) {
22 | return data.loan;
23 | }
24 |
25 | return null;
26 | }
27 |
--------------------------------------------------------------------------------
/lib/nftCollectionStats/index.ts:
--------------------------------------------------------------------------------
1 | import { collectionStatsEthMainnet } from 'lib/nftCollectionStats/reservoir';
2 | import { collectionStatsRinkeby } from 'lib/nftCollectionStats/mockData';
3 | import { SupportedNetwork } from 'lib/config';
4 | import { collectionStatsOptimism } from 'lib/nftCollectionStats/quixotic';
5 |
6 | export type CollectionStatistics = {
7 | floor: number | null;
8 | items: number | null;
9 | owners: number | null;
10 | volume: number | null;
11 | };
12 |
13 | export async function getCollectionStats(
14 | contractAddress: string,
15 | tokenId: string,
16 | network: SupportedNetwork,
17 | ): Promise {
18 | switch (network) {
19 | case 'ethereum':
20 | return collectionStatsEthMainnet(contractAddress, tokenId);
21 | case 'optimism':
22 | // quixotic api broke
23 | return nullCollectionStats;
24 | case 'polygon':
25 | return nullCollectionStats;
26 | case 'rinkeby':
27 | return collectionStatsRinkeby();
28 | default:
29 | return nullCollectionStats;
30 | }
31 | }
32 |
33 | const nullCollectionStats: CollectionStatistics = {
34 | floor: null,
35 | items: null,
36 | owners: null,
37 | volume: null,
38 | };
39 |
--------------------------------------------------------------------------------
/lib/nftCollectionStats/mockData.ts:
--------------------------------------------------------------------------------
1 | export function collectionStatsRinkeby() {
2 | const [items, owners] = getFakeItemsAndOwners();
3 | return {
4 | floor: getFakeFloor(),
5 | items,
6 | owners,
7 | volume: getFakeVolume(),
8 | };
9 | }
10 |
11 | // MOCK METHODS TO GENERATE FAKE STATS FOR RINKEBY
12 | export function getFakeFloor(): number {
13 | return Math.floor(Math.random() * (20 + 1));
14 | }
15 |
16 | export function getFakeItemsAndOwners(): [number, number] {
17 | const items = Math.floor(Math.random() * 800);
18 | const owners = Math.floor(Math.random() * (items - 1));
19 |
20 | return [items, owners];
21 | }
22 |
23 | export function getFakeVolume(): number {
24 | return Math.floor(Math.random() * 2000);
25 | }
26 |
--------------------------------------------------------------------------------
/lib/nftCollectionStats/quixotic.ts:
--------------------------------------------------------------------------------
1 | import { CollectionStatistics } from 'lib/nftCollectionStats';
2 |
3 | export async function collectionStatsOptimism(
4 | contractAddress: string,
5 | ): Promise {
6 | const quixoticAssetReq = await fetch(
7 | `https://api.quixotic.io/api/v1/opt/collection/${contractAddress}/stats`,
8 | {
9 | headers: new Headers({
10 | Accept: 'application/json',
11 | 'x-api-key': process.env.QUIXOTIC_API_KEY!,
12 | }),
13 | },
14 | );
15 |
16 | const json = (await quixoticAssetReq.json()) as any;
17 |
18 | return {
19 | floor: json?.stats?.floor_price || null,
20 | items: json?.stats?.total_supply || null,
21 | owners: json?.stats?.num_owners || null,
22 | volume: json?.stats?.total_volume || null,
23 | };
24 | }
25 |
--------------------------------------------------------------------------------
/lib/nftCollectionStats/reservoir.ts:
--------------------------------------------------------------------------------
1 | import { CollectionStatistics } from 'lib/nftCollectionStats';
2 |
3 | const reservoirResToStats = (json: any): CollectionStatistics => ({
4 | floor: json?.collection?.floorAsk.price || null,
5 | items: json?.collection?.tokenCount || null,
6 | owners: json?.collection?.ownerCount || null,
7 | volume: json?.collection?.volume.allTime || null,
8 | });
9 |
10 | export async function collectionStatsEthMainnet(
11 | contractAddress: string,
12 | tokenId: string,
13 | ): Promise {
14 | const tokensV4Req = await fetch(
15 | `https://api.reservoir.tools/tokens/v4?tokens=${contractAddress}:${tokenId}`,
16 | {
17 | headers: new Headers({
18 | Accept: 'application/json',
19 | 'x-api-key': process.env.RESERVOIR_API_KEY!,
20 | }),
21 | },
22 | );
23 |
24 | const collectionId = (await tokensV4Req.json())?.tokens[0]?.collection?.id;
25 |
26 | const collectionV2Req = await fetch(
27 | `https://api.reservoir.tools/collection/v2?id=${collectionId}`,
28 | {
29 | headers: new Headers({
30 | Accept: 'application/json',
31 | 'x-api-key': process.env.RESERVOIR_API_KEY!,
32 | }),
33 | },
34 | );
35 |
36 | return reservoirResToStats(await collectionV2Req.json());
37 | }
38 |
--------------------------------------------------------------------------------
/lib/notifications/script.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from '@prisma/client';
2 |
3 | const prisma = new PrismaClient();
4 |
5 | /*
6 | This file can be used as a script to run DB operations should we ever need to in the future (proceed with caution)
7 | run with: npx dotenv -e .env.development -- npx ts-node --skip-project notifications/index.ts
8 | */
9 |
10 | async function main() {
11 | // await prisma.notificationRequest.count; // returns count of all notification requests
12 | }
13 |
14 | main()
15 | .catch((e) => {
16 | throw e;
17 | })
18 | .finally(async () => {
19 | await prisma.$disconnect();
20 | });
21 |
--------------------------------------------------------------------------------
/lib/parseSerializedResponse.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from 'ethers';
2 |
3 | export function parseSerializedResponse(jsonString: string) {
4 | const parsed = JSON.parse(jsonString);
5 | if (Array.isArray(parsed)) {
6 | return parsed.map(toObjectWithBigNumbers);
7 | }
8 |
9 | return toObjectWithBigNumbers(parsed);
10 | }
11 |
12 | /**
13 | * Given an object, return a new object with the same values, except all serialized BigNumbers are instantiated.
14 | * @param obj
15 | */
16 | export function toObjectWithBigNumbers(obj: { [key: string]: any }) {
17 | const result = Object.assign({}, obj);
18 | Object.keys(result).forEach((key) => {
19 | if (result[key] && result[key].hex) {
20 | result[key] = ethers.BigNumber.from(result[key]);
21 | }
22 | });
23 | return result;
24 | }
25 |
--------------------------------------------------------------------------------
/lib/pirsch.ts:
--------------------------------------------------------------------------------
1 | export const pirsch: typeof window.pirsch = (...args) => {
2 | // Pirsch may be blocked by extensions; only try to send if it really exists
3 | if (window.pirsch) {
4 | window.pirsch(...args);
5 | }
6 | };
7 |
--------------------------------------------------------------------------------
/lib/signedMessages.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from 'ethers';
2 |
3 | export function generateAddressFromSignedMessage(
4 | signedMessage: string,
5 | ): string | null {
6 | try {
7 | const address = ethers.utils.verifyMessage(
8 | process.env.NEXT_PUBLIC_NOTIFICATION_REQ_MESSAGE!,
9 | signedMessage,
10 | );
11 | return address;
12 | } catch (_e) {
13 | return null;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/lib/text.ts:
--------------------------------------------------------------------------------
1 | import { Event } from 'types/Event';
2 |
3 | type EventName = Pick['typename'];
4 |
5 | const eventNames: {
6 | [key in EventName]: string;
7 | } = {
8 | BuyoutEvent: 'Buyout Event',
9 | CloseEvent: 'Close Loan',
10 | CreateEvent: 'Create Loan',
11 | RepaymentEvent: 'Repay Loan',
12 | CollateralSeizureEvent: 'Seize Collateral',
13 | LendEvent: 'Lend Event',
14 | };
15 |
16 | export function renderEventName(name: EventName) {
17 | return eventNames[name];
18 | }
19 |
--------------------------------------------------------------------------------
/lib/urql.ts:
--------------------------------------------------------------------------------
1 | import { createClient } from '@urql/core';
2 |
3 | // The local document cache is not very useful in our use case, because it
4 | // caches everything and only invalidates when there's a mutation. We never
5 | // perform mutations on data in the subgraphs, so our local cache would never
6 | // be invalidated.
7 | const requestPolicy = 'network-only';
8 |
9 | export function clientFromUrl(url: string) {
10 | return createClient({
11 | url,
12 | requestPolicy,
13 | });
14 | }
15 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/pages/404.tsx:
--------------------------------------------------------------------------------
1 | import { Custom404 } from 'components/Custom404';
2 | import Head from 'next/head';
3 |
4 | export default function Page() {
5 | return (
6 | <>
7 |
8 | Backed | 404
9 |
10 |
11 |
12 | >
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/pages/500.tsx:
--------------------------------------------------------------------------------
1 | import { Custom500 } from 'components/Custom500';
2 | import Head from 'next/head';
3 |
4 | export default function Page() {
5 | return (
6 | <>
7 |
8 | Backed | 500
9 |
10 |
11 |
12 | >
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/pages/about.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { OpenGraph } from 'components/OpenGraph';
3 | import { BUNNY_IMG_URL_MAP } from 'lib/constants';
4 | import { HeaderInfo } from 'components/HeaderInfo';
5 |
6 | export default function About() {
7 | return (
8 | <>
9 |
14 |
15 | >
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/pages/api/events/_middleware.ts:
--------------------------------------------------------------------------------
1 | import { authenticateRequest, AUTH_STATUS } from 'lib/authentication';
2 | import { NextFetchEvent, NextRequest, NextResponse } from 'next/server';
3 |
4 | export default function middleware(req: NextRequest, _ev?: NextFetchEvent) {
5 | try {
6 | const authStatus = authenticateRequest(req);
7 | if (authStatus == AUTH_STATUS.ok) {
8 | return NextResponse.next();
9 | } else {
10 | return new NextResponse(undefined, {
11 | status: authStatus,
12 | headers: {
13 | 'WWW-Authenticate': 'Basic realm',
14 | },
15 | });
16 | }
17 | } catch (e) {
18 | console.error(e);
19 | return new NextResponse(undefined, { status: 500 });
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/pages/api/events/community/nominationMessage.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from 'next';
2 | import { captureException, withSentry } from '@sentry/nextjs';
3 | import { sendBotMessage } from 'lib/events/consumers/discord/bot';
4 |
5 | async function handler(req: NextApiRequest, res: NextApiResponse) {
6 | if (req.method != 'POST') {
7 | res.status(405).send('Only POST requests allowed');
8 | return;
9 | }
10 |
11 | try {
12 | const { ethAddress, category, reason, value } = req.body;
13 |
14 | const message = `**New Form Nomination for ${ethAddress}**
15 | Category: ${category}
16 | Value: ${value} XP
17 | Reason: ${reason}
18 | `;
19 |
20 | await sendBotMessage(message, process.env.NOMINATION_CHANNEL_ID!);
21 |
22 | res.status(200).json(`discord nomination bot message successfully sent`);
23 | } catch (e) {
24 | captureException(e);
25 | res.status(404);
26 | }
27 | }
28 |
29 | export default withSentry(handler);
30 |
--------------------------------------------------------------------------------
/pages/api/events/consumers/discord.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from 'next';
2 | import {
3 | confirmTopicSubscription,
4 | EventsSNSMessage,
5 | } from 'lib/events/sns/helpers';
6 | import { sendBotUpdateForTriggerAndEntity } from 'lib/events/consumers/discord/formatter';
7 | import { captureException, withSentry } from '@sentry/nextjs';
8 | import { configs } from 'lib/config';
9 |
10 | async function handler(req: NextApiRequest, res: NextApiResponse) {
11 | if (req.method != 'POST') {
12 | res.status(405).send('Only POST requests allowed');
13 | return;
14 | }
15 |
16 | const parsedBody = JSON.parse(req.body);
17 |
18 | const isConfirmingSubscribe = await confirmTopicSubscription(parsedBody, res);
19 | if (isConfirmingSubscribe) {
20 | return;
21 | }
22 |
23 | try {
24 | const { eventName, event, mostRecentTermsEvent, network } = JSON.parse(
25 | parsedBody['Message'],
26 | ) as EventsSNSMessage;
27 |
28 | const now = Math.floor(new Date().getTime() / 1000);
29 | await sendBotUpdateForTriggerAndEntity(
30 | eventName,
31 | event,
32 | configs[network],
33 | mostRecentTermsEvent,
34 | );
35 |
36 | res.status(200).json(`discord bot messages successfully sent`);
37 | } catch (e) {
38 | captureException(e);
39 | res.status(404);
40 | }
41 | }
42 |
43 | export default withSentry(handler);
44 |
--------------------------------------------------------------------------------
/pages/api/events/consumers/twitter.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from 'next';
2 | import {
3 | confirmTopicSubscription,
4 | EventsSNSMessage,
5 | } from 'lib/events/sns/helpers';
6 | import { sendTweetForTriggerAndEntity } from 'lib/events/consumers/twitter/formatter';
7 | import { captureException, withSentry } from '@sentry/nextjs';
8 | import { configs } from 'lib/config';
9 |
10 | async function handler(req: NextApiRequest, res: NextApiResponse) {
11 | if (req.method != 'POST') {
12 | res.status(405).send('Only POST requests allowed');
13 | return;
14 | }
15 |
16 | const parsedBody = JSON.parse(req.body);
17 |
18 | const isConfirmingSubscribe = await confirmTopicSubscription(parsedBody, res);
19 | if (isConfirmingSubscribe) {
20 | return;
21 | }
22 |
23 | try {
24 | const { eventName, event, mostRecentTermsEvent, network } = JSON.parse(
25 | parsedBody['Message'],
26 | ) as EventsSNSMessage;
27 |
28 | await sendTweetForTriggerAndEntity(
29 | eventName,
30 | event,
31 | configs[network],
32 | mostRecentTermsEvent,
33 | );
34 |
35 | res.status(200).json(`tweet successfully sent`);
36 | } catch (e) {
37 | captureException(e);
38 | res.status(404);
39 | }
40 | }
41 |
42 | export default withSentry(handler);
43 |
--------------------------------------------------------------------------------
/pages/api/events/consumers/userNotifications.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from 'next';
2 | import { sendEmailsForTriggerAndEntity } from 'lib/events/consumers/userNotifications/emails/emails';
3 | import {
4 | confirmTopicSubscription,
5 | EventsSNSMessage,
6 | } from 'lib/events/sns/helpers';
7 | import { captureException, withSentry } from '@sentry/nextjs';
8 | import { configs } from 'lib/config';
9 |
10 | async function handler(req: NextApiRequest, res: NextApiResponse) {
11 | if (req.method != 'POST') {
12 | res.status(405).send('Only POST requests allowed');
13 | return;
14 | }
15 |
16 | const parsedBody = JSON.parse(req.body);
17 |
18 | const isConfirmingSubscribe = await confirmTopicSubscription(parsedBody, res);
19 | if (isConfirmingSubscribe) {
20 | return;
21 | }
22 |
23 | try {
24 | const { eventName, event, mostRecentTermsEvent, network } = JSON.parse(
25 | parsedBody['Message'],
26 | ) as EventsSNSMessage;
27 |
28 | const now = Math.floor(new Date().getTime() / 1000);
29 | await sendEmailsForTriggerAndEntity(
30 | eventName,
31 | event,
32 | now,
33 | configs[network],
34 | mostRecentTermsEvent,
35 | );
36 |
37 | res.status(200).json(`notifications successfully sent`);
38 | } catch (e) {
39 | captureException(e);
40 | res.status(404);
41 | }
42 | }
43 |
44 | export default withSentry(handler);
45 |
--------------------------------------------------------------------------------
/pages/api/events/cron/processNewOnchainEvents.ts:
--------------------------------------------------------------------------------
1 | import { captureException, withSentry } from '@sentry/nextjs';
2 | import { main } from 'lib/events/sqs/consumer';
3 | import { NextApiRequest, NextApiResponse } from 'next';
4 |
5 | async function handler(req: NextApiRequest, res: NextApiResponse) {
6 | if (req.method != 'POST') {
7 | res.status(405).send('Only POST requests allowed');
8 | return;
9 | }
10 |
11 | try {
12 | await main();
13 | res.status(200).json({ success: true });
14 | } catch (e) {
15 | captureException(e);
16 | res.status(404);
17 | }
18 | }
19 |
20 | export default withSentry(handler);
21 |
--------------------------------------------------------------------------------
/pages/api/network/[network]/addresses/[address]/loans/index.ts:
--------------------------------------------------------------------------------
1 | import { getAllLoansForAddress } from 'lib/loans/subgraph/getAllLoansEventsForAddress';
2 | import { Loan as SubgraphLoan } from 'types/generated/graphql/nftLoans';
3 | import { NextApiRequest, NextApiResponse } from 'next';
4 | import { captureException, withSentry } from '@sentry/nextjs';
5 | import { configs, SupportedNetwork, validateNetwork } from 'lib/config';
6 |
7 | async function handler(
8 | req: NextApiRequest,
9 | res: NextApiResponse,
10 | ) {
11 | try {
12 | validateNetwork(req.query);
13 | const { address, network } = req.query;
14 | const config = configs[network as SupportedNetwork];
15 | const loans = await getAllLoansForAddress(
16 | address as string,
17 | config.nftBackedLoansSubgraph,
18 | );
19 | res.status(200).json(loans);
20 | } catch (e) {
21 | captureException(e);
22 | res.status(404);
23 | }
24 | }
25 |
26 | export default withSentry(handler);
27 |
--------------------------------------------------------------------------------
/pages/api/network/[network]/loans/[id].tsx:
--------------------------------------------------------------------------------
1 | import { subgraphLoanById } from 'lib/loans/subgraph/subgraphLoanById';
2 | import { LoanByIdQuery } from 'types/generated/graphql/nftLoans';
3 | import { NextApiRequest, NextApiResponse } from 'next';
4 | import { captureException, withSentry } from '@sentry/nextjs';
5 | import { configs, SupportedNetwork, validateNetwork } from 'lib/config';
6 |
7 | // TODO: Should probably not just relying on
8 | // the subgraph, but fall back to the node, if the call didn't work
9 | // TODO: is this route used?
10 | async function handler(
11 | req: NextApiRequest,
12 | res: NextApiResponse,
13 | ) {
14 | try {
15 | validateNetwork(req.query);
16 | const { id, network } = req.query;
17 | const config = configs[network as SupportedNetwork];
18 | const idString: string = Array.isArray(id) ? id[0] : id;
19 |
20 | const loan = await subgraphLoanById(
21 | idString,
22 | config.nftBackedLoansSubgraph,
23 | );
24 | res.status(200).json(loan);
25 | } catch (e) {
26 | captureException(e);
27 | res.status(404);
28 | }
29 | }
30 |
31 | export default withSentry(handler);
32 |
--------------------------------------------------------------------------------
/pages/api/network/[network]/loans/all.tsx:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from 'next';
2 | import subgraphLoans from 'lib/loans/subgraph/subgraphLoans';
3 | import {
4 | Loan,
5 | Loan_OrderBy,
6 | OrderDirection,
7 | } from 'types/generated/graphql/nftLoans';
8 | import { captureException, withSentry } from '@sentry/nextjs';
9 | import { configs, SupportedNetwork, validateNetwork } from 'lib/config';
10 |
11 | async function handler(
12 | req: NextApiRequest,
13 | res: NextApiResponse,
14 | ) {
15 | try {
16 | validateNetwork(req.query);
17 | const { page, limit, sort, sortDirection, network } = req.query;
18 | const config = configs[network as SupportedNetwork];
19 |
20 | const loans = await subgraphLoans(
21 | parseInt(limit as string),
22 | config.nftBackedLoansSubgraph,
23 | parseInt(page as string),
24 | sort as Loan_OrderBy,
25 | sortDirection as OrderDirection,
26 | );
27 |
28 | res.status(200).json(loans);
29 | } catch (e) {
30 | captureException(e);
31 | res.status(404);
32 | }
33 | }
34 |
35 | export default withSentry(handler);
36 |
--------------------------------------------------------------------------------
/pages/api/network/[network]/loans/history/[id].ts:
--------------------------------------------------------------------------------
1 | import { Event } from 'types/Event';
2 | import { NextApiRequest, NextApiResponse } from 'next';
3 | import { nodeLoanEventsById } from 'lib/loans/node/nodeLoanEventsById';
4 | import { captureException, withSentry } from '@sentry/nextjs';
5 | import { configs, SupportedNetwork, validateNetwork } from 'lib/config';
6 |
7 | async function handler(
8 | req: NextApiRequest,
9 | res: NextApiResponse,
10 | ) {
11 | try {
12 | validateNetwork(req.query);
13 | const { id, network } = req.query as {
14 | id: string;
15 | network: SupportedNetwork;
16 | };
17 | const idString: string = Array.isArray(id) ? id[0] : id;
18 | const config = configs[network];
19 |
20 | const events = await nodeLoanEventsById(
21 | idString,
22 | config.jsonRpcProvider,
23 | config.facilitatorStartBlock,
24 | network,
25 | );
26 |
27 | res.status(200).json(events);
28 | } catch (e) {
29 | captureException(e);
30 | res.status(404);
31 | }
32 | }
33 |
34 | export default withSentry(handler);
35 |
--------------------------------------------------------------------------------
/pages/api/sharedTypes.ts:
--------------------------------------------------------------------------------
1 | export type APIErrorMessage = {
2 | message: string;
3 | };
4 |
--------------------------------------------------------------------------------
/pages/community/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { CommunityPage } from 'components/CommunityPageContent';
3 | import { useAccount } from 'wagmi';
4 | import { useRouter } from 'next/router';
5 |
6 | export default function Community() {
7 | const { address } = useAccount();
8 | const router = useRouter();
9 |
10 | useEffect(() => {
11 | if (address) {
12 | router.push(`/community/${address}`);
13 | }
14 | }, [address, router]);
15 | return ;
16 | }
17 |
--------------------------------------------------------------------------------
/pages/community/multisig.tsx:
--------------------------------------------------------------------------------
1 | import { PendingCommunityTransactions } from 'components/PendingCommunityTransactions/PendingCommunityTransactions';
2 | import { getPendingMultiSigChanges } from 'lib/communityNFT/multisig';
3 | import { GetServerSideProps } from 'next';
4 | import { PendingChanges } from 'lib/communityNFT/multisig';
5 |
6 | export type MultiSigProps = {
7 | multiSigChanges: string;
8 | };
9 |
10 | export const getServerSideProps: GetServerSideProps = async (
11 | context,
12 | ) => {
13 | return {
14 | props: {
15 | multiSigChanges: JSON.stringify(await getPendingMultiSigChanges()),
16 | },
17 | };
18 | };
19 |
20 | export default function MultiSig({ multiSigChanges }: MultiSigProps) {
21 | return (
22 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/pages/network/[network]/loans/create.module.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/pages/network/[network]/loans/create.module.css
--------------------------------------------------------------------------------
/pages/network/[network]/loans/create.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CreatePageHeader } from 'components/CreatePageHeader';
3 | import { GetServerSideProps } from 'next';
4 | import { SupportedNetwork, validateNetwork } from 'lib/config';
5 | import { captureException } from '@sentry/nextjs';
6 | import { OpenGraph } from 'components/OpenGraph';
7 | import { BUNNY_IMG_URL_MAP } from 'lib/constants';
8 | import { useConfig } from 'hooks/useConfig';
9 | import capitalize from 'lodash/capitalize';
10 |
11 | export const getServerSideProps: GetServerSideProps<{}> = async (context) => {
12 | try {
13 | validateNetwork(context.params!);
14 | } catch (e) {
15 | captureException(e);
16 | return {
17 | notFound: true,
18 | };
19 | }
20 |
21 | return {
22 | props: {},
23 | };
24 | };
25 |
26 | export default function Create() {
27 | const { network } = useConfig();
28 | return (
29 | <>
30 |
35 |
36 | >
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/pages/network/[network]/profile/[address].module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | margin: var(--gap) 0;
3 | display: flex;
4 | flex-direction: column;
5 | gap: var(--gap);
6 | width: 100%;
7 | max-width: var(--max-width);
8 | }
9 |
10 | .wrapper > div[role='checkbox'] {
11 | align-self: flex-start;
12 | }
13 |
14 | @media screen and (max-width: 700px) {
15 | .wrapper {
16 | margin: calc(var(--gap) / 2) 0;
17 | gap: 0;
18 | }
19 |
20 | .wrapper > div[role='checkbox'] {
21 | align-self: center;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/prisma/migrations/20220209173148_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "NotificationRequest" (
3 | "id" SERIAL NOT NULL,
4 | "ethAddress" TEXT NOT NULL,
5 | "event" TEXT NOT NULL,
6 | "deliveryMethod" TEXT NOT NULL,
7 | "deliveryDestination" TEXT NOT NULL,
8 |
9 | CONSTRAINT "NotificationRequest_pkey" PRIMARY KEY ("id")
10 | );
11 |
--------------------------------------------------------------------------------
/prisma/migrations/20220210183028_varchar/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to alter the column `ethAddress` on the `NotificationRequest` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(40)`.
5 | - You are about to alter the column `event` on the `NotificationRequest` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(40)`.
6 | - You are about to alter the column `deliveryMethod` on the `NotificationRequest` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(40)`.
7 | - You are about to alter the column `deliveryDestination` on the `NotificationRequest` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(40)`.
8 |
9 | */
10 | -- AlterTable
11 | ALTER TABLE "NotificationRequest" ALTER COLUMN "ethAddress" SET DATA TYPE VARCHAR(40),
12 | ALTER COLUMN "event" SET DATA TYPE VARCHAR(40),
13 | ALTER COLUMN "deliveryMethod" SET DATA TYPE VARCHAR(40),
14 | ALTER COLUMN "deliveryDestination" SET DATA TYPE VARCHAR(40);
15 |
--------------------------------------------------------------------------------
/prisma/migrations/20220210183615_eth_address_42/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "NotificationRequest" ALTER COLUMN "ethAddress" SET DATA TYPE VARCHAR(42);
3 |
--------------------------------------------------------------------------------
/prisma/migrations/20220223155259_timestamp_notifications/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "LastTimestampForNotifications" (
3 | "id" SERIAL NOT NULL,
4 | "lastWrittenTimestamp" INTEGER NOT NULL,
5 |
6 | CONSTRAINT "LastTimestampForNotifications_pkey" PRIMARY KEY ("id")
7 | );
8 |
--------------------------------------------------------------------------------
/prisma/migrations/20220329220615_uuid/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - The primary key for the `NotificationRequest` table will be changed. If it partially fails, the table could be left without primary key constraint.
5 |
6 | */
7 | -- AlterTable
8 | ALTER TABLE "NotificationRequest" DROP CONSTRAINT "NotificationRequest_pkey",
9 | ALTER COLUMN "id" DROP DEFAULT,
10 | ALTER COLUMN "id" SET DATA TYPE TEXT,
11 | ADD CONSTRAINT "NotificationRequest_pkey" PRIMARY KEY ("id");
12 | DROP SEQUENCE "NotificationRequest_id_seq";
13 |
--------------------------------------------------------------------------------
/prisma/migrations/20220401180609_discord_metrics/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "DiscordMetrics" (
3 | "id" SERIAL NOT NULL,
4 | "numLoansCreated" INTEGER NOT NULL,
5 | "numLoansLentTo" INTEGER NOT NULL,
6 | "dollarLoansLentTo" INTEGER NOT NULL,
7 |
8 | CONSTRAINT "DiscordMetrics_pkey" PRIMARY KEY ("id")
9 | );
10 |
--------------------------------------------------------------------------------
/prisma/migrations/20220419152427_backed_metrics/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the `DiscordMetrics` table. If the table is not empty, all the data it contains will be lost.
5 |
6 | */
7 | -- DropTable
8 | DROP TABLE "DiscordMetrics";
9 |
10 | -- CreateTable
11 | CREATE TABLE "BackedMetrics" (
12 | "id" SERIAL NOT NULL,
13 | "emailsSentPastDay" INTEGER NOT NULL,
14 |
15 | CONSTRAINT "BackedMetrics_pkey" PRIMARY KEY ("id")
16 | );
17 |
--------------------------------------------------------------------------------
/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "postgresql"
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | // This is your Prisma schema file,
2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema
3 |
4 | generator client {
5 | provider = "prisma-client-js"
6 | }
7 |
8 | datasource db {
9 | provider = "postgresql"
10 | url = env("DATABASE_URL")
11 | shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
12 | }
13 |
14 | model NotificationRequest {
15 | id String @id @default(uuid())
16 | ethAddress String @db.VarChar(42)
17 | event String @db.VarChar(40)
18 | deliveryMethod String @db.VarChar(40)
19 | deliveryDestination String @db.VarChar(40)
20 | }
21 |
22 | model LastTimestampForNotifications {
23 | id Int @id @default(autoincrement())
24 | lastWrittenTimestamp Int
25 | }
26 |
27 | model BackedMetrics {
28 | id Int @id @default(autoincrement())
29 | emailsSentPastDay Int
30 | }
31 |
--------------------------------------------------------------------------------
/public/angelbunny-XL.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/angelbunny-XL.png
--------------------------------------------------------------------------------
/public/carousel-images/borrower.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/carousel-images/borrower.png
--------------------------------------------------------------------------------
/public/carousel-images/buy-out.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/carousel-images/buy-out.png
--------------------------------------------------------------------------------
/public/carousel-images/continued-availability.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/carousel-images/continued-availability.png
--------------------------------------------------------------------------------
/public/carousel-images/contract.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/carousel-images/contract.png
--------------------------------------------------------------------------------
/public/carousel-images/deposit-requested.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/carousel-images/deposit-requested.png
--------------------------------------------------------------------------------
/public/carousel-images/duration-restart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/carousel-images/duration-restart.png
--------------------------------------------------------------------------------
/public/carousel-images/funds-sent.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/carousel-images/funds-sent.png
--------------------------------------------------------------------------------
/public/carousel-images/loan-matures.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/carousel-images/loan-matures.png
--------------------------------------------------------------------------------
/public/carousel-images/mint-borrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/carousel-images/mint-borrow.png
--------------------------------------------------------------------------------
/public/carousel-images/not-repay.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/carousel-images/not-repay.png
--------------------------------------------------------------------------------
/public/carousel-images/other-lenders.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/carousel-images/other-lenders.png
--------------------------------------------------------------------------------
/public/carousel-images/perpetual-buyouts.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/carousel-images/perpetual-buyouts.png
--------------------------------------------------------------------------------
/public/carousel-images/potential-lenders.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/carousel-images/potential-lenders.png
--------------------------------------------------------------------------------
/public/carousel-images/repay.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/carousel-images/repay.png
--------------------------------------------------------------------------------
/public/carousel-images/to-buy-out.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/carousel-images/to-buy-out.png
--------------------------------------------------------------------------------
/public/carousel-images/transfer-loan-principal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/carousel-images/transfer-loan-principal.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/favicon.ico
--------------------------------------------------------------------------------
/public/graph-square.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/graph-square.png
--------------------------------------------------------------------------------
/public/legal/privacy-policy.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/legal/privacy-policy.pdf
--------------------------------------------------------------------------------
/public/legal/terms-of-service.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/legal/terms-of-service.pdf
--------------------------------------------------------------------------------
/public/loanAssets/local.json:
--------------------------------------------------------------------------------
1 | {
2 | "tokens": [
3 | {
4 | "address": "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82",
5 | "symbol": "DAI"
6 | },
7 | {
8 | "address": "0x",
9 | "symbol": "USDC"
10 | },
11 | {
12 | "address": "0x",
13 | "symbol": "WETH"
14 | }
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/public/loanAssets/rinkeby.json:
--------------------------------------------------------------------------------
1 | {
2 | "tokens": [
3 | {
4 | "address": "0x6916577695D0774171De3ED95d03A3239139Eddb",
5 | "symbol": "DAI"
6 | }
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/public/logos/backed-bunny.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/logos/backed-bunny.png
--------------------------------------------------------------------------------
/public/logos/opbunny.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/logos/opbunny.png
--------------------------------------------------------------------------------
/public/logos/pbunny.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/logos/pbunny.png
--------------------------------------------------------------------------------
/sentry.client.config.js:
--------------------------------------------------------------------------------
1 | // This file configures the initialization of Sentry on the browser.
2 | // The config you add here will be used whenever a page is visited.
3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/
4 |
5 | import * as Sentry from '@sentry/nextjs';
6 |
7 | const shouldInitialize = !process.env.GITHUB_ACTIONS;
8 | const SENTRY_DSN = process.env.SENTRY_DSN;
9 |
10 | shouldInitialize &&
11 | Sentry.init({
12 | dsn:
13 | SENTRY_DSN ||
14 | 'https://7e7528c337a44263b8b757289e80e7fc@o1195560.ingest.sentry.io/6318688',
15 | // Adjust this value in production, or use tracesSampler for greater control
16 | tracesSampleRate: 0.2,
17 | environment: process.env.NEXT_PUBLIC_CHAIN_ID,
18 | // ...
19 | // Note: if you want to override the automatic release value, do not set a
20 | // `release` value here - use the environment variable `SENTRY_RELEASE`, so
21 | // that it will also get attached to your source maps
22 | });
23 |
--------------------------------------------------------------------------------
/sentry.properties:
--------------------------------------------------------------------------------
1 | defaults.url=https://sentry.io/
2 | defaults.org=non-fungible-finance
3 | defaults.project=backed
4 | cli.executable=../../.npm/_npx/a8388072043b4cbc/node_modules/@sentry/cli/bin/sentry-cli
5 |
--------------------------------------------------------------------------------
/sentry.server.config.js:
--------------------------------------------------------------------------------
1 | // This file configures the initialization of Sentry on the server.
2 | // The config you add here will be used whenever the server handles a request.
3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/
4 |
5 | import * as Sentry from '@sentry/nextjs';
6 |
7 | const shouldInitialize = !process.env.GITHUB_ACTIONS;
8 | const SENTRY_DSN = process.env.SENTRY_DSN;
9 |
10 | shouldInitialize &&
11 | Sentry.init({
12 | dsn:
13 | SENTRY_DSN ||
14 | 'https://7e7528c337a44263b8b757289e80e7fc@o1195560.ingest.sentry.io/6318688',
15 | // Adjust this value in production, or use tracesSampler for greater control
16 | tracesSampleRate: 0.2,
17 | environment: process.env.NEXT_PUBLIC_CHAIN_ID,
18 | // ...
19 | // Note: if you want to override the automatic release value, do not set a
20 | // `release` value here - use the environment variable `SENTRY_RELEASE`, so
21 | // that it will also get attached to your source maps
22 | });
23 |
--------------------------------------------------------------------------------
/stories/Carousel.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Carousel } from 'components/Carousel';
3 |
4 | export default {
5 | title: 'components/Carousel',
6 | component: Carousel,
7 | };
8 |
9 | export const CarouselStyles = () => {
10 | return ;
11 | };
12 |
--------------------------------------------------------------------------------
/stories/Disclosure.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ThreeColumn } from 'components/layouts/ThreeColumn';
3 | import { Fieldset } from 'components/Fieldset';
4 | import { FormWrapper } from 'components/layouts/FormWrapper';
5 | import { Disclosure } from 'components/Disclosure';
6 |
7 | export default {
8 | title: 'Components/Disclosure',
9 | component: Disclosure,
10 | };
11 |
12 | export const Disclosures = () => {
13 | return (
14 |
15 |
16 |
17 |
18 |
19 | Peanut Butter
20 | Jam
21 | Pickles
22 |
23 |
24 |
27 |
28 | Basket
29 | Blanket
30 | Umbrella
31 |
32 |
33 |
34 |
35 |
36 | );
37 | };
38 |
--------------------------------------------------------------------------------
/stories/Fallback.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Fallback } from 'components/Media/Fallback';
3 | import { ThreeColumn } from 'components/layouts/ThreeColumn';
4 | import { Fieldset } from 'components/Fieldset';
5 |
6 | export default {
7 | title: 'components/Media/Fallback',
8 | component: Fallback,
9 | };
10 |
11 | export const FieldsetStyles = () => {
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/stories/Fieldset.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Fieldset } from 'components/Fieldset';
3 | import { Input } from 'components/Input';
4 | import { Button } from 'components/Button';
5 | import { FormWrapper } from 'components/layouts/FormWrapper';
6 |
7 | export default {
8 | title: 'components/Fieldset',
9 | component: Fieldset,
10 | };
11 |
12 | export const FieldsetStyles = () => {
13 | return (
14 |
15 |
16 | {}} />
17 | {}} />
18 | Don't do anything
19 |
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/stories/Input.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { ChangeEvent, HTMLAttributes } from 'react';
2 |
3 | import { Input } from 'components/Input';
4 |
5 | export default {
6 | title: 'Components/Input',
7 | component: Input,
8 | };
9 |
10 | export const InputStyles = () => {
11 | return (
12 | <>
13 |
21 |
22 |
23 |
24 |
25 |
26 |
34 |
35 |
36 |
37 |
38 |
39 | >
40 | );
41 | };
42 |
--------------------------------------------------------------------------------
/stories/LoanCard.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { TwelveColumn } from 'components/layouts/TwelveColumn';
3 | import { LoanCard } from 'components/LoanCard';
4 | import {
5 | LoanCardLoaded,
6 | LoanCardLoading,
7 | ExpandedAttributes,
8 | Relationship,
9 | } from 'components/LoanCard/LoanCard';
10 | import { GetNFTInfoResponse } from 'lib/getNFTInfo';
11 | import { baseLoan } from 'lib/mockData';
12 |
13 | export default {
14 | title: 'components/LoanCard',
15 | component: LoanCard,
16 | };
17 |
18 | export const LoanCards = () => {
19 | return (
20 |
21 |
22 | borrower
23 |
24 |
25 |
36 | borrower
37 |
38 |
39 |
40 | );
41 | };
42 |
--------------------------------------------------------------------------------
/stories/LoanInfo.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { LoanInfo } from 'components/LoanInfo';
3 | import { baseLoan, loanWithLenderAccruing } from 'lib/mockData';
4 | import { CollateralSaleInfo } from 'lib/loans/collateralSaleInfo';
5 | import {
6 | getFakeFloor,
7 | getFakeItemsAndOwners,
8 | getFakeVolume,
9 | } from 'lib/nftCollectionStats/mockData';
10 |
11 | export default {
12 | title: 'components/LoanInfo',
13 | component: LoanInfo,
14 | };
15 |
16 | const [items, owners] = getFakeItemsAndOwners();
17 | const collectionStats = {
18 | floor: getFakeFloor(),
19 | items,
20 | owners,
21 | volume: getFakeVolume(),
22 | };
23 |
24 | const saleInfo: CollateralSaleInfo = {
25 | recentSale: null,
26 | collectionStats,
27 | };
28 |
29 | export function LoanInfoNoLender() {
30 | return ;
31 | }
32 |
33 | export function LoanInfoWithLender() {
34 | return (
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/stories/Marquee.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Marquee } from 'components/Marquee';
4 |
5 | export default {
6 | title: 'Components/Marquee',
7 | component: Marquee,
8 | };
9 |
10 | const messages = [
11 | 'this is some text',
12 | 'so is this',
13 | 'here is somewhat longer text for testing purposes',
14 | ];
15 |
16 | export const MarqueeStyles = () => {
17 | return ;
18 | };
19 |
--------------------------------------------------------------------------------
/stories/Modal.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Modal } from 'components/Modal';
3 | import { useDialogState, DialogDisclosure } from 'reakit/Dialog';
4 |
5 | export default {
6 | title: 'Components/Modal',
7 | component: Modal,
8 | };
9 |
10 | export const ModalStyles = () => {
11 | const dialog = useDialogState({ visible: true });
12 | return (
13 |
14 |
relaunch modal
15 |
16 | yote
17 | yote
18 | yote
19 | yote
20 | yote
21 | yote
22 | yote
23 | yote
24 | yote
25 | yote
26 | yote
27 | yote
28 | yote
29 | yote
30 | yote
31 | yote
32 | yote
33 | yote
34 | yote
35 | yote
36 | yote
37 | yote
38 | yote
39 | yote
40 |
41 |
42 | );
43 | };
44 |
--------------------------------------------------------------------------------
/stories/NFTCollateralPicker.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { NFTCollateralPicker } from 'components/NFTCollateralPicker/NFTCollateralPicker';
4 | import { Provider } from 'urql';
5 | import { clientFromUrl } from 'lib/urql';
6 | import { noop } from 'lodash';
7 | import { useDialogState, DialogDisclosure } from 'reakit/Dialog';
8 | import { configs } from 'lib/config';
9 |
10 | const NFTCollateralPickerStory = () => {
11 | const dialog = useDialogState({ visible: true });
12 | return (
13 |
14 | relaunch modal
15 |
20 |
21 | );
22 | };
23 |
24 | export const Wrapper = () => {
25 | return (
26 |
27 |
28 |
29 | );
30 | };
31 |
32 | export default {
33 | title: 'Components/NFTCollateralPicker',
34 | component: Wrapper,
35 | };
36 |
--------------------------------------------------------------------------------
/stories/NotificationsModal.stories.tsx:
--------------------------------------------------------------------------------
1 | import { NotificationsModal } from 'components/NotificationsModal';
2 | import React from 'react';
3 | import { useDialogState, DialogDisclosure } from 'reakit/Dialog';
4 |
5 | export const NotificationsModalStyles = () => {
6 | const dialog = useDialogState({ visible: true });
7 | return (
8 |
9 | relaunch modal
10 |
11 |
12 | );
13 | };
14 |
15 | export default {
16 | title: 'Components/NotificationsModal',
17 | component: NotificationsModal,
18 | };
19 |
--------------------------------------------------------------------------------
/stories/ParsedEvent.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ParsedEvent } from 'components/TicketHistory/ParsedEvent';
3 | import { Fieldset } from 'components/Fieldset';
4 | import { ThreeColumn } from 'components/layouts/ThreeColumn';
5 | import { baseLoan, events } from 'lib/mockData';
6 |
7 | export default {
8 | title: 'Components/TicketHistory/ParsedEvent',
9 | component: ParsedEvent,
10 | };
11 |
12 | const loan = baseLoan;
13 |
14 | export const ParsedEvents = () => {
15 | return (
16 |
17 |
18 | {events.map((e, i) => (
19 |
20 | ))}
21 |
22 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/stories/Select.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Select } from 'components/Select';
4 |
5 | export default {
6 | title: 'Components/Select',
7 | component: Select,
8 | };
9 |
10 | const options = [
11 | { value: 'chocolate', label: 'Chocolate' },
12 | { value: 'strawberry', label: 'Strawberry' },
13 | { value: 'vanilla', label: 'Vanilla' },
14 | ];
15 |
16 | export const SelectStyles = () => {
17 | return ;
18 | };
19 |
--------------------------------------------------------------------------------
/stories/ThreeColumn.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { GridItem } from 'stories/helpers';
4 | import { ThreeColumn } from 'components/layouts/ThreeColumn';
5 |
6 | export default {
7 | title: 'layouts/ThreeColumn',
8 | component: ThreeColumn,
9 | };
10 |
11 | export const LayoutEmpty = () => ;
12 |
13 | export const LayoutThreeItems = () => (
14 |
15 | {[1, 2, 3].map((index) => (
16 | {index}
17 | ))}
18 |
19 | );
20 |
21 | export const LayoutFiveItems = () => (
22 |
23 | {[1, 2, 3, 4, 5].map((index) => (
24 | {index}
25 | ))}
26 |
27 | );
28 |
--------------------------------------------------------------------------------
/stories/Toggle.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Toggle } from 'components/Toggle';
2 | import React from 'react';
3 |
4 | export default {
5 | title: 'components/Toggle',
6 | component: Toggle,
7 | };
8 |
9 | export const Toggles = () => {
10 | return (
11 | console.log({ checked })}
13 | left="Left"
14 | right="Right"
15 | />
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/stories/helpers.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent } from 'react';
2 |
3 | export const GridItem: FunctionComponent = ({ children }) => (
4 |
14 | {children}
15 |
16 | );
17 |
--------------------------------------------------------------------------------
/stories/urql.stories.tsx:
--------------------------------------------------------------------------------
1 | import { configs } from 'lib/config';
2 | import { clientFromUrl } from 'lib/urql';
3 | import React from 'react';
4 | import { Provider, useQuery } from 'urql';
5 |
6 | type Loan = {
7 | id: string;
8 | borrowTicketHolder: string;
9 | };
10 | type LoansQueryData = {
11 | loans: Loan[];
12 | };
13 |
14 | const LoansQuery = `
15 | query {
16 | loans(first: 5) {
17 | id
18 | borrowTicketHolder
19 | }
20 | }
21 | `;
22 |
23 | const Querier = () => {
24 | const [result] = useQuery({
25 | query: LoansQuery,
26 | });
27 |
28 | const { data, fetching, error } = result;
29 |
30 | if (fetching) return Loading...
;
31 | if (error) return Oh no... {error.message}
;
32 | if (!data)
33 | return Somehow we didn't fail, but also didn't receive any data...
;
34 |
35 | return (
36 |
37 | {data.loans.map((loan) => (
38 | {loan.borrowTicketHolder}
39 | ))}
40 |
41 | );
42 | };
43 |
44 | const Wrapper = () => {
45 | return (
46 |
47 |
48 |
49 | );
50 | };
51 |
52 | export default {
53 | title: 'lib/urql',
54 | component: Wrapper,
55 | };
56 |
57 | export const ItQueries = () => {
58 | return ;
59 | };
60 |
--------------------------------------------------------------------------------
/styles/fonts-maru.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'GT Maru Regular';
3 | src: url('https://with-backed-site-fonts.s3.amazonaws.com/GT-Maru-Regular.woff')
4 | format('woff'),
5 | url('https://with-backed-site-fonts.s3.amazonaws.com/GT-Maru-Light.woff')
6 | format('woff');
7 | font-display: swap;
8 | }
9 |
10 | @font-face {
11 | font-family: 'GT Maru Mono';
12 | src: url('https://with-backed-site-fonts.s3.amazonaws.com/GT-Maru-Mono-Regular.woff')
13 | format('woff'),
14 | url('https://with-backed-site-fonts.s3.amazonaws.com/GT-Maru-Mono-Regular-Oblique.woff')
15 | format('woff'),
16 | url('https://with-backed-site-fonts.s3.amazonaws.com/GT-Maru-Mono-Bold.woff')
17 | format('woff');
18 | font-display: swap;
19 | }
20 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "target": "es5",
5 | "lib": ["dom", "dom.iterable", "esnext"],
6 | "allowJs": true,
7 | "skipLibCheck": true,
8 | "strict": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "noEmit": true,
11 | "esModuleInterop": true,
12 | "module": "esnext",
13 | "moduleResolution": "node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "jsx": "preserve",
17 | "incremental": true
18 | },
19 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
20 | "exclude": ["node_modules"]
21 | }
22 |
--------------------------------------------------------------------------------
/types/CollateralMedia.ts:
--------------------------------------------------------------------------------
1 | export type CollateralMedia = {
2 | mediaUrl: string;
3 | mediaMimeType: string;
4 | };
5 |
--------------------------------------------------------------------------------
/types/Loan.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from 'ethers';
2 |
3 | export type Loan = {
4 | id: ethers.BigNumber;
5 | loanAssetContractAddress: string;
6 | collateralContractAddress: string;
7 | collateralTokenId: ethers.BigNumber;
8 | collateralName: string;
9 | perAnumInterestRate: ethers.BigNumber;
10 | accumulatedInterest: ethers.BigNumber;
11 | lastAccumulatedTimestamp: ethers.BigNumber;
12 | durationSeconds: ethers.BigNumber;
13 | loanAmount: ethers.BigNumber;
14 | closed: boolean;
15 | loanAssetDecimals: number;
16 | loanAssetSymbol: string;
17 | lender: string | null;
18 | borrower: string;
19 | interestOwed: ethers.BigNumber;
20 | endDateTimestamp: number;
21 | collateralTokenURI: string;
22 | allowLoanAmountIncrease: boolean;
23 | };
24 |
--------------------------------------------------------------------------------
/types/NFT.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from 'ethers';
2 |
3 | export interface NFTEntity {
4 | id: string;
5 | identifier: ethers.BigNumber;
6 | uri?: string | null;
7 | registry: {
8 | symbol?: string | null;
9 | name?: string | null;
10 | };
11 | approvals: Approval[];
12 | }
13 |
14 | interface Approval {
15 | id: string;
16 | approved: {
17 | id: string;
18 | };
19 | }
20 |
--------------------------------------------------------------------------------
/types/RawEvent.ts:
--------------------------------------------------------------------------------
1 | import {
2 | BuyoutEvent,
3 | CloseEvent,
4 | CollateralSeizureEvent,
5 | CreateEvent,
6 | LendEvent,
7 | RepaymentEvent,
8 | } from './generated/graphql/nftLoans';
9 |
10 | export type RawSubgraphEvent =
11 | | BuyoutEvent
12 | | CloseEvent
13 | | CollateralSeizureEvent
14 | | CreateEvent
15 | | LendEvent
16 | | RepaymentEvent;
17 |
18 | export type RawEventNameType =
19 | | 'BuyoutEvent'
20 | | 'CloseEvent'
21 | | 'CollateralSeizureEvent'
22 | | 'CreateEvent'
23 | | 'LendEvent'
24 | | 'RepaymentEvent';
25 |
--------------------------------------------------------------------------------
/types/pinata.d.ts:
--------------------------------------------------------------------------------
1 | declare module '@pinata/ipfs-gateway-tools/dist/node' {
2 | export default class IPFSGatewayTools {
3 | containsCID(url: string): { containsCid: boolean; cid?: string };
4 | convertToDesiredGateway(
5 | sourceUrl: string,
6 | desiredGatewayPrefix: string,
7 | ): string;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/types/pirsch.d.ts:
--------------------------------------------------------------------------------
1 | declare interface Window {
2 | pirsch(
3 | eventName: string,
4 | options: { duration?: number; meta?: { [key: string]: any } },
5 | ): void;
6 | }
7 |
--------------------------------------------------------------------------------