initiating public sequence

This commit is contained in:
Linkie Link 2022-04-29 23:45:14 +02:00
commit 2ecadb938f
No known key found for this signature in database
GPG Key ID: 0D521EAABD3B3B42
384 changed files with 79043 additions and 0 deletions

26
.gitignore vendored Normal file
View File

@ -0,0 +1,26 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
yarn.lock

9
.prettierrc Normal file
View File

@ -0,0 +1,9 @@
{
"singleQuote": true,
"jsxSingleQuote": true,
"bracketSpacing": true,
"semi": false,
"tabWidth": 4,
"bracketSameLine": false,
"arrowParens": "always"
}

BIN
Mars Web App License.pdf Normal file

Binary file not shown.

28
README.md Normal file
View File

@ -0,0 +1,28 @@
# Mars Protocol Interface
## Terms of Use
To use this repository you have to agree to the terms of the [Mars Web App License](https://github.com/mars-protocol/interface/blob/main/Mars%20Web%20App%20License.pdf)
## Available Scripts
In the project directory, you can run:
### `npm run install`
Installs all packages listed inside the [package.json](https://github.com/mars-protocol/interface/blob/main/package.json)
### `npm run start`
Runs the app in the development mode.
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.
### `npm run build`
Builds the app for production to the `build` folder.
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.
Your app is ready to be deployed!

42050
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

94
package.json Normal file
View File

@ -0,0 +1,94 @@
{
"name": "mars",
"homepage": "./",
"version": "0.1.0",
"private": true,
"dependencies": {
"@apollo/client": "^3.4.15",
"@material-ui/core": "^4.11.3",
"@material-ui/icons": "^4.11.2",
"@ramonak/react-progress-bar": "^4.2.0",
"@sentry/react": "^6.17.9",
"@sentry/tracing": "^6.17.9",
"@terra-dev/wallet-types": "^3.2.0",
"@terra-money/terra.js": "^3.0.1",
"@terra-money/wallet-provider": "^3.8.0",
"@testing-library/dom": "^8.11.1",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"@tippyjs/react": "^4.2.3",
"@tns-money/tns.js": "^1.2.0",
"@types/chart.js": "^2.9.31",
"@types/jest": "^26.0.15",
"@types/lodash.throttle": "^4.1.6",
"@types/node": "^12.0.0",
"@types/numeral": "^2.0.1",
"@types/ramda": "^0.27.38",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@types/react-modal": "^3.12.0",
"@types/react-router-dom": "^5.1.7",
"@types/react-table": "^7.0.28",
"bignumber.js": "^9.0.1",
"chart.js": "^2.9.4",
"create-conical-gradient": "^1.1.0",
"graphql": "^15.6.0",
"graphql-request": "^4.2.0",
"i18next": "^21.0.2",
"i18next-browser-languagedetector": "^6.1.2",
"i18next-xhr-backend": "^3.2.2",
"lodash.isequal": "^4.5.0",
"lodash.throttle": "^4.1.1",
"moment": "^2.29.1",
"moment-duration-format": "^2.3.2",
"numeral": "^2.0.6",
"ramda": "^0.27.1",
"react": "^17.0.1",
"react-chartjs-2": "^2.11.1",
"react-currency-input-field": "^3.6.4",
"react-device-detect": "^1.17.0",
"react-dom": "^17.0.1",
"react-i18next": "^11.11.4",
"react-modal": "^3.12.1",
"react-query": "^3.34.19",
"react-router-dom": "^5.2.0",
"react-scripts": "^4.0.3",
"react-table": "^7.6.3",
"react-use-clipboard": "^1.0.7",
"sass": "^1.35.2",
"styled-components": "^5.3.3",
"typescript": "^4.1.2",
"web-vitals": "^1.0.1",
"zustand": "^3.7.1"
},
"scripts": {
"start": "REACT_APP_STAGE=localhost react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
">0.2%",
"not dead",
"not op_mini all"
]
},
"devDependencies": {
"@types/lodash.isequal": "^4.5.5",
"prettier": "^2.5.1",
"pretty-quick": "^3.1.3"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
public/banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

9
public/browserconfig.xml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#ffffff</TileColor>
</tile>
</msapplication>
</browserconfig>

29
public/favicon.svg Normal file
View File

@ -0,0 +1,29 @@
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;">
<path d="M12.985,0.812C13.949,0.812 14.279,0.928 14.929,1.155C15.091,1.211 15.273,1.275 15.49,1.345C19.947,2.795 23.25,7.071 23.25,12.005C23.25,12.829 23.133,13.721 22.963,14.506C22.196,17.622 20.213,20.269 17.543,21.804C16.213,22.568 14.67,23.131 13.068,23.239C12.8,23.257 11.93,23.249 11.339,23.243C11.126,23.241 10.95,23.239 10.85,23.239C5.061,22.419 0.75,18.142 0.75,12.005C0.75,10.447 1.047,8.887 1.653,7.454C2.282,5.968 3.362,4.699 4.486,3.6C5.476,2.634 6.7,2.1 7.906,1.574C8.081,1.498 8.255,1.422 8.428,1.345C9.05,1.068 9.469,1.008 9.996,0.932C10.214,0.9 10.45,0.866 10.727,0.812C11.214,0.719 11.726,0.75 12.234,0.781C12.486,0.797 12.738,0.812 12.985,0.812Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
<path d="M12.985,0.812C13.949,0.812 14.279,0.928 14.929,1.155C15.091,1.211 15.273,1.275 15.49,1.345C19.947,2.795 23.25,7.071 23.25,12.005C23.25,12.829 23.133,13.721 22.963,14.506C22.196,17.622 20.213,20.269 17.543,21.804C16.213,22.568 14.67,23.131 13.068,23.239C12.8,23.257 11.93,23.249 11.339,23.243C11.126,23.241 10.95,23.239 10.85,23.239C5.061,22.419 0.75,18.142 0.75,12.005C0.75,10.447 1.047,8.887 1.653,7.454C2.282,5.968 3.362,4.699 4.486,3.6C5.476,2.634 6.7,2.1 7.906,1.574C8.081,1.498 8.255,1.422 8.428,1.345C9.05,1.068 9.469,1.008 9.996,0.932C10.214,0.9 10.45,0.866 10.727,0.812C11.214,0.719 11.726,0.75 12.234,0.781C12.486,0.797 12.738,0.812 12.985,0.812Z" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:1px;"/>
<path d="M11.999,0.166L15.656,0.746L18.955,2.426L21.573,5.044L23.254,8.342L23.834,11.999L23.254,15.656L21.573,18.955L18.955,21.573L15.656,23.254L11.999,23.834L8.343,23.254L5.044,21.573L2.426,18.955L0.746,15.656L0.166,11.999L0.746,8.343L2.426,5.044L5.044,2.426L8.343,0.746L11.999,0.166ZM8.549,1.381L5.436,2.966L2.967,5.436L1.381,8.549L0.834,11.999L1.381,15.45L2.967,18.562L5.436,21.032L8.549,22.619L11.999,23.166L15.45,22.619L18.562,21.032L21.032,18.562L22.619,15.45L23.166,11.999L22.619,8.549L21.032,5.436L18.562,2.966L15.45,1.381L11.999,0.834L8.549,1.381Z" style="fill:white;"/>
<path d="M11.915,3.928C11.993,3.881 12.094,3.907 12.141,3.985L14.738,8.343C14.785,8.421 14.759,8.523 14.681,8.569C14.603,8.616 14.501,8.59 14.455,8.512L11.857,4.154C11.811,4.076 11.836,3.975 11.915,3.928Z" style="fill:white;"/>
<path d="M11.999,3.904L17.36,4.484L17.325,4.812L11.999,4.236L6.674,4.812L6.639,4.484L11.999,3.904Z" style="fill:white;"/>
<path d="M9.237,8.426C9.237,8.334 9.311,8.261 9.402,8.261L14.596,8.261C14.687,8.261 14.761,8.334 14.761,8.426C14.761,8.517 14.687,8.591 14.596,8.591L9.402,8.591C9.311,8.591 9.237,8.517 9.237,8.426Z" style="fill:white;"/>
<path d="M12.083,3.928C12.161,3.975 12.187,4.076 12.14,4.154L9.544,8.512C9.497,8.59 9.396,8.616 9.318,8.569C9.24,8.523 9.214,8.421 9.261,8.343L11.857,3.985C11.903,3.907 12.004,3.881 12.083,3.928Z" style="fill:white;"/>
<path d="M8.392,0.803L11.999,3.853L15.606,0.803L17.49,4.573L17.195,4.721L15.499,1.326L11.999,4.285L8.499,1.326L6.804,4.721L6.509,4.573L8.392,0.803Z" style="fill:white;"/>
<path d="M5.143,2.563C5.217,2.509 5.32,2.525 5.374,2.599L11.999,11.719L18.625,2.599C18.679,2.525 18.782,2.509 18.856,2.563C18.929,2.616 18.946,2.719 18.892,2.793L12.133,12.097C12.102,12.14 12.052,12.165 11.999,12.165C11.947,12.165 11.897,12.14 11.866,12.097L5.107,2.793C5.053,2.719 5.07,2.616 5.143,2.563Z" style="fill:white;"/>
<path d="M9.525,8.314C9.565,8.358 9.578,8.42 9.56,8.476L7.992,13.303L12.081,16.274L16.727,18.266C16.811,18.302 16.85,18.399 16.814,18.482C16.778,18.566 16.681,18.605 16.597,18.569L11.934,16.57C11.923,16.565 11.912,16.559 11.902,16.552L7.701,13.499C7.643,13.457 7.619,13.383 7.641,13.315L9.156,8.651L4.493,9.709C4.404,9.729 4.315,9.674 4.295,9.585C4.275,9.496 4.331,9.408 4.42,9.387L9.367,8.265C9.424,8.251 9.485,8.27 9.525,8.314Z" style="fill:white;"/>
<path d="M4.347,9.426C4.416,9.366 4.52,9.373 4.58,9.442L7.922,13.259C7.982,13.328 7.975,13.432 7.907,13.492C7.838,13.552 7.734,13.545 7.674,13.477L4.332,9.659C4.272,9.591 4.279,9.486 4.347,9.426Z" style="fill:white;"/>
<path d="M6.931,4.439L4.635,9.553L4.661,9.616L4.603,9.652L3.534,14.751L7.435,18.284L12.078,20.948L15.442,22.696L16.535,18.244L21.105,18.581L20.481,14.837L20.807,14.783L21.499,18.941L16.789,18.593L15.662,23.182L11.917,21.236L7.24,18.553L3.238,14.928L0.234,11.971L4.25,9.483L2.464,5.109L6.931,4.439ZM4.215,9.893L0.765,12.03L3.254,14.48L4.215,9.893ZM4.463,9.131L6.382,4.855L2.928,5.373L4.463,9.131Z" style="fill:white;"/>
<path d="M7.814,13.202C7.904,13.211 7.971,13.291 7.963,13.382L7.502,18.433C7.494,18.524 7.414,18.591 7.323,18.583C7.232,18.574 7.165,18.494 7.174,18.403L7.634,13.352C7.642,13.261 7.723,13.194 7.814,13.202Z" style="fill:white;"/>
<path d="M12.151,16.353C12.187,16.436 12.148,16.533 12.065,16.569L7.403,18.569C7.319,18.604 7.222,18.566 7.186,18.482C7.15,18.398 7.189,18.301 7.273,18.265L11.935,16.266C12.018,16.23 12.115,16.269 12.151,16.353Z" style="fill:white;"/>
<path d="M2.893,18.581L3.517,14.837L3.191,14.783L2.499,18.941L7.211,18.594L8.336,23.182L12.075,21.24L11.923,20.947L8.556,22.696L7.465,18.244L2.893,18.581Z" style="fill:white;"/>
<path d="M12.097,11.865C12.14,11.896 12.165,11.946 12.165,11.998L12.165,23.5C12.165,23.591 12.091,23.665 12,23.665C11.909,23.665 11.835,23.591 11.835,23.5L11.835,12.226L1.115,15.709C1.029,15.737 0.936,15.69 0.908,15.603C0.879,15.517 0.927,15.423 1.013,15.395L11.949,11.842C11.999,11.825 12.054,11.834 12.097,11.865Z" style="fill:white;"/>
<path d="M17.212,4.547C17.248,4.499 17.308,4.475 17.368,4.484L21.327,5.078C21.377,5.085 21.421,5.115 21.446,5.159C21.471,5.203 21.474,5.256 21.455,5.303L19.749,9.483L23.587,11.861C23.63,11.887 23.658,11.932 23.664,11.982C23.669,12.033 23.652,12.083 23.616,12.118L20.762,14.926L20.756,14.932L20.755,14.932C20.755,14.933 20.754,14.934 20.753,14.934L16.773,18.541C16.727,18.583 16.661,18.595 16.602,18.572C16.544,18.55 16.504,18.496 16.498,18.434L16.039,13.4L14.44,8.478C14.421,8.421 14.435,8.359 14.475,8.315C14.515,8.272 14.575,8.253 14.633,8.266L19.258,9.315L17.193,4.715C17.168,4.659 17.175,4.595 17.212,4.547ZM19.237,9.649L14.843,8.652L16.269,13.04L19.237,9.649ZM16.372,13.422L16.797,18.074L20.464,14.751L19.45,9.907L16.372,13.422ZM19.784,9.893L20.744,14.48L23.234,12.03L19.784,9.893ZM19.536,9.13L21.07,5.373L17.617,4.855L19.536,9.13Z" style="fill:white;"/>
<path d="M11.842,11.947C11.87,11.861 11.963,11.813 12.05,11.842L22.987,15.395C23.074,15.423 23.121,15.517 23.093,15.603C23.065,15.69 22.972,15.737 22.885,15.709L11.948,12.155C11.861,12.127 11.814,12.034 11.842,11.947Z" style="fill:white;"/>
<path d="M16.335,13.27C16.389,13.343 16.372,13.447 16.298,13.5L12.096,16.552C12.022,16.606 11.919,16.59 11.866,16.516C11.812,16.442 11.828,16.339 11.902,16.285L16.104,13.233C16.178,13.18 16.281,13.196 16.335,13.27Z" style="fill:white;"/>
<path d="M16.58,18.276C16.653,18.234 16.746,18.254 16.796,18.322L18.892,21.208C18.945,21.282 18.929,21.385 18.855,21.439C18.782,21.492 18.678,21.476 18.625,21.402L16.616,18.636L12.081,21.237C12.002,21.282 11.901,21.255 11.856,21.176C11.81,21.097 11.838,20.996 11.917,20.951L16.58,18.276Z" style="fill:white;"/>
<path d="M11.999,0.335C12.09,0.335 12.164,0.409 12.164,0.5L12.164,4.068C12.164,4.159 12.09,4.233 11.999,4.233C11.908,4.233 11.834,4.159 11.834,4.068L11.834,0.5C11.834,0.409 11.908,0.335 11.999,0.335Z" style="fill:white;"/>
<path d="M23.093,8.396C23.121,8.483 23.073,8.576 22.987,8.604L19.594,9.706C19.507,9.735 19.414,9.687 19.386,9.601C19.358,9.514 19.405,9.421 19.492,9.393L22.885,8.29C22.971,8.262 23.064,8.309 23.093,8.396Z" style="fill:white;"/>
<path d="M7.435,18.286C7.509,18.34 7.525,18.443 7.471,18.516L5.374,21.402C5.32,21.476 5.217,21.492 5.143,21.439C5.069,21.385 5.053,21.282 5.107,21.208L7.204,18.322C7.258,18.249 7.361,18.232 7.435,18.286Z" style="fill:white;"/>
<path d="M0.908,8.396C0.936,8.309 1.029,8.262 1.115,8.29L4.508,9.393C4.595,9.421 4.642,9.514 4.614,9.601C4.586,9.687 4.493,9.735 4.406,9.706L1.013,8.604C0.927,8.576 0.879,8.483 0.908,8.396Z" style="fill:white;"/>
<defs>
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-14.8673,-14.7971,14.7971,-14.8673,20.4244,16.9042)"><stop offset="0" style="stop-color:rgb(239,65,54);stop-opacity:1"/><stop offset="0.01" style="stop-color:rgb(239,65,54);stop-opacity:1"/><stop offset="0.32" style="stop-color:rgb(223,81,83);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(172,11,27);stop-opacity:1"/></linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 8.6 KiB

35
public/index.html Normal file
View File

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="https://app.marsprotocol.io/favicon.svg" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#dd5b65" />
<meta name="robots" content="index,follow" />
<meta name="description"
content="Lend, borrow and earn on the galaxy's most powerful credit protocol or enter the Fields of Mars for advanced DeFi strategies." />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@mars_protocol" />
<meta name="twitter:creator" content="@mars_protocol" />
<meta property="og:url" content="https://app.marsprotocol.io" />
<meta property="og:title" content="Mars Protocol Application - Powered by Terra" />
<meta property="og:description"
content="Lend, borrow and earn on the galaxy's most powerful credit protocol or enter the Fields of Mars for advanced DeFi strategies." />
<meta property="og:image" content="https://app.marsprotocol.io/banner.png" />
<meta property="og:site_name" content="Mars Protocol" />
<meta name="msapplication-TileColor" content="#ffffff" />
<meta name="theme-color" content="#ffffff" />
<meta name="terra-webextension" />
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no" />
<title>Mars Protocol</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root">
</div>
</body>
</html>

24
public/logo.svg Normal file
View File

@ -0,0 +1,24 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.6386 3.90217L12.0968 2.36037L10.1538 1.37085L7.99998 1.0293L5.8462 1.37085L3.90315 2.36037L2.36135 3.90217L1.37182 5.84523L1.03027 7.999L1.37182 10.1528L2.36135 12.0958L3.90315 13.6376L5.8462 14.628L7.99998 14.9696L10.1538 14.628L12.0968 13.6376L13.6386 12.0958L14.629 10.1528L14.9706 7.999L14.629 5.84523L13.6386 3.90217Z" stroke="white" stroke-width="0.8" stroke-miterlimit="10"/>
<path d="M9.5738 5.83309L7.99951 3.19202" stroke="white" stroke-width="0.4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.76147 3.54235L7.9996 3.19202L11.2386 3.54235" stroke="white" stroke-width="0.4" stroke-miterlimit="10"/>
<path d="M6.4259 5.83167H9.5736" stroke="white" stroke-width="0.4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.99931 3.19202L6.4259 5.83309" stroke="white" stroke-width="0.4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.76147 3.54158L5.84583 1.37024L7.9996 3.19125L10.1534 1.37024L11.2386 3.54158" stroke="white" stroke-width="0.4" stroke-miterlimit="10"/>
<path d="M12.0966 2.35925L7.99978 7.99788L3.90295 2.35925" stroke="white" stroke-width="0.4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.42786 6.51213L6.42629 5.83167L5.45344 8.82571L7.9997 10.6757L10.826 11.8874" stroke="white" stroke-width="0.4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.42786 6.51355L5.45344 8.82713" stroke="white" stroke-width="0.4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.75948 9.70068L1.02979 7.9982L3.42765 6.51259L2.36086 3.90137L4.76136 3.54138L3.42765 6.51259L2.75948 9.70068ZM2.75948 9.70068L5.17403 11.8878L7.99949 13.5086L10.1533 14.6272L10.8258 11.8878L13.6381 12.095L13.2395 9.70068" stroke="white" stroke-width="0.4" stroke-miterlimit="10"/>
<path d="M5.45328 8.82605L5.17407 11.8877" stroke="white" stroke-width="0.4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.17407 11.8871L7.99953 10.6754" stroke="white" stroke-width="0.4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.99923 13.509L5.84545 14.6276L5.17376 11.8882L2.3606 12.0954L2.75922 9.70105" stroke="white" stroke-width="0.4" stroke-miterlimit="10"/>
<path d="M1.3717 10.151L7.99986 7.99719V14.9678" stroke="white" stroke-width="0.4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.5468 8.82617L10.826 11.8878L13.2397 9.70068L14.9703 7.9982L12.5724 6.51259L13.6383 3.90137L11.2387 3.54138L12.5724 6.51259L9.57397 5.83213L10.5468 8.82617ZM10.5468 8.82617L12.5724 6.51259L13.2397 9.70068" stroke="white" stroke-width="0.4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.99951 7.99719L14.6285 10.151" stroke="white" stroke-width="0.4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.99951 10.676L10.5466 8.82605" stroke="white" stroke-width="0.4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.99951 13.5094L10.8259 11.8885L12.0963 13.6376" stroke="white" stroke-width="0.4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.99951 3.191V1.02844" stroke="white" stroke-width="0.4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.572 6.51278L14.6283 5.8446" stroke="white" stroke-width="0.4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.17432 11.8885L3.90295 13.6376" stroke="white" stroke-width="0.4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.42802 6.51278L1.3717 5.8446" stroke="white" stroke-width="0.4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
public/mstile-144x144.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
public/mstile-150x150.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
public/mstile-310x150.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
public/mstile-310x310.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

BIN
public/mstile-70x70.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

3
public/robots.txt Normal file
View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -0,0 +1,247 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.14, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M3405 6875 c-44 -12 -642 -268 -654 -280 -2 -3 158 -5 357 -5 l362 0
0 150 c0 83 -3 150 -7 149 -5 0 -30 -6 -58 -14z"/>
<path d="M3530 6739 l0 -149 363 0 c209 0 356 4 347 9 -53 31 -642 279 -672
284 l-38 6 0 -150z"/>
<path d="M3865 6832 c10 -9 519 -232 529 -232 7 0 99 130 95 134 -2 2 -139 25
-304 51 -165 25 -307 48 -315 51 -9 3 -11 2 -5 -4z"/>
<path d="M2795 6783 c-154 -25 -282 -47 -284 -49 -4 -4 88 -134 95 -134 4 0
351 150 499 216 l30 13 -30 0 c-16 -1 -156 -21 -310 -46z"/>
<path d="M2148 6573 c-158 -81 -285 -149 -284 -151 5 -4 660 150 668 157 7 6
-81 141 -92 141 -3 0 -134 -67 -292 -147z"/>
<path d="M4511 6656 c-25 -35 -44 -67 -43 -72 1 -8 649 -165 665 -161 7 2
-564 297 -574 297 -2 0 -24 -29 -48 -64z"/>
<path d="M2797 6325 c80 -113 148 -205 152 -205 8 0 461 398 461 405 0 3 -171
5 -379 5 l-380 0 146 -205z"/>
<path d="M3811 6325 c128 -113 236 -205 239 -205 7 0 290 394 290 404 0 3
-171 6 -380 6 l-381 0 232 -205z"/>
<path d="M3261 6312 l-234 -207 237 -3 c130 -1 342 -1 473 0 l236 3 -233 205
c-128 113 -235 206 -239 207 -3 1 -111 -91 -240 -205z"/>
<path d="M2246 6208 c-185 -167 -332 -304 -326 -306 11 -4 927 175 952 186 14
6 -9 43 -130 214 -81 114 -150 208 -154 208 -4 0 -158 -136 -342 -302z"/>
<path d="M4258 6302 c-121 -171 -144 -208 -130 -214 24 -10 943 -189 953 -186
9 3 -643 596 -664 603 -7 3 -76 -86 -159 -203z"/>
<path d="M2040 6401 c-405 -97 -500 -124 -500 -143 0 -10 247 -355 260 -363 5
-3 534 466 679 601 12 11 -67 -6 -439 -95z"/>
<path d="M4521 6496 c174 -161 674 -604 679 -601 13 8 260 353 260 363 0 20
-92 46 -503 143 -394 93 -446 105 -436 95z"/>
<path d="M1448 6182 c-33 -33 -490 -773 -483 -780 4 -4 755 426 773 443 11 10
-10 45 -113 186 -70 96 -130 177 -134 181 -4 5 -23 -9 -43 -30z"/>
<path d="M5381 6039 c-107 -147 -130 -184 -119 -194 18 -16 769 -447 773 -443
7 8 -451 747 -484 781 l-38 39 -132 -183z"/>
<path d="M3022 6008 c14 -18 124 -174 247 -345 122 -172 226 -313 231 -313 5
0 109 141 231 313 122 171 233 327 247 345 l24 32 -502 0 -502 0 24 -32z"/>
<path d="M2350 5924 c-261 -52 -481 -97 -487 -99 -11 -4 355 -524 407 -578 17
-18 24 -10 185 210 430 582 416 563 392 562 -12 -1 -236 -44 -497 -95z"/>
<path d="M4213 5908 c45 -62 177 -241 291 -396 200 -272 209 -283 226 -265 51
54 418 574 407 578 -22 8 -975 195 -991 195 -11 0 12 -38 67 -112z"/>
<path d="M2643 5613 c-161 -219 -291 -399 -289 -400 4 -5 1078 96 1083 101 6
6 -486 696 -495 696 -4 0 -139 -179 -299 -397z"/>
<path d="M3805 5665 c-136 -191 -245 -348 -243 -351 5 -5 1080 -105 1084 -101
5 4 -576 791 -586 795 -5 2 -119 -153 -255 -343z"/>
<path d="M995 5730 l-229 -229 38 -55 c37 -54 64 -83 70 -75 1 2 76 126 166
274 90 149 170 280 178 293 8 12 13 22 11 22 -2 0 -107 -103 -234 -230z"/>
<path d="M5782 5940 c8 -14 90 -150 183 -302 l168 -278 48 65 c27 35 49 68 49
72 0 5 -104 112 -232 238 -127 127 -225 219 -216 205z"/>
<path d="M1546 5334 c-136 -245 -246 -447 -244 -449 5 -5 910 294 921 305 8 7
-409 590 -422 590 -3 0 -118 -201 -255 -446z"/>
<path d="M4981 5489 c-116 -159 -211 -293 -211 -297 1 -8 922 -313 928 -307 5
5 -491 895 -499 895 -4 0 -101 -131 -218 -291z"/>
<path d="M1313 5531 c-216 -124 -393 -230 -393 -234 0 -4 68 -100 152 -212
130 -176 153 -202 163 -187 28 42 477 853 474 856 -2 1 -181 -99 -396 -223z"/>
<path d="M5294 5743 c8 -19 452 -816 471 -845 10 -16 32 9 163 186 84 113 152
208 152 213 0 4 -80 54 -178 109 -97 56 -275 159 -396 228 -136 79 -216 120
-212 109z"/>
<path d="M583 5158 c-78 -155 -141 -283 -140 -284 1 -1 52 55 113 125 61 69
148 168 194 218 l82 93 -48 65 c-26 36 -50 65 -53 65 -3 0 -70 -127 -148 -282z"/>
<path d="M6216 5375 l-48 -65 139 -157 c76 -86 163 -184 193 -219 30 -34 56
-61 57 -60 4 4 -282 566 -288 566 -3 0 -27 -29 -53 -65z"/>
<path d="M2868 5200 c-278 -26 -511 -49 -517 -51 -9 -4 567 -808 579 -809 5 0
501 879 508 900 5 14 -6 13 -570 -40z"/>
<path d="M3562 5240 c10 -29 504 -899 509 -897 9 3 588 801 585 805 -3 3
-1047 102 -1079 102 -10 0 -17 -4 -15 -10z"/>
<path d="M877 5233 c-4 -8 -166 -501 -213 -651 l-23 -74 22 15 c12 7 128 76
257 152 129 76 241 144 249 150 11 11 -11 46 -130 206 -167 228 -155 213 -162
202z"/>
<path d="M5967 5038 c-84 -112 -145 -204 -140 -209 17 -17 516 -309 521 -305
5 5 -217 697 -227 708 -3 3 -73 -84 -154 -194z"/>
<path d="M3243 4771 c-139 -248 -253 -452 -253 -455 0 -3 230 -6 510 -6 281 0
510 3 510 6 0 9 -505 904 -510 904 -3 0 -119 -202 -257 -449z"/>
<path d="M555 4910 c-198 -224 -276 -324 -263 -335 2 -1 65 -23 140 -48 l138
-45 111 340 c61 187 109 341 107 343 -2 2 -107 -113 -233 -255z"/>
<path d="M6210 5165 c0 -5 50 -161 110 -346 l110 -337 138 45 c75 25 138 47
140 48 14 11 -69 117 -263 335 -264 296 -235 265 -235 255z"/>
<path d="M1745 4965 c-258 -86 -459 -158 -458 -164 2 -8 477 -655 499 -680 2
-2 6 -2 9 1 16 16 431 998 422 997 -7 -1 -219 -70 -472 -154z"/>
<path d="M4804 5043 c19 -43 116 -269 216 -503 100 -234 183 -426 184 -427 1
-2 118 153 259 344 142 191 254 350 250 354 -8 6 -925 309 -937 309 -3 0 10
-35 28 -77z"/>
<path d="M2286 5101 c-11 -17 -436 -1005 -433 -1007 3 -3 1017 203 1022 208 3
2 -562 788 -580 806 -2 2 -6 -1 -9 -7z"/>
<path d="M4413 4707 c-160 -221 -290 -403 -288 -406 4 -3 1019 -210 1022 -207
3 2 -428 1000 -435 1008 -4 4 -139 -174 -299 -395z"/>
<path d="M896 4595 c-142 -85 -261 -156 -264 -159 -6 -6 239 -576 247 -576 5
0 278 830 289 876 2 8 0 14 -5 13 -4 0 -124 -70 -267 -154z"/>
<path d="M1083 4283 c-84 -257 -151 -469 -150 -470 4 -4 801 248 809 255 6 7
-483 674 -499 679 -5 2 -77 -207 -160 -464z"/>
<path d="M5504 4414 c-137 -185 -249 -339 -249 -342 0 -7 806 -264 811 -259 5
5 -301 937 -307 937 -3 0 -118 -151 -255 -336z"/>
<path d="M5832 4733 c21 -75 284 -873 289 -873 7 0 253 570 247 575 -8 8 -528
315 -534 315 -4 0 -4 -8 -2 -17z"/>
<path d="M266 4503 c-11 -28 -5 -131 24 -428 17 -165 30 -307 30 -314 0 -9 3
-11 9 -5 12 13 224 665 217 671 -2 3 -65 24 -140 48 -111 35 -136 40 -140 28z"/>
<path d="M6591 4473 c-73 -23 -136 -44 -138 -47 -6 -5 206 -658 218 -670 6 -6
9 -2 9 10 0 10 14 154 30 319 30 305 36 402 22 421 -5 7 -51 -4 -141 -33z"/>
<path d="M6282 4074 c-68 -157 -121 -288 -118 -291 7 -8 470 -152 474 -148 4
3 -215 689 -227 712 -4 7 -62 -116 -129 -273z"/>
<path d="M588 4343 c-11 -18 -230 -705 -226 -708 4 -4 467 141 475 148 5 5
-224 544 -238 560 -4 5 -9 5 -11 0z"/>
<path d="M3000 4244 c0 -11 492 -684 500 -684 8 0 500 673 500 684 0 3 -225 6
-500 6 -275 0 -500 -3 -500 -6z"/>
<path d="M2367 4137 c-329 -66 -497 -104 -495 -112 5 -19 693 -763 699 -756 4
3 66 191 139 416 184 568 180 555 167 554 -7 -1 -236 -46 -510 -102z"/>
<path d="M4124 4199 c54 -176 300 -925 305 -930 7 -7 694 737 699 756 2 8
-155 43 -485 109 -268 54 -498 100 -510 103 -22 5 -22 4 -9 -38z"/>
<path d="M2830 3869 c-61 -189 -131 -404 -155 -478 -25 -74 -43 -136 -42 -138
3 -3 809 256 815 262 4 3 -470 658 -493 684 -11 11 -33 -48 -125 -330z"/>
<path d="M3800 3864 c-138 -190 -250 -347 -248 -349 6 -5 812 -265 814 -262 4
3 -303 947 -309 953 -3 3 -118 -151 -257 -342z"/>
<path d="M166 3874 c-27 -168 -46 -307 -43 -311 7 -6 144 34 150 45 5 8 -48
561 -55 567 -2 2 -26 -133 -52 -301z"/>
<path d="M6776 4150 c-3 -19 -15 -134 -26 -255 -11 -121 -23 -236 -26 -256
l-5 -35 75 -24 c42 -13 79 -21 82 -17 5 5 -79 576 -90 612 -2 6 -6 -6 -10 -25z"/>
<path d="M1346 3879 c-225 -71 -410 -130 -412 -131 -1 -2 19 -32 45 -68 27
-36 158 -218 292 -404 198 -277 244 -336 250 -320 10 23 240 1046 237 1049 -2
2 -187 -55 -412 -126z"/>
<path d="M5243 3987 c3 -12 56 -245 117 -517 61 -272 114 -504 118 -514 7 -16
61 54 299 385 159 222 289 405 289 406 -1 1 -175 57 -387 123 -211 66 -397
125 -413 130 -26 9 -28 8 -23 -13z"/>
<path d="M1694 3448 c-66 -293 -117 -534 -113 -536 3 -1 215 65 470 148 255
83 468 151 472 153 9 3 -691 767 -702 767 -4 0 -61 -240 -127 -532z"/>
<path d="M4844 3623 c-345 -375 -369 -401 -362 -409 8 -7 930 -305 937 -302 9
4 -231 1068 -240 1068 -3 0 -154 -161 -335 -357z"/>
<path d="M590 3643 c-129 -41 -236 -76 -237 -77 -3 -3 348 -791 365 -822 14
-24 12 -32 72 476 27 234 52 442 56 463 4 27 2 37 -8 36 -7 -1 -119 -35 -248
-76z"/>
<path d="M6154 3684 c3 -22 28 -230 56 -464 27 -234 52 -441 56 -460 5 -31 7
-32 17 -15 17 31 367 818 364 820 -2 2 -423 134 -481 152 -16 4 -17 1 -12 -33z"/>
<path d="M900 3669 c0 -8 -27 -240 -60 -514 -32 -275 -58 -501 -56 -502 3 -4
687 216 695 223 4 3 -539 767 -565 794 -12 12 -14 12 -14 -1z"/>
<path d="M5810 3288 c-157 -219 -286 -401 -288 -406 -2 -7 687 -236 694 -229
3 2 -110 968 -119 1017 -1 8 -130 -164 -287 -382z"/>
<path d="M188 3516 c-37 -12 -70 -25 -73 -28 -5 -5 90 -629 99 -653 3 -5 5 1
5 14 1 30 49 604 55 654 5 43 5 43 -86 13z"/>
<path d="M6726 3503 c5 -43 54 -627 55 -658 0 -11 2 -15 5 -10 7 18 105 647
101 651 -5 5 -148 54 -158 54 -4 0 -5 -17 -3 -37z"/>
<path d="M3058 3327 c-219 -72 -398 -134 -398 -138 0 -7 782 -579 802 -587 4
-2 8 190 8 427 0 237 -3 431 -7 430 -5 -1 -187 -60 -405 -132z"/>
<path d="M3530 3029 c0 -236 2 -429 5 -429 3 0 537 386 764 553 l53 39 -363
119 c-200 66 -385 126 -411 133 l-48 15 0 -430z"/>
<path d="M326 3428 c-9 -44 -66 -784 -66 -853 0 -38 4 -75 8 -81 5 -7 73 10
215 56 114 37 211 70 214 74 5 5 -355 826 -363 826 -2 0 -6 -10 -8 -22z"/>
<path d="M6480 3040 c-100 -226 -180 -413 -177 -416 3 -4 100 -37 214 -74 142
-46 210 -63 215 -56 4 6 8 45 8 86 -1 88 -64 858 -72 865 -3 3 -88 -179 -188
-405z"/>
<path d="M2070 3006 c-256 -84 -465 -156 -465 -161 1 -11 814 -729 820 -724 2
2 28 222 59 489 31 267 58 500 61 518 3 17 2 32 -2 31 -4 0 -217 -69 -473
-153z"/>
<path d="M4454 3123 c3 -21 31 -254 61 -517 31 -264 57 -482 60 -484 6 -6 819
712 818 723 -1 7 -917 314 -941 315 -2 0 -1 -17 2 -37z"/>
<path d="M2545 2630 c-32 -280 -56 -512 -53 -515 5 -6 923 409 940 424 6 6
-794 593 -817 599 -7 2 -31 -175 -70 -508z"/>
<path d="M3968 2842 c-224 -163 -404 -300 -400 -303 16 -15 934 -430 939 -425
10 10 -111 1026 -122 1025 -5 0 -193 -134 -417 -297z"/>
<path d="M1152 2707 c-189 -62 -343 -116 -343 -120 0 -4 157 -177 348 -384
l348 -376 2 289 c2 159 0 382 -5 496 l-7 206 -343 -111z"/>
<path d="M5495 2617 c-3 -111 -5 -334 -3 -495 l3 -294 348 376 c191 206 348
379 348 383 1 6 -442 156 -642 217 l-47 14 -7 -201z"/>
<path d="M1561 2498 c1 -161 5 -386 8 -499 l6 -206 409 137 409 136 -404 355
c-222 195 -410 358 -416 362 -10 7 -13 -53 -12 -285z"/>
<path d="M5020 2429 l-412 -362 409 -139 c224 -76 409 -138 409 -138 5 0 16
992 11 996 -2 3 -190 -158 -417 -357z"/>
<path d="M495 2494 c-110 -37 -202 -68 -204 -70 -13 -12 82 -133 323 -413 152
-176 282 -323 288 -327 7 -5 9 1 5 17 -3 13 -45 212 -93 442 -70 329 -92 417
-104 417 -8 0 -105 -30 -215 -66z"/>
<path d="M6181 2124 c-50 -240 -90 -438 -87 -441 7 -8 575 656 597 699 11 22
19 41 17 43 -7 6 -407 135 -420 135 -11 0 -37 -108 -107 -436z"/>
<path d="M802 2480 c21 -115 183 -867 189 -874 7 -8 465 138 483 154 4 5 -137
165 -314 357 -402 433 -363 394 -358 363z"/>
<path d="M5856 2134 c-186 -201 -334 -369 -330 -374 15 -13 475 -162 481 -156
7 7 193 877 190 888 -1 4 -154 -158 -341 -358z"/>
<path d="M2993 2272 c-260 -119 -473 -219 -473 -222 1 -8 941 -563 946 -558 2
3 3 228 2 500 l-3 495 -472 -215z"/>
<path d="M3530 1990 c0 -275 3 -500 6 -500 10 0 943 554 944 560 0 4 -803 376
-922 427 l-28 12 0 -499z"/>
<path d="M451 2105 c6 -15 291 -579 295 -584 6 -6 154 41 160 51 3 4 -95 124
-217 265 -245 284 -243 282 -238 268z"/>
<path d="M6316 1843 c-125 -146 -224 -268 -220 -273 16 -14 143 -52 154 -45 9
6 301 577 297 581 -1 1 -105 -117 -231 -263z"/>
<path d="M2008 1877 c-212 -73 -389 -135 -392 -138 -4 -3 172 -135 390 -293
217 -158 397 -286 399 -284 2 2 7 194 11 426 6 360 5 422 -7 421 -8 0 -188
-59 -401 -132z"/>
<path d="M4583 1589 c3 -232 8 -423 11 -426 2 -3 183 125 400 283 218 158 393
291 389 294 -9 7 -785 270 -798 270 -5 0 -5 -190 -2 -421z"/>
<path d="M2477 1798 c-9 -274 -9 -658 1 -658 14 -1 942 295 942 300 0 4 -622
376 -901 539 l-36 21 -6 -202z"/>
<path d="M4048 1722 c-258 -152 -468 -279 -468 -282 0 -5 929 -301 943 -300 8
0 7 355 -2 657 l-6 203 -467 -278z"/>
<path d="M1278 1632 c-126 -42 -229 -81 -230 -87 -1 -10 594 -447 600 -440 2
2 -26 136 -62 299 -36 163 -66 298 -66 301 0 8 -6 7 -242 -73z"/>
<path d="M5477 1688 c-27 -108 -127 -573 -125 -579 4 -9 600 422 602 435 1 7
-247 95 -455 161 -11 4 -19 -2 -22 -17z"/>
<path d="M1590 1662 c0 -16 131 -596 136 -601 6 -6 624 52 624 58 0 6 -669
494 -731 534 -16 11 -29 14 -29 9z"/>
<path d="M5023 1397 c-205 -150 -373 -275 -373 -278 0 -6 618 -65 624 -58 4 4
136 588 136 602 0 4 -3 7 -7 6 -5 0 -176 -123 -380 -272z"/>
<path d="M873 1497 c-34 -12 -64 -24 -67 -27 -3 -3 96 -107 220 -230 l226
-225 -44 75 c-204 348 -254 430 -263 429 -5 0 -38 -10 -72 -22z"/>
<path d="M5922 1313 c-67 -115 -134 -228 -148 -253 -20 -33 32 13 199 180 125
123 225 225 224 227 -5 4 -137 53 -145 53 -5 0 -63 -93 -130 -207z"/>
<path d="M1081 1419 c9 -13 88 -148 177 -299 89 -152 177 -290 195 -308 l34
-33 63 88 c35 48 74 102 87 120 l24 31 -287 209 c-158 114 -292 210 -298 212
-6 2 -3 -7 5 -20z"/>
<path d="M5635 1234 c-154 -112 -283 -208 -287 -212 -4 -4 31 -60 79 -125 l85
-119 34 33 c40 39 392 632 373 628 -2 0 -130 -92 -284 -205z"/>
<path d="M2998 1242 c-251 -81 -459 -149 -462 -152 -3 -3 194 -97 437 -210
243 -112 454 -209 470 -217 l27 -14 0 371 c0 203 -3 370 -7 369 -5 -1 -214
-67 -465 -147z"/>
<path d="M3530 1021 c0 -351 1 -371 18 -363 9 5 221 103 470 217 248 115 449
212 445 216 -5 4 -210 71 -458 149 -247 79 -456 145 -462 147 -10 4 -13 -73
-13 -366z"/>
<path d="M2065 1031 c-148 -14 -270 -29 -270 -33 0 -5 560 -416 603 -442 9 -6
12 47 12 248 l0 256 -37 -1 c-21 -1 -159 -13 -308 -28z"/>
<path d="M4590 805 c0 -213 2 -255 14 -248 7 4 147 105 310 223 225 163 292
217 279 222 -14 5 -560 58 -595 58 -5 0 -8 -115 -8 -255z"/>
<path d="M2467 792 c-4 -140 -5 -258 -2 -261 3 -6 880 82 907 91 10 4 -145 77
-738 351 l-161 74 -6 -255z"/>
<path d="M4071 836 c-250 -116 -449 -212 -442 -214 24 -8 902 -98 905 -92 1 3
0 120 -3 260 l-6 256 -454 -210z"/>
<path d="M1622 866 c-45 -62 -82 -118 -82 -123 0 -6 20 -18 43 -27 51 -19 676
-158 690 -153 9 3 -553 417 -565 417 -3 0 -41 -51 -86 -114z"/>
<path d="M5010 780 c-271 -198 -321 -236 -290 -226 8 3 161 37 339 76 321 71
401 93 401 112 0 11 -162 238 -170 238 -3 -1 -129 -91 -280 -200z"/>
<path d="M1960 524 c69 -36 196 -101 283 -145 l157 -79 0 85 c0 80 -1 85 -22
90 -181 41 -524 115 -532 115 -6 0 45 -30 114 -66z"/>
<path d="M4871 529 c-146 -32 -269 -63 -274 -68 -6 -6 -7 -40 -2 -85 l7 -75
156 79 c258 130 408 210 392 209 -8 0 -134 -27 -279 -60z"/>
<path d="M3205 545 c-115 -13 -306 -33 -422 -46 -117 -12 -213 -25 -213 -28 0
-3 62 -32 138 -63 75 -31 261 -108 412 -171 151 -63 292 -117 313 -120 l37 -6
0 230 0 229 -27 -1 c-16 -1 -122 -12 -238 -24z"/>
<path d="M3530 340 l0 -230 25 5 c14 3 37 8 52 11 24 5 811 331 822 340 2 2 2
6 0 8 -3 3 -868 96 -891 96 -5 0 -8 -104 -8 -230z"/>
<path d="M2460 359 c0 -49 3 -89 6 -89 3 0 148 -23 321 -50 174 -28 318 -49
320 -47 3 4 -98 47 -519 223 l-128 53 0 -90z"/>
<path d="M4203 308 c-172 -73 -311 -133 -309 -135 2 -2 146 19 321 47 174 27
319 50 321 50 2 0 4 38 4 85 0 62 -3 85 -12 85 -7 -1 -154 -60 -325 -132z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

19
public/site.webmanifest Normal file
View File

@ -0,0 +1,19 @@
{
"name": "Mars",
"short_name": "Mars",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

View File

@ -0,0 +1,191 @@
@use '../styles/master' as *;
.button {
transition: background-color 0.2s, border 0.2s;
color: $fontColorLightPrimary;
appearance: none;
border: none;
border-radius: $borderRadiusXXXL;
cursor: pointer;
outline: none;
display: flex;
justify-content: center;
align-items: center;
word-wrap: normal;
word-break: normal;
.prefix,
.suffix {
display: flex;
align-items: center;
display: inline-block;
}
// Sizes
&.small {
@include buttonS;
svg,
.progressIndicator {
width: rem-calc(10);
height: rem-calc(10);
}
.prefix {
margin-inline-end: space(2);
}
.suffix {
margin-inline-start: space(2);
}
}
&.medium {
@include buttonM;
svg {
width: rem-calc(12);
height: rem-calc(12);
}
.prefix {
margin-inline-end: space(3);
}
.suffix {
width: rem-calc(12);
margin-inline-start: space(3);
}
}
&.large {
@include buttonL;
svg {
width: rem-calc(18);
height: rem-calc(18);
}
.prefix {
margin-inline-end: space(4.5);
}
.suffix {
margin-inline-start: space(4.5);
}
}
// Variants
&.solid,
&.round {
@include buttonSolidPrimary;
@include buttonSolidSecondary;
@include buttonSolidTertiary;
}
&.round {
border-radius: $borderRadiusRound;
@include padding(0);
.prefix,
.suffix {
@include margin(0);
display: flex;
}
&.small {
height: rem-calc(32);
width: rem-calc(32);
svg {
width: rem-calc(12);
height: rem-calc(12);
}
}
&.medium {
height: rem-calc(40);
width: rem-calc(40);
svg {
width: rem-calc(14);
height: rem-calc(14);
}
}
&.large {
height: rem-calc(56);
width: rem-calc(56);
svg {
width: rem-calc(20);
height: rem-calc(20);
}
}
}
&.transparent {
background: none;
@include padding(0);
height: unset;
&.primary {
color: $colorPrimary;
* {
color: $colorPrimary;
}
&:hover {
color: $colorPrimaryHighlight;
* {
color: $colorPrimaryHighlight;
}
}
&:active,
&:focus {
color: $colorPrimaryHighlight;
}
}
&.secondary {
color: $colorSecondary;
* {
color: $colorSecondary;
}
&:hover,
&:active,
&:focus {
color: $colorSecondaryHighlight;
}
}
&.tertiary {
color: $colorSecondaryDark;
&:hover,
&:focus,
&:active {
color: lighten($colorSecondaryDark, 10%);
}
}
}
}
.link {
display: flex;
&:hover,
&:focus,
&:active {
text-decoration: none;
}
}
.disabled {
pointer-events: none;
opacity: 0.5 !important;
}

88
src/components/Button.tsx Normal file
View File

@ -0,0 +1,88 @@
import { CircularProgress } from '@material-ui/core'
import { ReactNode } from 'react'
import { ButtonStyleOverride } from '../types/components'
import styles from './Button.module.scss'
interface Props {
className?: any
color?: 'primary' | 'secondary' | 'tertiary'
disabled?: boolean
externalLink?: string
id?: string
suffix?: ReactNode
prefix?: ReactNode
showProgressIndicator?: boolean
size?: 'small' | 'medium' | 'large'
styleOverride?: ButtonStyleOverride
text?: string | ReactNode
variant?: 'solid' | 'transparent' | 'round'
onClick?: (e: any) => void
}
const Button = ({
className = '',
color = 'primary',
disabled,
externalLink,
id = '',
suffix,
prefix,
showProgressIndicator,
size = 'small',
styleOverride,
text,
variant = 'solid',
onClick,
}: Props) => {
const Button = () => (
<button
id={id}
onClick={disabled ? () => {} : onClick}
style={styleOverride}
className={`${styles.button} ${styles[size]} ${styles[color]} ${
styles[variant]
} ${className} ${disabled ? `${styles.disabled}` : ''}`}
>
{prefix && !showProgressIndicator && (
<div className={styles.prefix}>{prefix}</div>
)}
{text && (
<div className={styles.text}>
{showProgressIndicator ? (
<CircularProgress
color='inherit'
size={
size === 'small'
? '10px'
: size === 'medium'
? '12px'
: '18px'
}
/>
) : (
text
)}
</div>
)}
{suffix && !showProgressIndicator && (
<div className={styles.suffix}>{suffix}</div>
)}
</button>
)
return externalLink ? (
<a
href={externalLink}
target='_blank'
rel='noopener noreferrer'
className={styles.link}
>
{Button()}
</a>
) : (
Button()
)
}
export default Button

View File

@ -0,0 +1,52 @@
import { createStyles, Switch, withStyles } from '@material-ui/core'
import colors from '../styles/_assets.module.scss'
interface Props {
switchCallback: (
event: React.ChangeEvent<HTMLInputElement>,
enabled: boolean
) => void
checked: boolean
}
const CollateralSwitch = ({ switchCallback, checked }: Props) => {
const CustomSwitch = withStyles(() =>
createStyles({
root: {
width: 28,
height: 16,
padding: 0,
display: 'flex',
},
switchBase: {
padding: 2,
color: colors.grey,
'&$checked': {
transform: 'translateX(12px)',
color: colors.white,
'& + $track': {
opacity: 1,
backgroundColor: colors.primary,
borderColor: colors.primary,
},
},
},
thumb: {
width: 12,
height: 12,
boxShadow: 'none',
},
track: {
border: `1px solid ${colors.grey}`,
borderRadius: 16 / 2,
opacity: 1,
backgroundColor: colors.white,
},
checked: {},
})
)(Switch)
return <CustomSwitch checked={checked} onChange={switchCallback} />
}
export default CollateralSwitch

View File

@ -0,0 +1,70 @@
@use '../styles/master' as *;
.container {
@include layoutTooltip;
position: fixed;
display: flex;
flex-direction: column;
min-width: rem-calc(184);
box-sizing: unset;
.item {
display: block;
.valueItem {
margin-top: space(1);
display: flex;
flex-direction: row;
}
.dot {
border: 1px solid $colorWhite;
height: rem-calc(8);
border-radius: $borderRadiusRound;
width: rem-calc(8);
margin-inline-end: space(2);
margin-top: space(1.5);
display: flex;
flex: 0 0 rem-calc(8);
}
.subHeadline {
@include typoXS;
@include margin(0, 0, 2);
@include padding(0);
text-transform: uppercase;
opacity: 0.4;
height: rem-calc(16);
}
.content {
display: flex;
flex: 1 0 rem-calc(168);
flex-direction: column;
.titleContainer {
display: flex;
flex-direction: row;
:first-child {
flex: auto;
}
.titleText {
justify-content: start !important;
min-height: 0 !important;
}
}
.subTitleContainer {
display: flex;
flex-direction: row;
:first-child {
flex: auto;
}
.subTitleText {
opacity: 0.6;
}
}
}
}
}

View File

@ -0,0 +1,112 @@
import { formatValue } from '../libs/parse'
import styles from './CollectionHover.module.scss'
import { useTranslation } from 'react-i18next'
export interface HoverItem {
color?: string
name: string
amount?: number
usdValue?: number
negative?: boolean
}
interface Props {
data?: HoverItem[]
title?: string
noPercent?: boolean
}
const CollectionHover = ({ data, title, noPercent }: Props) => {
const { t } = useTranslation()
// -----------------
// CALCULATE
// -----------------
const totalUsdValue = data
? data.reduce((total, item) => total + (item.usdValue || 0), 0)
: 0
const producePercentage = (fraction: number, sum: number): string => {
if (fraction <= 0.01 || sum <= 0.01) {
return '0.00%'
} else {
return formatValue((fraction / sum) * 100, 2, 2, true, false, '%')
}
}
// -----------------
// PRESENTATION
// -----------------
const produceItem = (item: HoverItem, key: number) => {
return (
<div className={styles.item} key={key}>
{item.usdValue ? (
<div className={styles.valueItem}>
{item.color && (
<div
className={styles.dot}
style={{ backgroundColor: item.color }}
/>
)}
<div className={styles.content}>
<div className={styles.titleContainer}>
<span className={`body ${styles.titleText}`}>
{item.name}
</span>
<span className={`body ${styles.titleText}`}>
{item.amount
? formatValue(item.amount)
: formatValue(
item.usdValue,
2,
2,
true,
item.negative ? '$-' : '$'
)}
</span>
</div>
<div className={styles.subTitleContainer}>
<span
className={`caption ${styles.subTitleText}`}
>
{!noPercent &&
producePercentage(
item.usdValue,
totalUsdValue
)}
</span>
{item.amount && (
<span
className={`caption ${styles.subTitleText}`}
>
{formatValue(
item.usdValue,
2,
2,
true,
item.negative ? '$-' : '$'
)}
</span>
)}
</div>
</div>
</div>
) : (
<div className={styles.subHeadline}>{item.name}</div>
)}
</div>
)
}
return data ? (
<div className={styles.container}>
<p className='sub2'>{title ? title : t('common.summary')}</p>
{data.map((item: HoverItem, index: number) =>
produceItem(item, index)
)}
</div>
) : null
}
export default CollectionHover

View File

@ -0,0 +1,45 @@
import { useConnectedWallet, useWallet } from '@terra-money/wallet-provider'
import { ReactNode, useEffect } from 'react'
import networks from '../networks'
import useBlockHeightQuery from '../queries-new/BlockHeightQuery'
import useStore from '../store'
interface CommonContainerProps {
children: ReactNode
}
const CommonContainer = ({ children }: CommonContainerProps) => {
/**
* Network configurations
*/
const { network: extNetwork, status } = useWallet()
const networkName = extNetwork.name
const network = networks[networkName]
const connectedWallet = useConnectedWallet()
const isNetworkLoaded = useStore((s) => s.isNetworkLoaded)
const setNetworkInfo = useStore((s) => s.setNetworkInfo)
const setUserWalletAddress = useStore((s) => s.setUserWalletAddress)
const setNetworkConfig = useStore((s) => s.setNetworkConfig)
useEffect(() => {
setNetworkConfig(network || extNetwork)
}, [network, extNetwork, setNetworkConfig])
useEffect(() => {
setUserWalletAddress(connectedWallet?.terraAddress ?? '')
}, [setUserWalletAddress, connectedWallet])
useEffect(() => {
setNetworkInfo(networkName, status)
}, [networkName, status, setNetworkInfo])
/**
* Blockchain meta data
*/
useBlockHeightQuery()
return <>{isNetworkLoaded && children}</>
}
export default CommonContainer

View File

@ -0,0 +1,74 @@
@use '../styles/master' as *;
.network {
width: 100%;
left: 0;
top: 0;
z-index: 200;
position: fixed;
margin: -100% 0 0;
transition: margin 0.5s;
&.show {
@include margin(0);
}
.container {
@include margin(0);
@include padding(6, 1);
display: flex;
flex-direction: row;
flex-wrap: wrap;
text-align: center;
background-color: $colorAccent;
}
.headline {
width: 100%;
display: block;
@include typoScaps;
@include margin(0, 0, 3);
}
p {
width: 100%;
display: block;
letter-spacing: rem-calc(1);
@include typoXS;
@include margin(0, 0, 3);
}
.link {
color: $colorPrimary;
text-decoration: none;
}
.close {
position: absolute;
right: space(3);
top: space(3);
opacity: 0.6;
transition: opacity 0.5s;
&:hover {
opacity: 1;
cursor: pointer;
}
button {
border: none;
background: transparent;
@include padding(0);
&:hover {
cursor: pointer;
}
svg {
height: rem-calc(20);
width: rem-calc(20);
}
}
}
}

View File

@ -0,0 +1,91 @@
import { memo, useEffect, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import styles from './ErrorBanner.module.scss'
import { CloseSVG } from './Svg'
interface ErrorBannerProps {
hasNetworkError: boolean
hasQueryError: boolean
hasServerError: boolean
isNetworkSupported: boolean | undefined
}
const ErrorBanner = memo(
({
hasNetworkError,
hasQueryError,
hasServerError,
isNetworkSupported,
}: ErrorBannerProps) => {
const { t } = useTranslation()
const [hideError, setHideError] = useState(false)
useEffect(() => {
if (hasNetworkError || hasQueryError || hasServerError) {
setHideError(false)
}
}, [hasNetworkError, hasQueryError, hasServerError])
const getHeadline = (): string => {
return hasNetworkError
? t('common.appearToBeOffline')
: hasServerError
? t('error.serverOfflineTitle')
: t('error.failingRequest')
}
const getBody = (): string => {
return hasNetworkError
? t('error.youHaveAFailingNetworkRequest')
: hasServerError
? t('error.serverOfflineBody')
: t('error.failingRequestDescription')
}
return (
<div>
{/* Error banner is disabled for GQL errors currently */}
{(hasNetworkError || hasServerError) && isNetworkSupported && (
<div
className={
!hideError
? `${styles.network} ${styles.show}`
: styles.network
}
>
<div className={styles.container}>
<div className={styles.close}>
<button
onClick={() => {
setHideError(true)
}}
>
<CloseSVG />
</button>
</div>
<h3 className={styles.headline}>{getHeadline()}</h3>
<p>{getBody()}</p>
{!hasNetworkError && (
<p>
<Trans i18nKey={'error.problemPersists'}>
text
<a
className={styles.link}
href='https://discord.gg/marsprotocol'
target='_blank'
rel='noreferrer'
>
link
</a>
</Trans>
</p>
)}
</div>
</div>
)}
</div>
)
}
)
export default ErrorBanner

View File

@ -0,0 +1,54 @@
@use '../styles/master' as *;
.select {
border: none;
background-color: transparent;
color: $fontColorLightPrimary;
width: rem-calc(120);
height: rem-calc(37);
display: inline-block;
@include padding(0.5, 0, 0.5, 3);
appearance: none;
box-sizing: border-box;
@include typoM;
outline: none;
position: relative;
z-index: 2;
&:hover,
&:active,
&:focus {
cursor: pointer;
outline: none;
}
}
.select::-ms-expand {
display: none;
}
.selectWrapper {
border: 1px solid $buttonBorder;
color: $fontColorLightPrimary;
width: rem-calc(120);
height: rem-calc(37);
font-family: inherit;
font-size: inherit;
border-radius: $borderRadiusXXS;
display: flex;
flex: 0 0 rem-calc(37);
align-items: center;
position: relative;
&:after {
content: '';
width: rem-calc(12);
height: rem-calc(8);
background-color: $fontColorLightPrimary;
clip-path: polygon(100% 0%, 0 0%, 50% 100%);
justify-self: end;
position: absolute;
right: rem-calc(8);
z-index: 1;
}
}

View File

@ -0,0 +1,39 @@
import i18n from 'i18next'
import { useEffect, useState } from 'react'
import styles from './LanguageSelect.module.scss'
const LanguageSelect = () => {
const [currentLanguage, setCurrentLanguage] = useState('en')
useEffect(
() => {
const lang = i18n.language.substring(0, 2) || 'en'
if (currentLanguage !== lang) {
setCurrentLanguage(lang)
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[i18n.language, currentLanguage]
)
const changeLanguage = (lng: string) => {
setCurrentLanguage(lng)
i18n.changeLanguage(lng)
}
return (
<div className={styles.selectWrapper}>
<select
className={styles.select}
value={currentLanguage}
onChange={(e) => changeLanguage(e.target.value)}
>
<option value='de'>Deutsch</option>
<option value='en'>English</option>
</select>
</div>
)
}
export default LanguageSelect

View File

@ -0,0 +1,65 @@
@use '../styles/master' as *;
.notification {
width: 100%;
min-height: rem-calc(48);
display: flex;
align-items: center;
@include layoutPopover;
margin-bottom: space(8) !important;
@include padding(2, 3);
position: relative;
justify-content: center;
p {
@include margin(0);
font-weight: $fontWeightSemibold;
text-align: center;
}
&.info {
color: $colorAccent;
}
&.warning {
color: $colorInfoWarning;
}
&.error {
color: $colorInfoWarning;
}
&.closeBtnSpace {
@include padding(0, 10, 0, 0);
}
a {
display: inline-block;
@include margin(0, 1);
color: $colorSecondary;
text-decoration: underline;
&:hover,
&:focus {
text-decoration: none;
}
}
.closeNotification {
position: absolute;
right: 0;
@include margin(0, 4, 0, 0);
border: none;
background: transparent;
@include padding(0);
&:hover {
cursor: pointer;
}
svg {
width: rem-calc(14);
height: rem-calc(13);
}
}
}

View File

@ -0,0 +1,59 @@
import { ReactNode, useMemo, useState } from 'react'
import styles from './Notification.module.scss'
import { SmallCloseSVG } from './Svg'
import { useTranslation } from 'react-i18next'
import { NotificationType } from '../types/enums'
interface Props {
showNotification: boolean
content: ReactNode
type: NotificationType
hideCloseBtn?: boolean
}
const Notification = ({
showNotification,
content,
type,
hideCloseBtn = false,
}: Props) => {
const { t } = useTranslation()
const [closeNotification, setCloseNotification] = useState(false)
const typeClass = useMemo(() => {
return type === NotificationType.Warning
? styles.warning
: type === NotificationType.Error
? styles.error
: styles.info
}, [type])
return (
<>
{showNotification && !closeNotification ? (
<div
className={`${styles.notification} ${typeClass} ${
!hideCloseBtn ? styles.closeBtnSpace : null
}`}
>
<p>{content}</p>
{!hideCloseBtn && (
<button
title={t('common.close')}
className={styles.closeNotification}
onClick={() => {
setCloseNotification(true)
}}
>
<SmallCloseSVG />
</button>
)}
</div>
) : null}
</>
)
}
export default Notification

View File

@ -0,0 +1,78 @@
@use '../styles/master' as *;
.position {
display: flex;
flex: 0 0 100%;
width: 100%;
align-items: flex-start;
flex-wrap: nowrap;
min-height: rem-calc(61);
.container {
display: flex;
flex-direction: column;
width: rem-calc(200);
flex: 0 0 rem-calc(200);
.value {
word-wrap: normal;
word-break: normal;
h5 {
span {
@include typoM;
}
}
}
}
.bar {
display: flex;
flex: 1;
justify-content: flex-start;
@include margin(2, 0, 0);
height: rem-calc(8);
flex-wrap: nowrap;
> .fraction {
border-radius: $borderRadiusXXS;
display: inline-block;
@include margin(0, 0, 0, -1);
@include padding(0, 0, 0, 1);
position: relative;
&:hover {
cursor: pointer;
}
&:first-child {
@include margin(0);
@include padding(0);
}
}
}
}
.box {
box-sizing: unset;
width: 100%;
}
.compact {
display: block;
}
@media only screen and (max-width: $bpMediumHigh) {
.compact {
display: flex;
}
}
@media only screen and (max-width: $bpSmallHigh) {
.position {
.container {
width: rem-calc(120);
flex: 0 0 rem-calc(120);
}
}
}

View File

@ -0,0 +1,103 @@
import Tippy from '@tippyjs/react'
import styles from './PositionBar.module.scss'
import colors from '../styles/_assets.module.scss'
import { formatValue, lookup } from '../libs/parse'
import CollectionHover, { HoverItem } from './CollectionHover'
import { ReactElement } from 'react'
import { UST_DECIMALS, UST_DENOM } from '../constants/appConstants'
import { useTranslation } from 'react-i18next'
const PositionBar = ({
title,
value,
bars,
total,
compactView = false,
}: StrategyBarProps) => {
const { t } = useTranslation()
const produceData = (data: StrategyBarItem[]): HoverItem[] => {
const items: HoverItem[] = []
data.forEach((asset: StrategyBarItem) => {
if (asset.value !== 0) {
items.push({
color: asset.color || '',
name: t(`strategy.${asset.name}`) || '',
usdValue: lookup(asset.value, UST_DENOM, UST_DECIMALS),
})
}
})
return items
}
const renderBars = (bars: StrategyBarItem[]): ReactElement[] => {
const barParts: ReactElement[] = []
bars.forEach((item: StrategyBarItem, index: number) => {
barParts.push(
<div
key={index}
className={styles.fraction}
style={{
width:
item.value === 0
? '0%'
: ((item.value / total) * 100).toFixed(2) + '%',
zIndex: 50 - index,
backgroundColor: item.color,
}}
/>
)
})
return barParts
}
return (
<div
className={
compactView
? `${styles.position} ${styles.compact}`
: `${styles.position}`
}
>
<div className={styles.container}>
<div className={styles.value}>
<h4>
<span>$</span>
{formatValue(value)}
</h4>
</div>
<span className={'sub2'}>{title}</span>
</div>
{bars.length === 0 ? (
<div className={styles.bar}>
<div
className={styles.fraction}
style={{
width: '33%',
zIndex: 50 - 1,
backgroundColor: colors.transparentWhite,
}}
/>
</div>
) : (
<>
{!(bars.length === 1 && bars[0].value === 0) && (
<Tippy
className={styles.box}
content={
<CollectionHover
title={title}
data={produceData(bars)}
/>
}
>
<div className={styles.bar}>{renderBars(bars)}</div>
</Tippy>
)}
</>
)}
</div>
)
}
export default PositionBar

763
src/components/Svg.tsx Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,17 @@
@use '../styles/master' as *;
.title {
display: flex;
@include margin(8, 0);
opacity: 0.6;
h6 {
text-align: center;
}
.horizontalLine {
flex: auto;
@include devider60;
@include margin(0, 0, 3);
}
}

18
src/components/Title.tsx Normal file
View File

@ -0,0 +1,18 @@
import styles from './Title.module.scss'
interface Props {
text: string
margin?: string
}
const Title = ({ text, margin }: Props) => {
return (
<div className={styles.title}>
<div className={styles.horizontalLine} />
<h6 style={{ margin: margin || '0 40px' }}>{text}</h6>
<div className={styles.horizontalLine} />
</div>
)
}
export default Title

View File

@ -0,0 +1,22 @@
@use '../styles/master' as *;
.txFee {
@include padding(0, 1);
display: flex;
align-items: center;
justify-content: center;
opacity: 0.3;
.label {
display: flex;
align-items: center;
span {
@include margin(0, 1.5, 0, 0);
}
svg {
height: inherit;
}
}
}

24
src/components/TxFee.tsx Normal file
View File

@ -0,0 +1,24 @@
import styles from './TxFee.module.scss'
import { useTranslation } from 'react-i18next'
import { formatValue } from '../libs/parse'
interface Props {
txFee: string
styleOverride?: any
}
const TxFee = ({ txFee, styleOverride = {} }: Props) => {
const { t } = useTranslation()
return (
<div className={styles.txFee}>
<div className={`overline ${styles.label}`} style={styleOverride}>
<span>{t('common.txFee')}</span>
</div>
<div className={`overline ${styles.value}`} style={styleOverride}>
{formatValue(txFee, 2, 2, true, false, ' UST', true)}
</div>
</div>
)
}
export default TxFee

View File

@ -0,0 +1,41 @@
import { formatValue } from '../libs/parse'
import { useTranslation } from 'react-i18next'
interface Props {
gasFeeFormatted: string
taxFormatted: string
}
const TxFeeToolTip = ({ gasFeeFormatted, taxFormatted }: Props) => {
const { t } = useTranslation()
return (
<div style={{ fontSize: '0.8rem' }}>
<div style={{ display: 'flex' }}>
<span style={{ flex: 'auto', marginRight: '6px' }}>
{t('common.gas')}
</span>
<span>
{formatValue(
gasFeeFormatted,
2,
2,
true,
false,
' UST',
true
)}
</span>
</div>
<div style={{ display: 'flex' }}>
<span style={{ flex: 'auto', marginRight: '6px' }}>
{t('common.stability')}
</span>
<span>
{formatValue(taxFormatted, 2, 2, true, false, ' UST', true)}
</span>
</div>
</div>
)
}
export default TxFeeToolTip

View File

@ -0,0 +1,113 @@
@use '../../styles/master' as *;
.container {
position: relative;
@include margin(12.5, 25, 4);
width: rem-calc(230);
height: rem-calc(265);
border: 1px solid $graphAxis;
border-right: none;
border-top: none;
.scale {
@include typoXXS;
opacity: 0.6;
line-height: rem-calc(12);
position: absolute;
width: rem-calc(95);
text-align: end;
left: rem-calc(-100);
letter-spacing: rem-calc(2);
&.maxY {
top: 0;
}
&.minY {
bottom: 0;
}
}
.legend {
@include typoXXScaps;
opacity: 0.6;
position: absolute;
text-align: center;
bottom: rem-calc(-20);
&.position {
left: rem-calc(6);
width: rem-calc(120);
}
&.debt {
right: rem-calc(-10);
width: rem-calc(95);
}
}
.bar {
max-height: 100%;
width: rem-calc(36);
border-radius: $borderRadiusXS;
position: absolute;
bottom: 0;
&.supply {
left: rem-calc(30);
}
&.borrow {
left: rem-calc(75);
}
&.debt {
left: rem-calc(172);
transition: height 1s ease-out;
}
.label {
position: absolute;
width: rem-calc(34);
bottom: rem-calc(12);
left: rem-calc(2);
text-align: center;
@include typoXXScaps;
letter-spacing: 0;
}
}
.liquidation {
@include typoXXScaps;
opacity: 0.6;
position: absolute;
width: rem-calc(105);
margin-bottom: space(-3);
text-align: end;
left: rem-calc(-110);
transition: bottom 1s ease-out;
span {
display: block;
width: 100%;
word-wrap: normal;
word-break: normal;
}
}
.liquidationLine {
display: block;
width: rem-calc(110);
height: 1px;
border-bottom: 2px dashed $graphLiquidationsLine;
position: absolute;
left: 0;
transition: bottom 1s ease-out;
}
}
@media only screen and (max-width: $bpSmallHigh) {
.container {
display: none;
}
}

View File

@ -0,0 +1,112 @@
import React from 'react'
import styles from './BarGraph.module.scss'
import { formatValue } from '../../libs/parse'
interface barGraphData {
bars: number[]
labels: string[]
classNames: string[]
range: number[]
liquidation: number
legend: string[]
}
interface Props {
data: barGraphData
}
const BarGraph = ({ data }: Props) => {
const liquidationPosition = data.liquidation / (data.range[1] / 100)
const getBarHeightPercentage = (barIndex: number): number => {
return Math.floor(
(data.bars[barIndex] /
Math.max(
data.bars[0] || 0,
data.bars[1] || 0,
data.bars[2] || 0
)) *
100
)
}
return (
<div className={styles.container}>
<span className={`${styles.scale} ${styles.maxY}`}>
{formatValue(data.range[1], 2, 2, true, '$')}
</span>
<span className={`${styles.scale} ${styles.minY}`}>
{formatValue(data.range[0], 0, 0, true, '$')}
</span>
<span className={`${styles.legend} ${styles.position}`}>
{data.legend[0]}
</span>
{data.bars[2] > 0 && (
<span className={`${styles.legend} ${styles.debt}`}>
{data.legend[1]}
</span>
)}
{data.bars[0] > 0 && (
<div
className={`${styles.bar} ${styles.supply} ${data.classNames[0]}`}
style={
data.bars[0] === 0
? { opacity: 0, height: '0%' }
: { height: `${getBarHeightPercentage(0)}%` }
}
>
<span className={styles.label}>{data.labels[0]}</span>
</div>
)}
{data.bars[1] > 0 && (
<div
className={`${styles.bar} ${styles.borrow} ${data.classNames[1]}`}
style={
data.bars[1] === 0
? { opacity: 0, height: '0%' }
: { height: `${getBarHeightPercentage(1)}%` }
}
>
<span className={styles.label}>{data.labels[1]}</span>
</div>
)}
{data.bars[2] > 0 && (
<div
className={`${styles.bar} ${styles.debt} ${data.classNames[2]}`}
style={
data.bars[2] === 0
? { height: '0%' }
: { height: `${getBarHeightPercentage(2)}%` }
}
>
<span className={styles.label}>{data.labels[2]}</span>
</div>
)}
{data.liquidation > 0 && (
<>
<div
className={styles.liquidation}
style={{
bottom: `${
liquidationPosition < 13
? 13
: liquidationPosition
}%`,
}}
>
<span>Liquidation threshold</span>
</div>
<div
className={styles.liquidationLine}
style={{
bottom: `${liquidationPosition}%`,
}}
/>
</>
)}
</div>
)
}
export default BarGraph

View File

@ -0,0 +1,118 @@
@use '../../styles/master' as *;
.container {
display: flex;
align-items: center;
justify-content: center;
.title {
@include margin(0, 0, 3);
}
.progressbarContainer {
position: relative;
height: rem-calc(22);
.progressbar {
position: absolute;
height: 100%;
@include bgHatched;
box-shadow: $shadowInset;
border-radius: $borderRadiusXXXL;
}
.limitLine {
position: absolute;
height: 100%;
width: 1px;
background: $colorInfoWarning;
transition: left 2s;
box-shadow: $shadowInset;
}
.limit {
position: absolute;
height: 100%;
background: $colorGreyDark;
box-shadow: $shadowInset;
border-top-left-radius: $borderRadiusXXXL;
border-bottom-left-radius: $borderRadiusXXXL;
transition: width 2s;
}
.dot {
position: absolute;
width: rem-calc(3);
height: rem-calc(3);
background: $colorInfoWarning;
margin-top: space(-1.75);
margin-inline-start: space(-0.25);
border-radius: $borderRadiusRound;
transition: left 2s;
}
.dotGlow {
position: absolute;
width: rem-calc(7);
height: rem-calc(7);
background: $colorInfoWarning;
margin-top: space(-2.25);
margin-inline-start: space(-1.25);
border-radius: $borderRadiusXS;
@include glowM;
transition: left 2s;
}
.ltvContainer {
position: absolute;
width: 100%;
height: 100%;
.mask {
overflow-x: hidden;
height: 100%;
border-radius: $borderRadiusL;
transition: width 2s;
color: $fontColorLtv;
> span {
transition: left 2s;
z-index: 2;
}
.indicator {
height: 100%;
border-radius: inherit;
@include bgLimit;
}
.glow {
position: absolute;
height: 100%;
border-radius: inherit;
@include bgLimit;
opacity: 0.4;
transition: width 2s;
@include glowXXL;
@include margin(-5.5, 0, 0);
}
}
}
}
.values {
display: flex;
opacity: 0.6;
margin-top: space(1.25);
.zero {
text-align: start;
flex: auto;
}
.limit {
flex: 0;
white-space: nowrap;
}
}
}

View File

@ -0,0 +1,172 @@
import { formatValue, lookup } from '../../libs/parse'
import styles from './BorrowLimit.module.scss'
import { addDecimals } from '../../libs/math'
import { UST_DECIMALS, UST_DENOM } from '../../constants/appConstants'
interface Props {
width: string
ltv: number
maxLtv: number
liquidationThreshold: number
barHeight: string
showPercentageText: boolean
showTitleText: boolean
showLegend?: boolean
top?: number
percentageThreshold?: number
percentageOffset?: number
title?: string
mode?: string
criticalIndicator?: number
}
const BorrowLimit = ({
width,
ltv,
maxLtv,
liquidationThreshold,
barHeight = '22px',
showPercentageText = true,
showLegend = true,
top = 4,
showTitleText = true,
percentageThreshold = 15,
percentageOffset = 45,
title = 'Borrowing Capacity',
mode = 'default',
criticalIndicator,
}: Props) => {
const ltvPercent =
+(((ltv || 0) / (liquidationThreshold || 0)) * 100).toFixed(2) || 0
const ltvPercentRounded = +(Math.round(ltvPercent * 100) / 100).toFixed(1)
const ltvPercentRestrained =
ltvPercent > 100 ? 100 : ltvPercent < 0 ? 0 : ltvPercent
const ltvPercentMargin =
ltvPercent > percentageThreshold
? `-${percentageOffset + 15}px`
: '10px'
const maxBorrowPercent =
criticalIndicator || (maxLtv / liquidationThreshold) * 100
return (
<div className={styles.container}>
<div style={{ width: width }}>
{showTitleText ? (
<div className={`overline ${styles.title}`}>{title}</div>
) : null}
<div
style={{ height: barHeight }}
className={styles.progressbarContainer}
>
<div
style={{ width: width }}
className={styles.progressbar}
>
<div
style={{ left: `${maxBorrowPercent}%` }}
className={styles.limitLine}
/>
<div
style={{
width: `${maxBorrowPercent}%`,
maxWidth: width,
}}
className={styles.limit}
/>
<div
style={{ left: `${maxBorrowPercent}%` }}
className={styles.dot}
/>
<div
style={{ left: `${maxBorrowPercent}%` }}
className={styles.dotGlow}
/>
<div className={styles.ltvContainer}>
<div
style={{
width: `${ltvPercentRestrained}%`,
maxWidth: width,
}}
className={styles.mask}
>
{showPercentageText ? (
<span
style={{
position: 'absolute',
left: `${ltvPercentRestrained}%`,
top: `${top}px`,
marginLeft: ltvPercentMargin,
width: `${percentageOffset + 8}px`,
textAlign:
ltvPercent > percentageThreshold
? 'right'
: 'left',
}}
className='overline'
>
{ltv < 0 ? (
'0%'
) : (
<>
{`${
mode === 'default'
? ltvPercentRounded
: addDecimals(ltv)
}%`}
</>
)}
</span>
) : null}
<div
style={{ width: width }}
className={styles.indicator}
/>
<div
style={{
width: `${ltvPercentRestrained}%`,
maxWidth: width,
}}
className={styles.glow}
/>
</div>
</div>
</div>
</div>
{showLegend && (
<div className={`overline ${styles.values}`}>
<div className={styles.zero}>
{mode === 'default' ? '$0' : '0%'}
</div>
<div className={styles.limit}>
{mode === 'default' ? (
<span>
{formatValue(
lookup(maxLtv, UST_DENOM, UST_DECIMALS),
2,
2,
true,
'$'
)}
</span>
) : (
formatValue(
liquidationThreshold,
0,
0,
true,
false,
'%'
)
)}
</div>
</div>
)}
</div>
</div>
)
}
export default BorrowLimit

View File

@ -0,0 +1,5 @@
@use '../../styles/master' as *;
.container {
@include layoutTile;
}

View File

@ -0,0 +1,17 @@
import { ReactNode } from 'react'
import styles from './Card.module.scss'
interface Props {
children?: ReactNode
styleOverride?: object
}
const Card = ({ children, styleOverride }: Props) => {
return (
<div style={styleOverride} className={styles.container}>
{children}
</div>
)
}
export default Card

View File

@ -0,0 +1,48 @@
@use '../../styles/master' as *;
.container {
position: relative;
margin-inline-start: 0;
margin-inline-end: 0;
flex: auto;
display: flex;
flex-direction: column;
align-items: center;
z-index: 1;
height: 100%;
.innerText {
position: absolute;
top: 47.17%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
.title {
position: absolute;
top: 58.66%;
left: 50%;
transform: translate(-50%, -50%);
max-width: rem-calc(110);
}
}
.overlay {
display: block;
width: 100%;
height: 100%;
z-index: 2;
position: absolute;
left: 0;
top: 0;
&:hover {
cursor: pointer;
}
}
@media only screen and (max-width: $bpMediumHigh) and (min-width: $bpMediumLow) {
.container {
@include padding(3, 0, 0);
}
}

View File

@ -0,0 +1,266 @@
import React, { useEffect, useMemo, useState } from 'react'
import { Doughnut, defaults } from 'react-chartjs-2'
import styles from './DonutGraph.module.scss'
import DonutHover from './charts/DonutHover'
import { producePercentData } from '../../libs/assetInfo'
import Tippy from '@tippyjs/react'
import { formatValue } from '../../libs/parse'
defaults.global.defaultFontFamily = 'Inter'
function hexToRgb(hex: any) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
}
: null
}
// Create a gradient to use on our chart. This is essentially a circular border around the slice(s)
function createGrad(
ctx: any,
canvasWidth: number,
canvasHeight: number,
colorOne: string,
colorTwo = '#1b1d24',
opacityTwo = 0.35
) {
let opacityOne = 1,
colorStop = 92
var rgbOne = hexToRgb(colorOne)
var rgbTwo = hexToRgb(colorTwo)
colorStop = colorStop / 100
var grd = ctx
.getContext('2d')
.createRadialGradient(
canvasWidth / 2,
canvasHeight / 2,
0.0,
canvasWidth / 2,
canvasHeight / 2,
canvasHeight / 2
)
if (rgbOne == null || rgbTwo == null) {
return null
}
// Add color stops to the gradient
grd.addColorStop(
0.0,
'rgba(' +
rgbOne.r +
', ' +
rgbOne.g +
', ' +
rgbOne.b +
', ' +
opacityOne +
')'
)
grd.addColorStop(
colorStop,
'rgba(' +
rgbOne.r +
', ' +
rgbOne.g +
', ' +
rgbOne.b +
', ' +
opacityOne +
')'
)
grd.addColorStop(
0.94,
'rgba(' +
rgbTwo.r +
', ' +
rgbTwo.g +
', ' +
rgbTwo.b +
', ' +
opacityTwo +
')'
) // required to prevent jagged edges
grd.addColorStop(
1.0,
'rgba(' +
rgbTwo.r +
', ' +
rgbTwo.g +
', ' +
rgbTwo.b +
', ' +
opacityTwo +
')'
)
return grd
}
function generateColors(mychart: any, defaultColors: any[]) {
if (mychart == null) {
return defaultColors
}
return defaultColors.map((color) =>
createGrad(
mychart.chartInstance.ctx.canvas,
mychart.chartInstance.width,
mychart.chartInstance.height,
color,
'#1b1d24',
0.0
)
)
}
function generateHoverColors(mychart: any, defaultColors: any[]) {
if (mychart == null) {
return defaultColors
}
return defaultColors.map((color) =>
createGrad(
mychart.chartInstance.ctx.canvas,
mychart.chartInstance.width,
mychart.chartInstance.height,
color,
color
)
)
}
interface Props {
title: string
placeholderTitle?: string
value: number
cutoutPercentage: number
labels: string[]
data: AssetInfo[]
colors: string[]
expandableOnHover?: boolean
labelsOnHover?: boolean
styleOverride: Object
}
const DonutGraph = ({
title,
value,
cutoutPercentage,
labels,
data,
colors,
expandableOnHover = true,
labelsOnHover = true,
styleOverride,
}: Props) => {
const [showToolTip, setShowToolTip] = useState(false)
const [myChart, setMyChart] = useState()
const _chartRef: any = React.createRef()
useEffect(() => {
setMyChart(_chartRef.current)
}, [_chartRef])
const colorsToAdd = generateColors(myChart, colors)
const hoverColors = generateHoverColors(myChart, colors)
const percentData = useMemo(() => producePercentData(data), [data])
// Set up our dataset based on whether we require expandable sections on hover
const chartHasData = useMemo(() => data.some((d) => !!d), [data])
const datasetToAdd = chartHasData
? [
{
data: percentData,
backgroundColor: expandableOnHover ? colorsToAdd : colors,
hoverBackgroundColor: expandableOnHover
? hoverColors
: colors,
borderWidth: 0,
},
]
: [
{
data: [100],
backgroundColor: expandableOnHover
? generateColors(myChart, ['#3a3c49'])
: ['#3a3c49'],
hoverBackgroundColor: expandableOnHover
? generateHoverColors(myChart, ['#3a3c49'])
: ['#3a3c49'],
borderWidth: 0,
},
]
const chartData = {
labels: labels,
datasets: datasetToAdd,
}
const chartOptions = {
tooltips: {
enabled: false,
custom: (tooltipModel: any) => {
if (!chartHasData || !labelsOnHover) return
// if chart is not defined, return early
var chart: any = _chartRef.current
if (!chart) {
return
}
// hide the tooltip when chartjs determines you've hovered out
if (tooltipModel.opacity <= 0) {
setShowToolTip(false)
return
}
setShowToolTip(true)
},
},
legend: {
display: false,
},
cutoutPercentage: cutoutPercentage,
maintainAspectRatio: true,
circumference: Math.PI * 2,
rotation: 0,
}
return (
<div className={styles.container}>
{showToolTip && expandableOnHover && (
<Tippy content={<DonutHover title={title} data={data} />}>
<div className={styles.overlay} />
</Tippy>
)}
<div style={styleOverride}>
<Doughnut
data={chartData}
options={chartOptions}
ref={_chartRef}
/>
<div className={styles.innerText}>
{
<div>
<span>$</span>
<span className={'h4'}>
{formatValue(Number(value), 2, 2, true, false)}
</span>
</div>
}
</div>
<div className={styles.title}>
<p className={'sub2'}>{title}</p>
</div>
</div>
</div>
)
}
export default DonutGraph

View File

@ -0,0 +1,5 @@
@use '../../../styles/master' as *;
.container {
position: fixed;
}

View File

@ -0,0 +1,38 @@
import { UST_DENOM, UST_DECIMALS } from '../../../constants/appConstants'
import { lookup } from '../../../libs/parse'
import CollectionHover, { HoverItem } from '../../CollectionHover'
interface Props {
data: AssetInfo[]
title: string
}
const DonutHover = ({ data, title }: Props) => {
const produceData = (data: AssetInfo[]): HoverItem[] => {
const items: HoverItem[] = []
data.forEach((asset: AssetInfo) => {
if (Number(asset.uusdBalance) > 0) {
items.push({
color: asset.color || '',
name: asset.symbol || '',
amount: lookup(
Number(asset.balance),
asset.denom,
asset.decimals
),
usdValue: lookup(
asset.uusdBalance || 0,
UST_DENOM,
UST_DECIMALS
),
})
}
})
return items
}
return <CollectionHover title={title} data={produceData(data)} />
}
export default DonutHover

View File

@ -0,0 +1,17 @@
interface Props {
symbol: string
name: string
}
const Asset = ({ symbol, name }: Props) => {
return (
<div>
<div>{symbol}</div>
<div style={{ opacity: 0.6 }} className='caption'>
{name}
</div>
</div>
)
}
export default Asset

View File

@ -0,0 +1,47 @@
import { UST_DECIMALS, UST_DENOM } from '../../constants/appConstants'
import { formatValue, lookup } from '../../libs/parse'
interface Props {
denom: string
decimals: number
amount: number
uusdAmount: number
}
const CellAmount = ({ denom, decimals, amount, uusdAmount }: Props) => {
const usdAmount = lookup(uusdAmount, UST_DENOM, UST_DECIMALS)
const assetAmount = lookup(amount, denom, decimals)
return (
<div>
{denom !== 'uusd'
? formatValue(
assetAmount > 0 && assetAmount < 0.01
? 0.01
: assetAmount,
2,
2,
true,
assetAmount > 0 && assetAmount < 0.01 ? '< ' : false
)
: null}
<div
style={{
opacity: denom !== 'uusd' ? 0.6 : 1,
}}
className={denom !== 'uusd' ? 'caption' : ''}
>
<span>
{formatValue(
usdAmount > 0 && usdAmount < 0.01 ? 0.01 : usdAmount,
2,
2,
true,
usdAmount > 0 && usdAmount < 0.01 ? '< $' : '$'
)}
</span>
</div>
</div>
)
}
export default CellAmount

View File

@ -0,0 +1,232 @@
import { useTable, useSortBy } from 'react-table'
import { useEffect } from 'react'
import colors from '../../styles/_assets.module.scss'
const Grid = ({
columns,
data,
initialState,
hideheader = false,
updateSort,
}) => {
const {
columns: instanceColumns,
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
toggleSortBy,
} = useTable(
{
columns,
data,
initialState,
autoResetSortBy: false,
disableSortRemove: true,
disableMultiSort: true,
sortDescFirst: true,
},
useSortBy
)
useEffect(
() => {
instanceColumns.forEach((column) => {
if (column.isSorted) {
if (
initialState.sortBy[0].id !== column.id ||
initialState.sortBy[0].desc !== column.isSortedDesc
)
updateSort({
sortBy: [
{
id: column.id,
desc: column.isSortedDesc,
},
],
})
return false
}
})
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[rows]
)
useEffect(
() => {
instanceColumns.forEach((column) => {
let cleared = false
if (column.isSorted) {
if (
initialState.sortBy[0].id !== column.id ||
initialState.sortBy[0].desc !== column.isSortedDesc
) {
cleared = true
column.clearSortBy()
}
}
if (cleared) {
toggleSortBy(
initialState.sortBy[0].id,
initialState.sortBy[0].desc,
false
)
}
})
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[initialState]
)
return (
<table
{...getTableProps()}
style={{
tableLayout: 'fixed',
borderSpacing: '0px',
width: '100%',
}}
>
<thead>
<tr>
{headerGroups[0].headers.map((column) => {
return (
<th
{...column.getHeaderProps(
column.getSortByToggleProps()
)}
style={{
borderTop: hideheader
? 'none'
: `1px solid ${colors.tableBorder}`,
borderBottom: `1px solid ${colors.tableBorder}`,
cursor: column.disableSortBy
? 'default'
: 'pointer',
width: column.width,
color: colors.tableHeader, // Overwrite the .caption color to be 50% transparent
}}
className='caption'
>
{column.hideHeader || hideheader ? null : (
<div
style={{
padding:
column.headingPaddingOverride
? column.headingPaddingOverride
: '8px',
}}
>
<span
style={{
display: 'flex',
whiteSpace:
column.whiteSpace ||
'nowrap',
textAlign:
column.textAlign ||
'inherit',
justifyContent:
column.textAlign === 'right'
? 'flex-end'
: '',
}}
>
{column.textAlign !== 'right' &&
column.render('Header')}
{!column.disableSortBy ? (
<div
style={{
display: 'flex',
flexDirection: 'column',
margin:
column.textAlign ===
'right'
? 'auto 7px auto 0'
: 'auto 0 auto 7px',
}}
>
<span
style={{
width: 0,
height: 0,
borderLeft:
'4px solid transparent',
borderRight:
'4px solid transparent',
borderBottom:
!column.disableSortBy
? !column.isSortedDesc &&
column.isSorted
? `4px solid ${colors.tableSortActive}`
: `4px solid ${colors.tableSort}`
: 'none',
}}
/>
<span
style={{
marginTop: '3px',
width: 0,
height: 0,
borderLeft:
'4px solid transparent',
borderRight:
'4px solid transparent',
borderTop:
!column.disableSortBy
? column.isSortedDesc &&
column.isSorted
? `4px solid ${colors.tableSortActive}`
: `4px solid ${colors.tableSort}`
: 'none',
}}
/>
</div>
) : null}
{column.textAlign === 'right' &&
column.render('Header')}
</span>
</div>
)}
</th>
)
})}
</tr>
</thead>
<tbody {...getTableBodyProps()}>
{rows.map((row) => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map((cell) => {
return (
<td
{...cell.getCellProps()}
style={{
textAlign: cell.column.textAlign,
height: '53px',
width: cell.column.width,
padding: cell.column.paddingOverride
? cell.column.paddingOverride
: '0 8px',
borderBottom: `1px solid ${colors.tableBorder}`,
overflow: cell.column.showOverflow
? 'visable'
: 'hidden',
}}
className='body2'
>
{cell.render('Cell')}
</td>
)
})}
</tr>
)
})}
</tbody>
</table>
)
}
export default Grid

View File

@ -0,0 +1,191 @@
import OverflowDropdown from '../../components/grid/dropdown/OverflowDropdown'
import useActionButtonClickHandler from '../../hooks/useActionButtonClickHandler'
import { useRedBank, useAccountBalance } from '../../hooks'
import { useTranslation } from 'react-i18next'
import { DropdownItemProps } from '../../types/components'
import { useHistory, useLocation } from 'react-router'
import { ASTROPORT_URL } from '../../constants/appConstants'
import useStore from '../../store'
import Button from '../Button'
import { ExternalSVG } from '../Svg'
import { getRoute } from '../../libs/parse'
import { ActionType } from '../../types/enums'
export enum GridActionType {
BorrowAction,
DepositAction,
WithdrawAction,
ManageAction,
RepayAction,
None,
}
interface Props {
denom: string
menuItems: DropdownItemProps[]
actionType: GridActionType
strategy?: StrategyObject
disabled?: boolean
}
const GridActions = ({
denom,
menuItems,
actionType,
strategy,
disabled = false,
}: Props) => {
const { t } = useTranslation()
const newMenuItems = [...menuItems]
const mainButton = newMenuItems[0]
const whitelistedAssets = useStore((s) => s.whitelistedAssets)
const { find, findDeposit } = useAccountBalance()
const { findLiquidity, findMarketInfo } = useRedBank()
const provideClickHandler = useActionButtonClickHandler()
const history = useHistory()
const location = useLocation()
const swapUrl =
denom === 'uusd'
? `${ASTROPORT_URL}?from=uluna&to=uusd`
: denom === 'uluna'
? `${ASTROPORT_URL}?from=uusd&to=uluna`
: `${ASTROPORT_URL}?from=uusd&to=${
whitelistedAssets?.find((asset) => asset.denom === denom)
?.contract_addr
}`
const marketInfo = findMarketInfo(denom)
const buyClickHandler = provideClickHandler(
ActionType.ExternalLink,
denom,
GridActionType.None,
swapUrl
)
let dropdownMenuItems = newMenuItems.slice(1, menuItems.length)
let useBuyButton = false
if (actionType === GridActionType.DepositAction) {
const assetWallet = find(denom)
if (Number(assetWallet?.amount || 0) <= 0) useBuyButton = true
}
let disabledDepositButton: boolean = false
// If the market has had deposits disabled then disable deposit button
if (
actionType === GridActionType.DepositAction &&
!marketInfo?.deposit_enabled
) {
disabledDepositButton = true
}
// Remove deposit from submenu if it's disabled (applicable to My Station's grid)
if (
actionType === GridActionType.WithdrawAction &&
!marketInfo?.deposit_enabled
) {
dropdownMenuItems = dropdownMenuItems.filter(
(item) => item.gridAction !== GridActionType.DepositAction
)
}
let disableBorrowButton: boolean = false
if (actionType === GridActionType.BorrowAction) {
// If the market has had borrows disabled then disable borrow button
if (!marketInfo?.borrow_enabled) {
disableBorrowButton = true
}
// If the user has no collateral then disable borrow button
if (!disableBorrowButton && whitelistedAssets?.length) {
let depositSum: number = 0
whitelistedAssets.forEach((asset) => {
depositSum += Number(findDeposit(asset.denom)?.amount || 0)
})
if (depositSum <= 0) disableBorrowButton = true
}
// if the market has no liquidity available then disable borrow button
if (!disableBorrowButton) {
const assetLiquidity = findLiquidity(denom)
if (Number(assetLiquidity?.amount || 0) <= 0)
disableBorrowButton = true
}
}
// Remove borrow from submenu if it's disabled (applicable to My Station's grid)
if (
actionType === GridActionType.RepayAction &&
!marketInfo?.borrow_enabled
) {
dropdownMenuItems = dropdownMenuItems.filter(
(item) => item.gridAction !== GridActionType.BorrowAction
)
}
// Disabled prop is used to disable all actions of a market on the UI, i.e. on the not connected screens where we show a dummie grid
// The !marketInfo?.active flag determines if all actions against the market has been disabled on the SC level
const isAllActionsDisabled: boolean = disabled || !marketInfo?.active
const isDropdownDisabled: boolean =
useBuyButton &&
dropdownMenuItems.length === 1 &&
dropdownMenuItems[0].title === 'Swap'
const isPrimaryActionDisabled: boolean =
disabledDepositButton || disableBorrowButton
return (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
}}
>
{useBuyButton ? (
<Button
text={t('common.buy')}
suffix={<ExternalSVG />}
color='secondary'
onClick={buyClickHandler}
disabled={disabled}
/>
) : (
<>
{strategy ? (
<Button
text={mainButton?.title || ''}
onClick={() => {
history.push(`/fields/${actionType}`)
}}
disabled={disabled}
/>
) : (
<Button
text={mainButton?.title || ''}
disabled={
isAllActionsDisabled || isPrimaryActionDisabled
}
onClick={() => {
history.push(
`${location.pathname}/${getRoute(
actionType
)}/${denom}`
)
}}
/>
)}
</>
)}
<OverflowDropdown
menuItems={dropdownMenuItems}
denom={denom}
strategy={strategy}
disabled={isAllActionsDisabled || isDropdownDisabled}
/>
</div>
)
}
export default GridActions

View File

@ -0,0 +1,13 @@
@use '../../../styles/master' as *;
.container {
@include padding(1, 4);
display: flex;
flex: auto;
align-items: center;
&:hover {
background-color: $alphaWhite10;
cursor: pointer;
}
}

View File

@ -0,0 +1,38 @@
import { useHistory } from 'react-router'
import useActionButtonClickHandler from '../../../hooks/useActionButtonClickHandler'
import { DropdownItemProps } from '../../../types/components'
import styles from './DropdownItem.module.scss'
const DropdownItem = ({
icon,
title,
denom,
actionType,
gridAction,
url,
close,
strategy,
}: DropdownItemProps) => {
const provideClickHandler = useActionButtonClickHandler()
const history = useHistory()
const clickHandler = strategy
? () => {
history.push(`/fields/strategy/${strategy.key}`)
}
: provideClickHandler(actionType, denom, gridAction, url)
return (
<div
className={styles.container}
onClick={() => {
clickHandler()
close()
}}
>
{icon}
<span className='body2'>{title}</span>
</div>
)
}
export default DropdownItem

View File

@ -0,0 +1,9 @@
@use '../../../styles/master' as *;
.container {
@include layoutTooltip;
}
.buttonContainer {
margin-inline-start: space(1);
}

View File

@ -0,0 +1,88 @@
import { useState } from 'react'
import Tippy, { TippyProps } from '@tippyjs/react'
import DropdownItem from './DropdownItem'
import MoreHorizIcon from '@material-ui/icons/MoreHoriz'
import styles from './OverflowDropdown.module.scss'
import { DropdownItemProps } from '../../../types/components'
import Button from '../../Button'
export const DefaultTippyProps: TippyProps = {
animation: false,
interactive: true,
appendTo: document.body,
}
export const DropdownTippyProps: TippyProps = {
...DefaultTippyProps,
placement: 'bottom-end',
}
interface OverflowDropdownProps {
menuItems: DropdownItemProps[]
denom: string
strategy?: StrategyObject
disabled?: boolean
}
const OverflowDropdown = ({
menuItems,
denom,
strategy,
disabled = false,
}: OverflowDropdownProps) => {
const [visible, setVisible] = useState(false)
const show = () => setVisible(true)
const hide = () => setVisible(false)
const renderDropdown = () => (
<div className={styles.container}>
{menuItems.map((menuItem, index) => (
<DropdownItem
key={index}
icon={menuItem.icon}
title={menuItem.title}
denom={denom}
actionType={menuItem.actionType}
gridAction={menuItem.gridAction}
url={menuItem.url}
close={hide}
strategy={strategy}
/>
))}
</div>
)
if (menuItems.length) {
return (
<div>
<Tippy
{...DropdownTippyProps}
render={renderDropdown}
visible={visible}
onClickOutside={hide}
>
<div className={styles.buttonContainer}>
<Button
color='tertiary'
variant='round'
prefix={
<MoreHorizIcon
style={{
width: '1.2rem',
height: '1.2rem',
}}
/>
}
onClick={visible ? hide : show}
disabled={disabled}
/>
</div>
</Tippy>
</div>
)
} else {
return null
}
}
export default OverflowDropdown

View File

@ -0,0 +1,156 @@
@use '../../styles/master' as *;
.button,
.disabledButton,
.secondary {
display: flex;
flex: 1;
justify-content: center;
align-items: center;
align-content: center;
flex-wrap: nowrap;
border-radius: $borderRadiusXXL;
height: rem-calc(31);
@include padding(0.5, 4, 0);
@include typoButton;
cursor: pointer;
color: $colorWhite;
border: 1px solid $alphaWhite60;
outline: none;
background: $alphaBlack10;
text-transform: uppercase;
> span {
@include margin(0, 0, 0, 2);
}
&:hover {
background: $alphaWhite60;
text-decoration: none;
}
&:active {
outline: none;
}
svg {
@include margin(-0.5, 0, 0);
height: rem-calc(16);
width: auto;
}
}
.disabledButton:hover {
background: $alphaWhite30;
}
.secondary {
background: $colorSecondaryHighlight;
border: none;
@include padding(0.5, 5.5);
height: rem-calc(36);
&:hover {
background: $colorSecondary;
}
}
.wrapper {
position: relative;
}
.dropdown {
display: flex;
position: absolute;
flex-direction: column;
justify-content: flex-start;
top: rem-calc(38);
@include padding(6, 6, 5.5);
@include layoutPopover;
width: rem-calc(360);
max-width: 100vw;
left: 50%;
transform: translateX(-50%);
z-index: 100;
}
.option {
background-color: transparent;
border-radius: 0;
color: $colorAccent;
border: none;
appearance: none;
@include typoL;
@include padding(2, 0);
font-weight: $fontWeightRegular;
text-align: start;
width: 100%;
outline: none;
display: flex;
align-items: center;
flex: 0 0 100%;
text-decoration: none;
.image {
opacity: 0.6;
display: flex;
align-items: center;
}
&:hover {
cursor: pointer;
text-decoration: none;
color: $colorSecondaryHighlight;
.image {
opacity: 1;
}
}
}
.image {
@include margin(0, 2, 0, 0);
svg,
img {
width: rem-calc(24);
height: auto;
}
}
.tooltip {
@include margin(-1.5, 0, 0, 1);
svg {
fill: $colorAccent;
opacity: 0.5;
align-self: flex-start;
&:hover {
opacity: 1;
}
}
}
@media only screen and (max-width: $bpMediumHigh) {
.dropdown {
&:not(.centered) {
right: 0;
transform: none;
left: unset;
}
}
.secondary {
+ .dropdown {
right: unset;
left: 50%;
transform: translateX(-50%);
}
}
}
@media only screen and (max-width: $bpSmallHigh) {
.dropdown {
max-width: calc(100vw - 24px);
}
}

View File

@ -0,0 +1,146 @@
import { ReactNode, useCallback, useState } from 'react'
import styles from './ConnectButton.module.scss'
import { WalletSVG } from '../Svg'
import { ClickAwayListener } from '@material-ui/core'
import { ConnectType, useWallet } from '@terra-money/wallet-provider'
import * as rdd from 'react-device-detect'
import { useTranslation } from 'react-i18next'
interface Props {
textOverride?: string | ReactNode
disabled?: boolean
color?: string
centered?: boolean
}
const ConnectButton = ({
textOverride,
disabled = false,
color,
centered = false,
}: Props) => {
const { t } = useTranslation()
const [openConnectList, setOpenConnectList] = useState(false)
const { connect, availableConnections, availableInstallations } =
useWallet()
const onClickAway = useCallback(() => {
setOpenConnectList(false)
}, [])
return (
<div className={styles.wrapper}>
<button
className={
disabled
? styles.disabledButton
: color
? styles[color]
: styles.button
}
onClick={() => {
rdd.isMobile
? connect(ConnectType.WALLETCONNECT)
: setOpenConnectList(!openConnectList)
}}
>
<WalletSVG />
<span className='overline'>
{textOverride || t('common.connectWallet')}
</span>
</button>
{openConnectList && (
<ClickAwayListener onClickAway={onClickAway}>
<div
className={
centered
? `${styles.dropdown} ${styles.centered}`
: styles.dropdown
}
>
{!rdd.isMobile && (
<>
{availableConnections
.filter(
({ type }) =>
type !== ConnectType.READONLY
)
.map(({ type, name, identifier }) => {
if (
(type === ConnectType.EXTENSION &&
identifier === 'station') ||
type === ConnectType.WALLETCONNECT
) {
return (
<button
key={
'connection' +
type +
identifier
}
className={styles.option}
onClick={() => {
connect(
type,
identifier
)
setOpenConnectList(
false
)
}}
>
{name}
</button>
)
} else {
return null
}
})}
{availableInstallations
.filter(
({ type }) =>
type === ConnectType.EXTENSION
)
.map(({ type, identifier, name, url }) => {
if (
type === ConnectType.EXTENSION &&
identifier === 'station'
) {
return (
<a
key={
'installation' +
type +
identifier
}
className={styles.option}
href={url}
target='_blank'
rel='noreferrer'
onClick={() => {
setOpenConnectList(
false
)
}}
>
{t('common.installName', {
name: name,
})}
</a>
)
} else {
return null
}
})}
</>
)}
</div>
</ClickAwayListener>
)}
</div>
)
}
export default ConnectButton

View File

@ -0,0 +1,227 @@
@use '../../styles/master' as *;
.button {
display: flex;
flex: 1;
justify-content: center;
align-items: center;
align-content: center;
flex-wrap: nowrap;
border-radius: $borderRadiusXXL;
height: rem-calc(31);
@include padding(0.5, 2, 0, 3);
@include typoS;
cursor: pointer;
color: $colorWhite;
border: 1px solid $alphaWhite60;
outline: none;
background: $alphaBlack10;
.walletIcon {
display: grid;
place-content: center;
svg {
margin-top: space(-1);
height: rem-calc(16);
width: auto;
}
}
.address {
margin-inline-start: space(1.5);
font-weight: $fontWeightRegular;
}
.balance {
font-weight: $fontWeightRegular;
position: relative;
display: flex;
align-items: center;
height: 100%;
margin-inline-start: space(2);
padding-inline-start: rem-calc(8);
&:before {
content: '';
position: absolute;
top: 1px;
bottom: 1px;
left: 0;
border-left: 1px solid $alphaWhite60;
}
}
&:hover {
border: 1px solid $colorWhite;
background-color: $alphaWhite10;
}
.circularProgress {
margin-inline-end: space(2);
}
}
.wrapper {
position: relative;
}
.details {
display: flex;
position: absolute;
flex-direction: column;
justify-content: flex-start;
top: rem-calc(38);
@include padding(6, 6, 5.5);
@include layoutPopover;
width: rem-calc(420);
left: rem-calc(-233);
z-index: 100;
}
.detailsHeader {
display: flex;
flex: 0;
flex-wrap: nowrap;
width: 100%;
@include margin(0, 0, 4);
}
.detailsBalance {
display: flex;
flex: 1;
width: auto;
align-items: center;
svg {
@include margin(-1, 4, 0, 0);
height: space(6);
width: auto;
}
p {
@include margin(0);
@include typoH4;
color: $colorSecondaryDark;
}
}
.detailsButton {
display: flex;
flex: 0 0 rem-calc(116);
width: rem-calc(116);
}
.detailsBody {
flex: 0;
width: 100%;
.address,
.addressMobile,
.addressLabel,
.tnsAddress {
color: $colorSecondaryDark;
opacity: 1;
@include margin(0, 0, 1);
@include typoS;
word-break: break-all;
}
.addressMobile {
display: none;
}
svg {
height: rem-calc(16);
width: auto;
@include margin(-1, 1, 0, 0);
}
.buttons {
display: flex;
flex: 0 0 100%;
flex-wrap: wrap;
@include padding(1, 0, 0);
> button {
font-weight: $fontWeightRegular;
&:first-child {
width: rem-calc(100);
}
}
}
button {
display: flex;
flex: 0 0 auto;
width: auto;
align-items: center;
color: $colorSecondaryDark;
background: transparent;
border: none;
@include padding(2, 0);
&:hover {
cursor: pointer;
}
}
}
.network {
background-color: $colorSecondaryHighlight;
text-transform: uppercase;
border-radius: $borderRadiusL;
@include padding(0, 2);
@include margin(0);
@include typoNetwork;
position: absolute;
top: space(-4);
right: space(-4);
cursor: default;
z-index: 1;
}
@media only screen and (max-width: $bpMediumHigh) {
.details {
right: 0;
transform: none;
left: unset;
}
.detailsButton {
@include margin(4, 0, 0);
flex: 1 0 100%;
width: 100%;
> div {
width: 100%;
button {
width: 100%;
}
}
}
.detailsHeader {
flex-wrap: wrap;
}
.network {
right: rem-calc(-12);
}
}
@media only screen and (max-width: $bpSmallHigh) {
.details {
max-width: calc(100vw - 24px);
}
.detailsBody {
.addressMobile {
display: block;
}
.address {
display: none;
}
}
}

View File

@ -0,0 +1,150 @@
import { useAccountBalance, useTNS } from '../../hooks'
import { truncate } from '../../libs/text'
import styles from './ConnectedButton.module.scss'
import { formatValue, lookup } from '../../libs/parse'
import { useCallback, useState } from 'react'
import Button from '../Button'
import useClipboard from 'react-use-clipboard'
import { CheckSVG, CopySVG, ExternalSVG, WalletSVG } from '../Svg'
import colors from '../../styles/_assets.module.scss'
import { CircularProgress, ClickAwayListener } from '@material-ui/core'
import { useWallet } from '@terra-money/wallet-provider'
import { useTranslation } from 'react-i18next'
import {
FINDER_URL,
UST_DECIMALS,
UST_DENOM,
} from '../../constants/appConstants'
import { State } from '../../types/enums'
import useStore from '../../store'
interface Props {
address: string
}
const ConnectedButton = ({ address }: Props) => {
const { find, state } = useAccountBalance()
const chainID = useStore((s) => s.networkConfig?.chainID)
const name = useStore((s) => s.networkConfig?.name)
const { disconnect } = useWallet()
const uusdAmount = Number(find(UST_DENOM)?.amount || 0)
const [showDetails, setShowDetails] = useState(false)
const [isCopied, setCopied] = useClipboard(address, {
successDuration: 1000 * 5,
})
const viewOnFinder = useCallback(() => {
window.open(`${FINDER_URL}/${chainID}/account/${address}`, '_blank')
}, [chainID, address])
const onClickAway = useCallback(() => {
setShowDetails(false)
}, [])
const { t } = useTranslation()
const walletAddress = useTNS(address, true)
const tnsAddress = useTNS(address, false)
return (
<div className={styles.wrapper}>
{name !== 'mainnet' && (
<span className={styles.network}>{name}</span>
)}
<button
className={styles.button}
onClick={() => {
setShowDetails(!showDetails)
}}
>
<span className={styles.walletIcon}>
<WalletSVG className={styles.walletIcon} />
</span>
<span className={styles.address}>
{walletAddress ? walletAddress : truncate(address, [2, 4])}
</span>
<div className={styles.balance}>
{state === State.READY ? (
`${formatValue(
lookup(uusdAmount, UST_DENOM, UST_DECIMALS)
)}`
) : (
<CircularProgress
color='inherit'
size={'0.9rem'}
className={styles.circularProgress}
/>
)}{' '}
UST
</div>
</button>
{showDetails && (
<ClickAwayListener onClickAway={onClickAway}>
<div className={styles.details}>
<div className={styles.detailsHeader}>
<div className={styles.detailsBalance}>
<WalletSVG color={colors.secondaryDark} />
<p>
{formatValue(
lookup(
uusdAmount,
UST_DENOM,
UST_DECIMALS
),
0
)}{' '}
UST
</p>
</div>
<div className={styles.detailsButton}>
<Button
text={t('common.disconnect')}
color='secondary'
onClick={disconnect}
/>
</div>
</div>
<div className={styles.detailsBody}>
<p className={`sub2 ${styles.addressLabel}`}>
{tnsAddress
? tnsAddress
: t('common.yourAddress')}
</p>
<p className={styles.address}>{address}</p>
<p className={styles.addressMobile}>
{truncate(address, [14, 14])}
</p>
<div className={styles.buttons}>
<button
className={styles.copy}
onClick={setCopied}
>
<CopySVG color={colors.secondaryDark} />
{isCopied ? (
<>
{t('common.copied')}{' '}
<CheckSVG
color={colors.secondaryDark}
/>
</>
) : (
<>{t('common.copy')}</>
)}
</button>
<button
className={styles.external}
onClick={viewOnFinder}
>
<ExternalSVG color={colors.secondaryDark} />{' '}
{t('common.viewOnFinder')}
</button>
</div>
</div>
</div>
</ClickAwayListener>
)}
</div>
)
}
export default ConnectedButton

View File

@ -0,0 +1,247 @@
@use '../../styles/master' as *;
.button {
display: flex;
flex: 1;
justify-content: center;
align-items: center;
align-content: center;
flex-wrap: nowrap;
border-radius: $borderRadiusXXL;
height: rem-calc(31);
@include padding(0.5, 3, 0);
@include margin(0, 2, 0, 0);
@include typoS;
cursor: pointer;
color: $colorWhite;
border: 1px solid $alphaWhite60;
outline: none;
background: $alphaBlack10;
svg {
margin-top: space(-1);
height: rem-calc(19);
width: auto;
}
.balance {
font-weight: $fontWeightRegular;
position: relative;
display: flex;
align-items: center;
height: 100%;
padding-inline-start: rem-calc(8);
}
&:hover {
border: 1px solid $colorWhite;
background-color: $alphaWhite10;
}
}
.buttonHighlight {
@include layoutIncentiveButton;
}
.wrapper {
position: relative;
}
.details {
display: flex;
position: absolute;
flex-direction: column;
justify-content: flex-start;
top: rem-calc(38);
width: rem-calc(390);
left: rem-calc(-155);
z-index: 100;
@include layoutPopover;
}
.tooltip {
position: absolute;
top: rem-calc(12);
right: rem-calc(16);
svg {
fill: $colorSecondaryDark;
}
}
.detailsHeader {
display: flex;
flex: 0;
flex-wrap: nowrap;
width: 100%;
@include padding(4, 0);
@include margin(0);
position: relative;
border-bottom: 1px solid $alphaBlack20;
text-align: center;
}
.detailsHead {
@include margin(0);
@include typoScaps;
color: $colorSecondaryDark;
width: 100%;
}
.detailsBody {
flex: 0;
width: 100%;
@include padding(4, 8);
color: $colorSecondaryDark;
.successContainer {
display: flex;
flex-direction: column;
align-items: center;
.successTitle {
text-align: center;
color: $colorSecondaryDark;
@include margin(0, 0, 4);
}
.succcessTxHash {
display: flex;
@include margin(0, 0, 4);
.label {
@include margin(0, 2, 0, 0);
opacity: 0.4;
}
}
}
.container,
.total {
display: flex;
flex: 0 0 100%;
@include padding(4, 0, 0);
border-bottom: 1px solid $colorSecondaryDark;
flex-wrap: wrap;
.position {
display: flex;
flex: 0 0 100%;
flex-wrap: nowrap;
@include padding(0, 0, 4);
.head {
@include typoScaps;
}
p {
width: 100%;
@include margin(0);
}
.label {
flex: 1;
min-height: rem-calc(12);
display: flex;
flex-wrap: wrap;
.subhead {
color: $colorSecondaryDark;
opacity: 0.6;
@include margin(1, 0);
@include typoScaps;
}
.token {
@include typoS;
}
}
.value {
min-height: rem-calc(12);
flex: 0 0 rem-calc(76);
display: flex;
flex-wrap: wrap;
@include margin(0, 0, 0, 3);
p {
text-align: end;
}
.headline {
@include typoXXScaps;
font-weight: $fontWeightSemibold;
}
.tokenAmount {
@include typoS;
}
.tokenValue {
@include margin(1, 0);
@include typoXXS;
opacity: 0.6;
}
}
}
}
.total {
border-bottom: none;
.position {
.label {
.subhead {
opacity: 1;
font-weight: $fontWeightSemibold;
}
}
}
}
.claimButton {
display: flex;
justify-content: center;
flex: 0 0 rem-calc(200);
max-width: rem-calc(200);
margin: 0 auto;
@include padding(6, 0, 0);
> div {
display: flex;
flex: 0 0 100%;
width: 100%;
justify-content: center;
button {
width: 100%;
@include padding(2, 0);
}
}
}
}
@media only screen and (max-width: $bpMediumHigh) {
.details {
right: 0;
top: rem-calc(46);
transform: none;
left: unset;
}
.button {
@include margin(2, 0, 0);
}
}
@media only screen and (max-width: $bpSmallHigh) {
.details {
max-width: calc(100vw - 24px);
}
}
@keyframes moveGradient {
50% {
background-position: 100% 50%;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,118 @@
@use '../../styles/master' as *;
.container {
width: rem-calc(550);
@include margin(-5, -10, 0);
.glow {
width: rem-calc(550);
position: absolute;
opacity: 0.6;
@include glowXL;
}
.dialContainer {
width: rem-calc(550);
display: flex;
position: absolute;
}
.dotContainer {
align-items: center;
display: flex;
flex-direction: column;
margin-top: space(17.5);
.dot {
position: absolute;
width: rem-calc(4);
height: rem-calc(4);
background: $colorInfoWarning;
margin-top: space(13);
margin-inline-start: space(59);
border-radius: $borderRadiusXS;
transition: left 2s;
}
.dotGlow {
position: absolute;
width: rem-calc(7);
height: rem-calc(7);
background: $colorInfoWarning;
margin-top: space(13);
margin-inline-start: space(59);
border-radius: $borderRadiusXS;
@include glowM;
transition: left 2s;
}
}
.borrowLimitTextContainer {
align-items: center;
display: flex;
flex-direction: column;
.percent {
margin-bottom: space(-1.25);
word-break: normal;
word-wrap: unset;
}
.warn {
color: $colorInfoWarning;
}
}
.maxText {
margin-top: space(19);
flex-direction: column;
display: flex;
align-items: center;
.caption {
text-transform: uppercase;
opacity: 0.4;
margin-bottom: space(1);
}
}
}
@media only screen and (max-width: $bpMediumHigh) {
.container {
@include margin(10, 0);
width: rem-calc(360);
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
flex-direction: row;
.glow {
display: flex;
justify-content: center;
}
.dialContainer {
display: flex;
justify-content: center;
}
.maxText {
width: 100%;
@include margin(19, 0, 0);
}
}
}
@media only screen and (max-width: $bpSmallHigh) {
.container {
width: rem-calc(282);
.dialContainer {
@include margin(0);
+ div {
margin-top: space(6) !important;
}
}
}
}

View File

@ -0,0 +1,99 @@
import 'create-conical-gradient'
import ChartComponent from 'react-chartjs-2'
import styles from './RadialGauge.module.scss'
import Chart from 'chart.js'
import produceValueArc, { GradientValue } from './elements/ValueArc'
import {
setUpDefaults,
RadialGaugeController,
} from './controllers/RadialGaugeController'
import { ReactNode } from 'react'
import produceTrackArc from './elements/TrackArc'
import { GAUGE_SCALE } from '../../constants/appConstants'
import { formatValue } from '../../libs/parse'
interface Props {
currentLtv: number
maxLtv: number
maxValue: number
generateText: (limit: number) => ReactNode
showMaxValue: boolean
colorStops: Array<GradientValue>
showDot: boolean
maxTextPrefix?: string
maxTextTitle?: string
}
const RadialGauge = ({
currentLtv,
maxLtv,
maxValue,
generateText,
showMaxValue,
colorStops,
showDot = true,
maxTextPrefix = '$',
maxTextTitle = 'MAX',
}: Props) => {
Chart.defaults.radialGauge = Chart.defaults.doughnut
setUpDefaults()
// @ts-ignore : Chart.elements is not exposed on the d.ts file for chart.js
Chart.elements.RoundedOverArc = produceValueArc(colorStops)
//@ts-ignore
Chart.elements.RoundedArc = produceTrackArc(currentLtv > 0)
var controller = RadialGaugeController()
// todo enable tooltips when we have the finalised specification for them
Chart.defaults.global.tooltips.enabled = false
Chart.controllers.radialGauge = controller
const chartData = {
datasets: [
{
// This is the value we want to show
data: [(currentLtv > 100 ? 100 : currentLtv) * GAUGE_SCALE],
borderWidth: 0,
maxLtv: maxLtv,
},
],
}
return (
<div className={styles.container}>
<div className={styles.glow}>
<ChartComponent
data={chartData}
// We can do this because of https://github.com/jerairrest/react-chartjs-2/blob/master/src/index.js#L25
// where it is checking the Chart.js instance for a controller that matches the type.
// Typescript will complain however because it is not one of the preconfigured charts in react-chart-js.
//@ts-ignore
type='radialGauge'
/>
</div>
<div className={styles.dialContainer}>
<ChartComponent
data={chartData}
//@ts-ignore - see comment above
type='radialGauge'
/>
</div>
{generateText(currentLtv ?? 0)}
{showMaxValue ? (
<div className={styles.maxText}>
<span className={`caption ${styles.caption}`}>
{maxTextTitle}
</span>
<span className={'sub2'} style={{ opacity: '1' }}>
{maxTextPrefix}
{formatValue(maxValue, 2, 2, true, false, true, true)}
</span>
</div>
) : null}
</div>
)
}
export default RadialGauge

View File

@ -0,0 +1,279 @@
import Chart, { helpers } from 'chart.js'
import { GAUGE_SCALE } from '../../../constants/appConstants'
const setUpDefaults = () => {
Chart.defaults._set('radialGauge', {
animation: {
// Boolean - Whether we animate the rotation of the radialGauge
animateRotate: true,
// Boolean - Whether we animate scaling the radialGauge from the centre
animateScale: true,
},
// Todo remove these out of the table
// The percentage of the chart that is the center area
centerPercentage: 95.5,
maintainAspectRatio: true,
circumference: Math.PI + 2,
rotation: -Math.PI - 1,
// the color of the radial gauge's track
trackColor: '#1c1c1c',
// whether arc for the gauge should have rounded corners
roundedCorners: true,
hover: {
mode: 'disable',
},
options: {
tooltips: {
enabled: false,
},
},
tooltips: {
enabled: false,
},
legend: {
display: false,
},
// the domain of the metric
domain: [0, 100],
})
}
const getCustomRadialGaugeController = () => {
return Chart.controllers.doughnut.extend({
// @ts-ignore
dataElementType: Chart.elements.RoundedOverArc,
linkScales: helpers.noop,
draw: function (ease: any) {
this.drawTrack()
Chart.controllers.doughnut.prototype.draw.call(this, ease)
this.drawDot()
},
drawTrack() {
//@ts-ignore
new Chart.elements.RoundedArc({
_view: {
backgroundColor: this.chart.options.trackColor,
borderColor: this.chart.options.trackColor,
borderWidth: 0,
startAngle: this.chart.options.rotation,
endAngle: Math.PI * 0.33,
x: this.centerX,
y: this.centerY,
innerRadius: this.innerRadius,
outerRadius: this.outerRadius,
roundedCorners: true,
},
_chart: this.chart,
}).draw()
},
drawDot() {
const maxLtv = this.getDataset()['maxLtv']
if (!maxLtv || maxLtv <= 0) {
return
}
// Calculate the angle of our borrowing capacity
const [domainStart, domainEnd] = this.getDomain() || [0, 100]
const value = maxLtv * GAUGE_SCALE
const domainSize = domainEnd - domainStart
const limitAngle =
domainSize > 0
? Math.PI *
2.0 *
(Math.abs(value - domainStart) / domainSize)
: 0
// Get the center of the arc
const arcCentre = (this.outerRadius + this.innerRadius) / 2
// Calculate offset from the centre of our circle
const x =
arcCentre * Math.cos(limitAngle + this.chart.options.rotation)
const y =
arcCentre * Math.sin(limitAngle + this.chart.options.rotation)
// @ts-ignore
new Chart.elements.Point({
_view: {
radius: 2,
pointStyle: 'circle',
backgroundColor: '#c83333',
borderColor: '#c83333',
borderWidth: 1,
// Hover
hitRadius: 1,
hoverRadius: 4,
hoverBorderWidth: 1,
x: this.centerX + x,
y: this.centerY + y,
},
_chart: this.chart,
}).draw()
},
update(reset: any) {
const chart = this.chart
const chartArea = chart.chartArea
const opts = chart.options
const arcOpts = opts.elements.arc
const availableWidth =
chartArea.right - chartArea.left - arcOpts.borderWidth
const availableHeight =
chartArea.bottom - chartArea.top - arcOpts.borderWidth
const availableSize = Math.min(availableWidth, availableHeight)
const meta = this.getMeta()
const centerPercentage = opts.centerPercentage
this.borderWidth = this.getMaxBorderWidth(meta.data)
this.outerRadius = Math.max(
(availableSize - this.borderWidth) / 2,
0
)
this.innerRadius = Math.max(
centerPercentage
? (this.outerRadius / 100) * centerPercentage
: 0,
0
)
meta.total = this.getMetricValue()
this.centerX = (chartArea.left + chartArea.right) / 2
this.centerY = (chartArea.top + chartArea.bottom) / 2
helpers.each(meta.data, (arc: any, index: any) => {
this.updateElement(arc, index, reset)
})
},
updateElement(arc: any, index: any, reset: any) {
const chart = this.chart
const chartArea = chart.chartArea
const opts = chart.options
const animationOpts = opts.animation
const centerX = (chartArea.left + chartArea.right) / 2
const centerY = (chartArea.top + chartArea.bottom) / 2
const startAngle = opts.rotation // non reset case handled later
const dataset = this.getDataset()
const arcAngle =
reset && animationOpts.animateRotate
? 0
: this.calculateArcAngle(dataset.data[index])
const value =
reset && animationOpts.animateScale ? 0 : this.getMetricValue()
const endAngle = startAngle + arcAngle // arcAngle
const innerRadius = this.innerRadius
const outerRadius = this.outerRadius
const valueAtIndexOrDefault = helpers.valueAtIndexOrDefault
helpers.extend(arc, {
// Utility
_datasetIndex: this.index,
_index: index,
// Desired view properties
_model: {
x: centerX,
y: centerY,
startAngle,
endAngle,
outerRadius,
innerRadius,
label: valueAtIndexOrDefault(
dataset.label,
index,
chart.data.labels[index]
),
roundedCorners: opts.roundedCorners,
value,
},
})
const model = arc._model
// Resets the visual styles
const custom = arc.custom || {}
const valueOrDefault = helpers.valueAtIndexOrDefault
const elementOpts = this.chart.options.elements.arc
model.backgroundColor = custom.backgroundColor
? custom.backgroundColor
: valueOrDefault(
dataset.backgroundColor,
index,
elementOpts.backgroundColor
)
model.borderColor = custom.borderColor
? custom.borderColor
: valueOrDefault(
dataset.borderColor,
index,
elementOpts.borderColor
)
model.borderWidth = custom.borderWidth
? custom.borderWidth
: valueOrDefault(
dataset.borderWidth,
index,
elementOpts.borderWidth
)
arc.pivot()
},
getMetricValue() {
let value = this.getDataset().data[0]
if (value == null) {
value = this.chart.options.domain[0] || 0
}
return value
},
getDomain() {
return this.chart.options.domain
},
calculateArcAngle() {
const [domainStart, domainEnd] = this.getDomain() || [0, 100]
const value = this.getMetricValue()
const domainSize = domainEnd - domainStart
return domainSize > 0
? Math.PI * 2.0 * (Math.abs(value - domainStart) / domainSize)
: 0
},
// gets the max border or hover width to properly scale pie charts
getMaxBorderWidth(arcs: any) {
let max = 0
const index = this.index
const length = arcs.length
let borderWidth
let hoverWidth
for (let i = 0; i < length; i++) {
borderWidth = arcs[i]._model ? arcs[i]._model.borderWidth : 0
hoverWidth = arcs[i]._chart
? arcs[i]._chart.config.data.datasets[index]
.hoverBorderWidth
: 0
max = borderWidth > max ? borderWidth : max
max = hoverWidth > max ? hoverWidth : max
}
return max
},
})
}
export {
getCustomRadialGaugeController as RadialGaugeController,
setUpDefaults,
}

View File

@ -0,0 +1,49 @@
import Chart from 'chart.js'
const produceTrackArc = (hasValue: boolean) => {
//@ts-ignore
return Chart.elements.Arc.extend({
draw() {
const noDataTrackColor = '#3a3c49'
const ctx = this._chart.ctx
const vm = this._view
const { startAngle, endAngle } = vm
const cornerRadius = (vm.outerRadius - vm.innerRadius) / 2
const cornerX = (vm.outerRadius + vm.innerRadius) / 2
// translate + rotate to make drawing the corners simpler
ctx.translate(vm.x, vm.y)
ctx.rotate(startAngle)
const angle = endAngle - startAngle
ctx.beginPath()
if (vm.roundedCorners) {
ctx.arc(cornerX, 0, cornerRadius, Math.PI, 0)
}
ctx.arc(0, 0, vm.outerRadius, 0, angle)
const x = cornerX * Math.cos(angle)
const y = cornerX * Math.sin(angle)
if (vm.roundedCorners) {
ctx.arc(x, y, cornerRadius, angle, angle + Math.PI)
}
ctx.arc(0, 0, vm.innerRadius, angle, 0, true)
ctx.closePath()
ctx.rotate(-startAngle)
ctx.translate(-vm.x, -vm.y)
ctx.strokeStyle = hasValue ? vm.borderColor : noDataTrackColor
ctx.lineWidth = vm.borderWidth
ctx.fillStyle = hasValue ? vm.backgroundColor : noDataTrackColor
ctx.fill()
ctx.lineJoin = 'bevel'
if (vm.borderWidth) {
ctx.stroke()
}
},
})
}
export default produceTrackArc

View File

@ -0,0 +1,75 @@
export interface GradientValue {
color: string
value: number
}
const produceValueArc = (colorStops: Array<GradientValue>) => {
//@ts-ignore
return Chart.elements.Arc.extend({
draw() {
const ctx = this._chart.ctx
const vm = this._view
const { startAngle, endAngle } = vm
if (vm.value === 0) {
return
}
const cornerRadius = (vm.outerRadius - vm.innerRadius) / 2
const cornerX = (vm.outerRadius + vm.innerRadius) / 2
// translate + rotate to make drawing the corners simpler
ctx.translate(vm.x, vm.y)
ctx.rotate(startAngle)
const angle = endAngle - startAngle
ctx.beginPath()
if (vm.roundedCorners) {
ctx.arc(cornerX, 0, cornerRadius, Math.PI, 0)
}
ctx.arc(0, 0, vm.outerRadius, 0, angle)
const x = cornerX * Math.cos(angle)
const y = cornerX * Math.sin(angle)
if (vm.roundedCorners) {
ctx.arc(x, y, cornerRadius, angle, angle + Math.PI)
}
ctx.arc(0, 0, vm.innerRadius, angle, 0, true)
ctx.closePath()
ctx.rotate(-startAngle)
ctx.translate(-vm.x, -vm.y)
ctx.strokeStyle = vm.borderColor
ctx.lineWidth = vm.borderWidth
ctx.strokeStyle = vm.borderColor
// create gradient and fill it
const rotationOffSet = Math.PI * 0.5
const gradient = ctx.createConicalGradient(
vm.x,
vm.y - 10,
0 + rotationOffSet,
Math.PI * 2 + rotationOffSet
)
for (var index = 0; index < colorStops.length; index += 1) {
const colorStop = colorStops[index]
gradient.addColorStop(colorStop.value, colorStop.color)
}
ctx.fillStyle = gradient.pattern
ctx.strokeStyle = gradient
ctx.fillStyle = gradient
ctx.fill()
ctx.lineJoin = 'bevel'
if (vm.borderWidth) {
ctx.stroke()
}
},
})
}
export default produceValueArc

View File

@ -0,0 +1,50 @@
@use '../../styles/master' as *;
.container {
@include layoutTooltip;
position: fixed;
display: flex;
flex-direction: column;
min-width: rem-calc(184);
.item {
@include margin(1, 0, 0);
display: flex;
flex-direction: row;
.dot {
border: 1px solid $colorWhite;
height: space(2);
border-radius: $borderRadiusXXS;
width: space(2);
@include margin(1.5, 2, 0, 0);
}
.content {
display: flex;
width: rem-calc(168);
flex-direction: column;
.titleContainer {
display: flex;
flex-direction: row;
:first-child {
flex: auto;
}
.titleText {
justify-content: start !important;
min-height: 0 !important;
}
}
.subTitleContainer {
display: flex;
flex-direction: row;
:first-child {
flex: auto;
}
.subTitleText {
opacity: 0.6;
}
}
}
}
}

View File

@ -0,0 +1,90 @@
import styles from './Apr.module.scss'
import { formatValue } from '../../libs/parse'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
interface Props {
data: AssetInfo
title: string
}
interface HoverItem {
color?: string
symbol?: string
apr?: number
subtitle: string
}
const Apr = ({ data }: Props) => {
const [aprData, setAprData] = useState<HoverItem[]>([])
const { t } = useTranslation()
const produceData = (data: AssetInfo[]): HoverItem[] => {
const items: HoverItem[] = []
data.forEach((asset: AssetInfo, key: number) => {
items.push({
color: asset.color,
symbol: asset.symbol,
apr: asset.apy,
subtitle:
key === 0 ? t('fields.baseApy') : t('common.incentiveApr'),
})
})
return items
}
useEffect(
() => {
const baseData = data.incentive ? [data, data.incentive] : [data]
setAprData(produceData(baseData))
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[data]
)
const produceItem = (item: HoverItem, key: number) => {
return (
<div className={styles.item} key={key}>
<div
className={styles.dot}
style={{ backgroundColor: item.color }}
/>
<div className={styles.content}>
<div className={styles.titleContainer}>
<span className={`body ${styles.titleText}`}>
{item.symbol}
</span>
<span className={`body ${styles.titleText}`}>
{formatValue(
item?.apr || 0,
2,
2,
true,
false,
'%'
)}
</span>
</div>
<div className={styles.subTitleContainer}>
<span className={`caption ${styles.subTitleText}`}>
{item.subtitle}
</span>
</div>
</div>
</div>
)
}
return (
<div className={styles.container}>
<p className='sub2'>{t('fields.apyBreakdown')}</p>
{aprData.map((item: HoverItem, index: number) =>
produceItem(item, index)
)}
</div>
)
}
export default Apr

View File

@ -0,0 +1,38 @@
@use '../../styles/master' as *;
.container {
position: fixed;
display: flex;
flex-direction: column;
@include layoutTooltip;
min-width: rem-calc(184);
.item {
margin-top: space(2);
display: flex;
width: rem-calc(175);
flex-direction: row;
@include typoXS;
&:last-child {
@include devider20;
font-weight: $fontWeightSemibold;
}
.label {
display: flex;
flex: 0 0 rem-calc(115);
}
.value {
display: flex;
justify-content: flex-end;
flex: 0 0 rem-calc(60);
}
&.leverage {
@include margin(2, 0, 0);
font-weight: $fontWeightSemibold;
}
}
}

View File

@ -0,0 +1,159 @@
import styles from './Apy.module.scss'
import { formatValue } from '../../libs/parse'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useRedBank } from '../../hooks'
interface Props {
trueApy: number
apyData: StrategyRate
aprData: StrategyRate
token: string
leverage?: string
borrowDenom?: string
}
const Apy = ({
trueApy,
apyData,
aprData,
token,
leverage,
borrowDenom = 'uusd',
}: Props) => {
const { t } = useTranslation()
const { findMarketInfo } = useRedBank()
const [checkedApyData, setApyData] = useState<StrategyRate>(apyData)
const [checkedAprData, setAprData] = useState<StrategyRate>(aprData)
useEffect(
() => {
if (apyData?.total !== checkedApyData?.total) {
setApyData(apyData)
}
if (aprData?.total !== checkedAprData?.total) {
setAprData(aprData)
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[apyData, aprData]
)
return (
<div className={styles.container}>
<p className='sub2'>{t('fields.apyBreakdown')}</p>
{checkedAprData?.trading > 0 && (
<div className={styles.item}>
<span className={styles.label}>
{t('fields.tradingFeesApr')}
</span>
<span className={styles.value}>
{formatValue(
checkedAprData.trading,
2,
2,
true,
false,
'%'
)}
</span>
</div>
)}
{checkedAprData?.astro > 0 && (
<div className={styles.item}>
<span className={styles.label}>
{t('fields.protocolApr', { protocol: 'ASTRO' })}
</span>
<span className={styles.value}>
{formatValue(
checkedAprData.astro,
2,
2,
true,
false,
'%'
)}
</span>
</div>
)}
{checkedAprData?.protocol > 0 && (
<div className={styles.item}>
<span className={styles.label}>
{t('fields.protocolApr', { protocol: token })}
</span>
<span className={styles.value}>
{formatValue(
checkedAprData.protocol,
2,
2,
true,
false,
'%'
)}
</span>
</div>
)}
{checkedApyData?.total > 0 && (
<div className={styles.item}>
<span className={styles.label}>
{t('fields.totalRewardsApy')}
</span>
<span className={styles.value}>
{formatValue(
checkedApyData.total,
2,
2,
true,
false,
'%'
)}
</span>
</div>
)}
{leverage && Number(leverage) > 0 && trueApy > 0 && (
<>
<div className={styles.item}>
<span className={styles.label}>
{t('fields.borrowRateApr')}
</span>
<span className={styles.value}>
{formatValue(
Number(
findMarketInfo(borrowDenom)?.borrow_rate
) * 100,
2,
2,
true,
'-',
'%'
)}
</span>
</div>
<div className={`${styles.item} ${styles.leverage}`}>
<span className={styles.label}>
{`${t('fields.leverage')} ${formatValue(
leverage || 0,
2,
2,
true,
false,
'x',
true
)}`}
</span>
</div>
<div className={styles.item}>
<span className={styles.label}>
{t('fields.leveragedApy')}
</span>
<span className={styles.value}>
{formatValue(trueApy, 2, 2, true, false, '%')}
</span>
</div>
</>
)}
</div>
)
}
export default Apy

View File

@ -0,0 +1,38 @@
@use '../../styles/master' as *;
.container {
position: fixed;
display: flex;
flex-direction: column;
@include layoutTooltip;
min-width: rem-calc(184);
.item {
margin-top: space(2);
display: flex;
width: rem-calc(175);
flex-direction: row;
@include typoXS;
&:last-child {
@include devider20;
font-weight: $fontWeightSemibold;
}
.label {
display: flex;
flex: 0 0 rem-calc(115);
}
.value {
display: flex;
justify-content: flex-end;
flex: 0 0 rem-calc(60);
}
&.leverage {
@include margin(2, 0, 0);
font-weight: $fontWeightSemibold;
}
}
}

View File

@ -0,0 +1,52 @@
import styles from './PnL.module.scss'
import { formatValue } from '../../libs/parse'
import { useTranslation } from 'react-i18next'
interface Props {
initial: string
current: string
pnlValue: number
}
const PnL = ({ initial, current, pnlValue }: Props) => {
const { t } = useTranslation()
return (
<div className={styles.container}>
<p className='sub2'>{t('fields.pnlBreakdown')}</p>
<div className={styles.item}>
<span className={styles.label}>{t('fields.initialValue')}</span>
<span className={styles.value}>
{formatValue(initial, 2, 2, true, '$')}
</span>
</div>
<div className={styles.item}>
<span className={styles.label}>{t('fields.currentValue')}</span>
<span className={styles.value}>
{formatValue(current, 2, 2, true, '$')}
</span>
</div>
<div className={styles.item}>
<span className={styles.label}>
{pnlValue >= 0 ? t('fields.profit') : t('fields.loss')}
</span>
<span
className={`${styles.value} ${
pnlValue > 0
? 'colorInfoProfit'
: pnlValue < 0
? 'colorInfoLoss'
: ''
}`}
>
{formatValue(pnlValue, 2, 2, true, '$')}
</span>
</div>
</div>
)
}
export default PnL

View File

@ -0,0 +1,8 @@
@use '../../styles/master' as *;
.info {
color: $tooltipIconColor;
fill: $tooltipIconColor !important;
cursor: pointer;
outline: none;
}

View File

@ -0,0 +1,24 @@
import { ReactNode } from 'react'
import Tippy from '@tippyjs/react'
import HelpOutlineIcon from '@material-ui/icons/HelpOutline'
import styles from './Tooltip.module.scss'
interface TooltipProps {
content: string | ReactNode
iconWidth: string
}
const Tooltip = ({ content, iconWidth }: TooltipProps) => (
<Tippy
className='tippyContainer'
content={<span className='body2'>{content}</span>}
interactive={true}
appendTo={() => document.body}
>
<HelpOutlineIcon style={{ width: iconWidth }} className={styles.info} />
</Tippy>
)
export default Tooltip

View File

@ -0,0 +1,86 @@
import colors from '../../styles/_assets.module.scss'
// Native asset images
import luna from '../../images/LUNA.svg'
import ust from '../../images/UST.svg'
// CW20 asset images
import mars from '../../images/MARS-COLORED.svg'
import astro from '../../images/ASTRO.png'
import anc from '../../images/ANC.svg'
import mir from '../../images/MIR.svg'
const Assets: Assets = {
luna: {
symbol: 'LUNA',
name: 'Terra',
logo: luna,
denom: 'uluna',
maToken: 'maluna',
color: colors.luna,
native: true,
decimals: 6,
hasOraclePrice: true,
},
ust: {
symbol: 'UST',
name: 'Terra USD',
logo: ust,
denom: 'uusd',
maToken: 'mausd',
color: colors.ust,
native: true,
decimals: 6,
hasOraclePrice: true,
},
mars: {
symbol: 'MARS',
name: 'Mars',
logo: mars,
denom: 'MARS',
contract_addr: 'terra12hgwnpupflfpuual532wgrxu2gjp0tcagzgx4n',
maToken: 'mamars',
color: colors.mars,
native: false,
decimals: 6,
hasOraclePrice: false,
},
astro: {
symbol: 'ASTRO',
name: 'Astroport',
logo: astro,
denom: 'ASTRO',
contract_addr: 'terra1xj49zyqrwpv5k928jwfpfy2ha668nwdgkwlrg3',
maToken: 'maastro',
color: colors.astro,
native: false,
decimals: 6,
hasOraclePrice: false,
},
anc: {
symbol: 'ANC',
name: 'Anchor',
logo: anc,
denom: 'ANC',
contract_addr: 'terra14z56l0fp2lsf86zy3hty2z47ezkhnthtr9yq76',
maToken: 'maanc',
color: colors.anc,
native: false,
decimals: 6,
hasOraclePrice: true,
},
mir: {
symbol: 'MIR',
name: 'Mirror',
logo: mir,
denom: 'MIR',
contract_addr: 'terra15gwkyepfc6xgca5t5zefzwy42uts8l2m4g40k6',
maToken: 'mamir',
color: colors.mir,
native: false,
decimals: 6,
hasOraclePrice: true,
},
}
export default Assets

View File

@ -0,0 +1,125 @@
import colors from '../../styles/_assets.module.scss'
// Native asset images
import luna from '../../images/LUNA.svg'
import ust from '../../images/UST.svg'
// CW20 asset images
import mars from '../../images/MARS-COLORED.svg'
import astro from '../../images/ASTRO.png'
import anc from '../../images/ANC.svg'
import mir from '../../images/MIR.svg'
import stLuna from '../../images/STLUNA.svg'
// import mine from '../../images/MINE.svg'
// import ornb from '../../images/ORNb.svg'
const Assets: Assets = {
luna: {
symbol: 'LUNA',
name: 'Terra',
logo: luna,
denom: 'uluna',
maToken: 'maluna',
color: colors.luna,
native: true,
decimals: 6,
hasOraclePrice: true,
},
ust: {
symbol: 'UST',
name: 'Terra USD',
logo: ust,
denom: 'uusd',
maToken: 'mausd',
color: colors.ust,
native: true,
decimals: 6,
hasOraclePrice: true,
},
stLuna: {
symbol: 'stLUNA',
name: 'Lido Staked LUNA',
logo: stLuna,
denom: 'stLuna',
contract_addr: 'terra1e42d7l5z5u53n7g990ry24tltdphs9vugap8cd',
maToken: 'mastluna',
color: colors.stLuna,
native: false,
decimals: 6,
hasOraclePrice: true,
},
mars: {
symbol: 'MARS',
name: 'Mars',
logo: mars,
denom: 'MARS',
contract_addr: 'terra1qs7h830ud0a4hj72yr8f7jmlppyx7z524f7gw6',
maToken: 'mamars',
color: colors.mars,
native: false,
decimals: 6,
hasOraclePrice: false,
},
astro: {
symbol: 'ASTRO',
name: 'Astroport',
logo: astro,
denom: 'ASTRO',
contract_addr: 'terra1cc2up8erdqn2l7nz37qjgvnqy56sr38aj9vqry',
maToken: 'maastro',
color: colors.astro,
native: false,
decimals: 6,
hasOraclePrice: false,
},
anc: {
symbol: 'ANC',
name: 'Anchor',
logo: anc,
denom: 'TTN',
contract_addr: 'terra1747mad58h0w4y589y3sk84r5efqdev9q4r02pc',
maToken: 'mattn',
color: colors.anc,
native: false,
decimals: 6,
hasOraclePrice: true,
},
fanc: {
symbol: 'FANC',
name: 'Anchor',
logo: anc,
denom: 'FTTN',
contract_addr: 'terra12n6pf6rj3z86s90594d57nmmh65q4pyh502c8z',
maToken: 'mafttn',
color: colors.anc,
native: false,
decimals: 6,
hasOraclePrice: true,
},
mir: {
symbol: 'MIR',
name: 'Mirror',
logo: mir,
denom: 'MIR',
contract_addr: 'terra10llyp6v3j3her8u3ce66ragytu45kcmd9asj3u',
maToken: 'mamir',
color: colors.mir,
native: false,
decimals: 6,
hasOraclePrice: true,
},
fmir: {
symbol: 'FMIR',
name: 'Mirror',
logo: mir,
denom: 'FMIR',
contract_addr: 'terra1swlg8vmfajfw6j0ay57wpa533dywwqz9zqd7cj',
maToken: 'mafmir',
color: colors.mir,
native: false,
decimals: 6,
hasOraclePrice: true,
},
}
export default Assets

View File

@ -0,0 +1,5 @@
{
"contracts": {
"basecampAddress": "terra1685de0sx5px80d47ec2xjln224phshysqxxeje"
}
}

View File

@ -0,0 +1,5 @@
{
"contracts": {
"basecampAddress": "terra1jtdz9fhrrwd8yak6e3z7utmkypvx0qf0n393c6"
}
}

View File

@ -0,0 +1,127 @@
import { StrategyProviders } from '../../constants/strategyProviders'
import Assets from '../assets/mainnet'
const FieldsStrategies: FieldsStrategy[] = [
{
key: 'lunaBullStrategy',
name: 'LUNA-UST LP',
description: 'LUNA-UST Bull Leveraged Yield Farm (Max 2x)',
minter: 'terra1m6ywlgn6wrjuagcmmezzz2a029gtldhey5k552',
logo: Assets.ust.logo,
borrow: Assets.ust.symbol,
color: Assets.luna.color,
astroportGenerator: 'terra1zgrx9jjqrfye8swykfgmd6hpde60j0nszzupp9',
contract_addr: 'terra1kztywx50wv38r58unxj9p6k3pgr2ux6w5x68md',
lpToken: 'terra1m24f7k4g66gnh9f7uncp32p722v0kyt3q4l3u5',
assets: [Assets.luna, Assets.ust],
maxLeverage: 2,
provider: StrategyProviders.MARS,
apyQuery: {
url: 'https://api.astroport.fi/graphql',
query: `
lunaBullStrategy: pool(address:"terra1m6ywlgn6wrjuagcmmezzz2a029gtldhey5k552") {
trading_fees {
apy
apr
}
astro_rewards {
apy
apr
}
protocol_rewards {
apy
apr
}
total_rewards {
apy
apr
}
}`,
target: ['pool', 'total_rewards', 'apy'],
apr: true,
absolute: false,
},
},
{
key: 'ancBullStrategy',
name: 'ANC-UST LP',
description: 'ANC-UST Bull Leveraged Yield Farm (Max 2x)',
minter: 'terra1qr2k6yjjd5p2kaewqvg93ag74k6gyjr7re37fs',
logo: Assets.ust.logo,
borrow: Assets.ust.symbol,
color: Assets.anc.color,
contract_addr: 'terra1vapq79y9cqghqny7zt72g4qukndz282uvqwtz6',
astroportGenerator: 'terra1zgrx9jjqrfye8swykfgmd6hpde60j0nszzupp9',
lpToken: 'terra1wmaty65yt7mjw6fjfymkd9zsm6atsq82d9arcd',
assets: [Assets.anc, Assets.ust],
maxLeverage: 2,
provider: StrategyProviders.MARS,
apyQuery: {
url: 'https://api.astroport.fi/graphql',
query: `
ancBullStrategy: pool(address:"terra1qr2k6yjjd5p2kaewqvg93ag74k6gyjr7re37fs") {
trading_fees {
apy
apr
}
astro_rewards {
apy
apr
}
protocol_rewards {
apy
apr
}
total_rewards {
apy
apr
}
}`,
target: ['pool', 'total_rewards', 'apy'],
apr: true,
absolute: false,
},
},
{
key: 'mirBullStrategy',
name: 'MIR-UST LP',
description: 'MIR-UST Bull Leveraged Yield Farm (Max 2x)',
minter: 'terra143xxfw5xf62d5m32k3t4eu9s82ccw80lcprzl9',
logo: Assets.ust.logo,
borrow: Assets.ust.symbol,
color: Assets.mir.color,
contract_addr: 'terra12dq4wmfcsnz6ycep6ek4umtuaj6luhfp256hyu',
astroportGenerator: 'terra1zgrx9jjqrfye8swykfgmd6hpde60j0nszzupp9',
lpToken: 'terra17trxzqjetl0q6xxep0s2w743dhw2cay0x47puc',
assets: [Assets.mir, Assets.ust],
maxLeverage: 2,
provider: StrategyProviders.MARS,
apyQuery: {
url: 'https://api.astroport.fi/graphql',
query: `
mirBullStrategy: pool(address:"terra143xxfw5xf62d5m32k3t4eu9s82ccw80lcprzl9") {
trading_fees {
apy
apr
}
astro_rewards {
apy
apr
}
protocol_rewards {
apy
apr
}
total_rewards {
apy
apr
}
}`,
target: ['pool', 'total_rewards', 'apy'],
apr: true,
absolute: false,
},
},
]
export default FieldsStrategies

View File

@ -0,0 +1,239 @@
import { StrategyProviders } from '../../constants/strategyProviders'
import Assets from '../assets/testnet'
const FieldsStrategies: FieldsStrategy[] = [
{
key: 'apolloAncBullStrategy',
externalLink: 'https://app.apollo.farm/',
minter: 'terra13r3vngakfw457dwhw9ef36mc8w6agggefe70d9',
logo: Assets.ust.logo,
borrow: Assets.ust.symbol,
color: Assets.luna.color,
astroportGenerator: 'terra1gjm7d9nmewn27qzrvqyhda8zsfl40aya7tvaw5',
contract_addr: 'terra1lanxgewats337t49mnkaeh5g098j2g7e87k87s',
lpToken: 'terra1agu2qllktlmf0jdkuhcheqtchnkppzrl4759y6',
assets: [Assets.anc, Assets.ust],
maxLeverage: 2.5,
provider: StrategyProviders.APOLLO,
apyQuery: {
url: 'https://api.astroport.fi/graphql',
query: `
apolloAncBullStrategy: pool(address:"terra1qr2k6yjjd5p2kaewqvg93ag74k6gyjr7re37fs") {
trading_fees {
apy
apr
}
astro_rewards {
apy
apr
}
protocol_rewards {
apy
apr
}
total_rewards {
apy
apr
}
}`,
target: ['pool', 'total_rewards', 'apy'],
apr: true,
absolute: false,
},
},
{
key: 'lunaBullStrategy',
minter: 'terra1e49fv4xm3c2znzpxmxs0z2z6y74xlwxspxt38s',
logo: Assets.ust.logo,
borrow: Assets.ust.symbol,
color: Assets.luna.color,
astroportGenerator: 'terra1gjm7d9nmewn27qzrvqyhda8zsfl40aya7tvaw5',
contract_addr: 'terra1pkpgcqy38gyr978xfh9fx0ttq0jllzyfl05k4f',
lpToken: 'terra1dqjpcqej9nxej80u0p56rhkrzlr6w8tp7txkmj',
assets: [Assets.luna, Assets.ust],
maxLeverage: 2,
provider: StrategyProviders.MARS,
apyQuery: {
url: 'https://api.astroport.fi/graphql',
query: `
lunaBullStrategy: pool(address:"terra1m6ywlgn6wrjuagcmmezzz2a029gtldhey5k552") {
trading_fees {
apy
apr
}
astro_rewards {
apy
apr
}
protocol_rewards {
apy
apr
}
total_rewards {
apy
apr
}
}`,
target: ['pool', 'total_rewards', 'apy'],
apr: true,
absolute: false,
},
},
{
key: 'ancBullStrategy',
minter: 'terra13r3vngakfw457dwhw9ef36mc8w6agggefe70d9',
logo: Assets.ust.logo,
borrow: Assets.ust.symbol,
color: Assets.anc.color,
contract_addr: 'terra1x3tu0tgsa3wuz97w2nm29fvhnhjnag00nxsgmy',
astroportGenerator: 'terra1gjm7d9nmewn27qzrvqyhda8zsfl40aya7tvaw5',
lpToken: 'terra1agu2qllktlmf0jdkuhcheqtchnkppzrl4759y6',
assets: [Assets.anc, Assets.ust],
maxLeverage: 2,
provider: StrategyProviders.MARS,
apyQuery: {
url: 'https://api.astroport.fi/graphql',
query: `
ancBullStrategy: pool(address:"terra1qr2k6yjjd5p2kaewqvg93ag74k6gyjr7re37fs") {
trading_fees {
apy
apr
}
astro_rewards {
apy
apr
}
protocol_rewards {
apy
apr
}
total_rewards {
apy
apr
}
}`,
target: ['pool', 'total_rewards', 'apy'],
apr: true,
absolute: false,
},
},
{
key: 'mirBullStrategy',
minter: 'terra1xrt4j56mkefvhnyqqd5pgk7pfxullnkvsje7wx',
logo: Assets.ust.logo,
borrow: Assets.ust.symbol,
color: Assets.mir.color,
contract_addr: 'terra1wrj7lrrxzdmcmpask6y48eudq7huvu8eylssjs',
astroportGenerator: 'terra1gjm7d9nmewn27qzrvqyhda8zsfl40aya7tvaw5',
lpToken: 'terra1efmcf22aweaj3zzjhzgyghv88dda0yk4j9jp29',
assets: [Assets.mir, Assets.ust],
maxLeverage: 2,
provider: StrategyProviders.MARS,
apyQuery: {
url: 'https://api.astroport.fi/graphql',
query: `
mirBullStrategy: pool(address:"terra143xxfw5xf62d5m32k3t4eu9s82ccw80lcprzl9") {
trading_fees {
apy
apr
}
astro_rewards {
apy
apr
}
protocol_rewards {
apy
apr
}
total_rewards {
apy
apr
}
}`,
target: ['pool', 'total_rewards', 'apy'],
apr: true,
absolute: false,
},
},
{
key: 'fakeMirStrat',
minter: 'terra1408najmjzueu9ktg4rw7a8yge3z280d5yp9nay',
logo: Assets.ust.logo,
borrow: Assets.ust.symbol,
color: Assets.fmir.color,
contract_addr: 'terra16htwvqxqkygazqxmgt9wpqr0vtf6jekm6mf44n',
astroportGenerator: 'terra1f7awhwwqcjkxlk83qvgzrpnnnzm7577u5ffsx7',
lpToken: 'terra1r5q4cepddwn9sklkm4x0jhspv09w8rwsfszx5u',
assets: [Assets.fmir, Assets.ust],
maxLeverage: 2,
provider: StrategyProviders.MARS,
apyQuery: {
url: 'https://api.astroport.fi/graphql',
query: `
fakeMirStrat: pool(address:"terra1m6ywlgn6wrjuagcmmezzz2a029gtldhey5k552") {
trading_fees {
apy
apr
}
astro_rewards {
apy
apr
}
protocol_rewards {
apy
apr
}
total_rewards {
apy
apr
}
}`,
target: ['pool', 'total_rewards', 'apy'],
apr: true,
absolute: false,
},
},
{
key: 'fancBullStrategy',
minter: 'terra1q2kv38ldy03g8ql6l8vfhc0sf5a2ypur7ee50c',
logo: Assets.ust.logo,
borrow: Assets.ust.symbol,
color: Assets.fanc.color,
contract_addr: 'terra1gq03knyygm3v0fu2cvc3nptdhndl9cr4vekxkv',
astroportGenerator: 'terra1f7awhwwqcjkxlk83qvgzrpnnnzm7577u5ffsx7',
lpToken: 'terra1llys4hxu8wkt0msnm0nz328ymyrk3j3nz65rkx',
assets: [Assets.fanc, Assets.ust],
maxLeverage: 2,
provider: StrategyProviders.MARS,
apyQuery: {
url: 'https://api.astroport.fi/graphql',
query: `
fancBullStrategy: pool(address:"terra1qr2k6yjjd5p2kaewqvg93ag74k6gyjr7re37fs") {
trading_fees {
apr
apy
}
astro_rewards {
apy
apr
}
protocol_rewards {
apy
apr
}
total_rewards {
apy
apr
}
}`,
target: ['pool', 'total_rewards', 'apy'],
apr: true,
absolute: false,
},
},
]
export default FieldsStrategies

View File

@ -0,0 +1,19 @@
{
"contracts": {
"redBankContractAddress": "terra19dtgj9j5j7kyf3pmejqv8vzfpxtejaypgzkz5u"
},
"whitelist": {
"terra1dj9jfrrjc0yckz8l5g3mmqd86e67msvfh3q30f": {
"denom": "ANC",
"decimals": 6
},
"terra1x4rrkxx5pyuce32wsdn8ypqnpx8n27klnegv0d": {
"denom": "uluna",
"decimals": 6
},
"terra1cuku0vggplpgfxegdrenp302km26symjk4xxaf": {
"denom": "uusd",
"decimals": 6
}
}
}

View File

@ -0,0 +1,19 @@
{
"contracts": {
"redBankContractAddress": "terra1avkm5w0gzwm92h0dlxymsdhx4l2rm7k0lxnwq7"
},
"whitelist": {
"terra1l7dzcwhz24prg3dsuzrl9s7l6cpc2l72lesacd": {
"denom": "stLuna",
"decimals": 6
},
"terra1gwl5srdgdy2r24dxpdzn7m2t7509yhnzjyc36w": {
"denom": "uluna",
"decimals": 6
},
"terra1xmx2e3eyj9d0mcycpak4fa95z0wdvykp7zesqt": {
"denom": "uusd",
"decimals": 6
}
}
}

View File

@ -0,0 +1,6 @@
{
"lockdropAddress": "terra1n38982txtv2yygtcfv3e9wp2ktmjyxl6z88rma",
"airdropAddress": "terra106htgurux879drvjy5tzp8l7ydcnytmhpj7yr3",
"auctionAddress": "terra1hgyamk2kcy3stqx82wrnsklw9aq7rask5dxfds",
"astroportMarsUstPoolAddress": "terra19wauh79y42u5vt62c5adt2g5h4exgh26t3rpds"
}

View File

@ -0,0 +1,5 @@
{
"lockdropAddress": "terra1eqpcgsa7750r8rjqhxzcmpzvw46fn7gquywzfe",
"airdropAddress": "terra1xn9079ly7qfxf3mspf3pny3w4q4hl6agxwlnps",
"auctionAddress": "terra1myp6y5869dngaczu943pqej56r8at3as2ehqje"
}

View File

@ -0,0 +1,5 @@
{
"contracts": {
"astroportFactoryAddress": "terra1fnywlw4edny3vw44x04xd67uzkdqluymgreu7g"
}
}

View File

@ -0,0 +1,5 @@
{
"contracts": {
"astroportFactoryAddress": "terra15jsahkaf9p0qu8ye873p0u5z6g07wdad0tdq43"
}
}

View File

@ -0,0 +1,14 @@
import Assets from '../assets/mainnet'
const nativeAssetWhitelist: WhitelistAsset[] = [Assets.luna, Assets.ust]
const cw20AssetWhitelist: WhitelistAsset[] = [Assets.anc]
// These assets are supported by the red bank and should use the Mars oracle
export const Whitelist: WhitelistAsset[] = [
...nativeAssetWhitelist,
...cw20AssetWhitelist,
]
// These assets are not supported by red bank but will be displayed in the app where they need an exchange rate
export const Other: OtherAsset[] = [Assets.mars, Assets.astro, Assets.mir]

View File

@ -0,0 +1,25 @@
import Assets from '../assets/testnet'
const nativeAssetWhitelist: WhitelistAsset[] = [
Assets.luna,
Assets.ust,
Assets.stLuna,
]
const cw20AssetWhitelist: WhitelistAsset[] = []
// These assets are supported by the red bank and should use the Mars oracle
export const Whitelist: WhitelistAsset[] = [
...nativeAssetWhitelist,
...cw20AssetWhitelist,
]
// These assets are not supported by red bank but will be displayed in the app where they need an exchange rate
export const Other: OtherAsset[] = [
Assets.mars,
Assets.astro,
Assets.mir,
Assets.anc,
Assets.fanc,
Assets.fmir,
]

View File

@ -0,0 +1,42 @@
/* terra:network */
export const FINDER_URL = 'https://terrascope.info/'
export const EXTENSION = 'https://terra.money/extension'
export const CHROME = 'https://google.com/chrome'
export const ASTROPORT_URL = 'https://app.astroport.fi/swap'
export const FORUM_URL = 'https://forum.marsprotocol.io/'
/* contract deploy block heights */
export const STAKING_CONTRACT_DEPLOY_HEIGHT = 6531019
export const MARS_CONTRACT_DEPLOY_HEIGHT = 6530880
export const XMARS_CONTRACT_DEPLOY_HEIGHT = 6531023
export const AUCTION_CONTRACT_END_TIMESTAMP = 1646046000 + 432000 + 172800 // init_timestamp + ust_deposit_window + withdrawal_window
export const AUCTION_LP_TOKENS_VESTING_DURATION =
AUCTION_CONTRACT_END_TIMESTAMP + 7776000 // lp_tokens_vesting_duration
/* mars:unit */
export const MARS_DENOM = 'MARS'
export const MARS_DECIMALS = 6
export const XMARS_DENOM = 'XMARS'
export const XMARS_DECIMALS = 6
export const ASTRO_DENOM = 'ASTRO'
export const ASTRO_DECIMALS = 6
export const PROPOSAL_LIMIT = 5
export const UST_DENOM = 'uusd'
export const UST_DECIMALS = 6
export const BLOCK_TIME = 7500
export const BLOCKS_PER_DAY = (86400 * 1000) / BLOCK_TIME
export const COOLDOWN_BUFFER = BLOCK_TIME * 2
/* borrowLimit */
export const GAUGE_SCALE = 0.825
export const DEFAULT_SLIPPAGE = 0.01
/* other */
export const VOLATILITY_THRESHOLD = 0.05
/* feature flags */
export const FIELDS_FEATURE: Boolean = true
export const MY_LOCKDROP_FEATURE: Boolean = true
export const PROPOSAL_ACTION_BUTTONS_FEATURE: Boolean = false
export const AIRDROP_CLAIM_FEATURE: Boolean = true

View File

@ -0,0 +1,15 @@
import mars from '../images/MARS-COLORED.svg'
import apollo from '../images/Apollo.svg'
import { StrategyProvider } from '../types/interfaces/strategyProvider'
export const StrategyProviders: StrategyProvider = {
MARS: {
logo: mars,
name: 'Mars',
},
APOLLO: {
logo: apollo,
name: 'Apollo',
},
}

View File

@ -0,0 +1,3 @@
export const DAY_IN_SECONDS = 60 * 60 * 24
export const HOUR_IN_SECONDS = 60 * 60
export const MINUTE_IN_SECONDS = 60

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More