diff --git a/.env.development b/.env.development index 9b14a1f..9ac3054 100644 --- a/.env.development +++ b/.env.development @@ -5,7 +5,7 @@ ENABLE_AUCTION_PARTICIPATION=true AUCTION_START_TIMESTAMP=1614686400 # The auction API base URL -AUCTION_API_URL=https://backend.auction.tlbc.trustlines.foundation/auction-summary +AUCTION_API_URL=auction-summary.json # The auction address that's rendered into the auction page AUCTION_ADDRESS=0xCDB34843978684f8DD39B3eDE59e73Bf49d3FBf4 @@ -19,5 +19,15 @@ REACT_APP_EXPLORER_URL=https://etherscan.io/ REACT_APP_CHAIN_ID=1 REACT_APP_MERKLE_DROP_ADDRESS=0x0A6f0C541Be542c098B7Ee03C9C634f20BCf8422 +# Number of decimal places of the Token. Will be 18 most of the time. +REACT_APP_TOKEN_DECIMALS=18 + +# Number of decimal places to show to the user. The Value will be rounded. +REACT_APP_SHOW_DECIMALS=2 + # Configure newsletter subscription REACT_APP_MAILCHIMP_URL=https://foundation.us20.list-manage.com/subscribe/post?u=ad97861a3786f82a87c0c7417&id=7125095980 + +# GATSBY +GATSBY_CONTACT_MAIL=contact@trustlines.foundation +GATSBY_FORM_POST_URL=https://europe-west1-trustlines-network.cloudfunctions.net/websiteContact diff --git a/.env.example b/.env.example index a5b6d7f..7c632c8 100644 --- a/.env.example +++ b/.env.example @@ -37,3 +37,9 @@ REACT_APP_CONFIRMATIONS=1 # Mailchimp URL to register subscribers based on their signup forms REACT_APP_MAILCHIMP_URL=https://..list.manage.com/subscribe/post?u=&id= + +# Conatct email +GATSBY_CONTACT_MAIL=contact@trustlines.foundation + +# Contact message sending service URL +GATSBY_FORM_POST_URL=https://europe-west1-trustlines-network.cloudfunctions.net/websiteContact diff --git a/.env.production b/.env.production index 0be8335..93d99fe 100644 --- a/.env.production +++ b/.env.production @@ -21,3 +21,7 @@ REACT_APP_MERKLE_DROP_ADDRESS=0x0A6f0C541Be542c098B7Ee03C9C634f20BCf8422 # Configure newsletter subscription REACT_APP_MAILCHIMP_URL=https://foundation.us20.list-manage.com/subscribe/post?u=ad97861a3786f82a87c0c7417&id=7125095980 + +# GATSBY +GATSBY_CONTACT_MAIL=contact@trustlines.foundation +GATSBY_FORM_POST_URL=https://europe-west1-trustlines-network.cloudfunctions.net/websiteContact diff --git a/.gitignore b/.gitignore index 722aca5..75c5e28 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ dist/ yarn-error.log .DS_Store + +public +.cache diff --git a/.prettierrc b/.prettierrc index 80bf05a..757fd64 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,4 +1,3 @@ { - "trailingComma": "es5", - "semi": false, + "trailingComma": "es5" } diff --git a/gatsby-browser.js b/gatsby-browser.js new file mode 100644 index 0000000..aa2bdc3 --- /dev/null +++ b/gatsby-browser.js @@ -0,0 +1 @@ +import "./src/styles/global.css"; diff --git a/gatsby-config.js b/gatsby-config.js new file mode 100644 index 0000000..ee2ea45 --- /dev/null +++ b/gatsby-config.js @@ -0,0 +1,3 @@ +module.exports = { + plugins: ["gatsby-plugin-postcss", "gatsby-plugin-react-helmet"], +}; diff --git a/gatsby-node.js b/gatsby-node.js new file mode 100644 index 0000000..af40ba0 --- /dev/null +++ b/gatsby-node.js @@ -0,0 +1,21 @@ +exports.onCreateWebpackConfig = ({ actions, plugins }) => { + actions.setWebpackConfig({ + resolve: { + fallback: { + crypto: require.resolve("crypto-browserify"), + stream: require.resolve("stream-browserify"), + vm: require.resolve("vm-browserify"), + http: require.resolve("stream-http"), + https: require.resolve("https-browserify"), + os: require.resolve("os-browserify/browser"), + scrypt: require.resolve("scrypt-js"), + }, + }, + plugins: [ + plugins.provide({ + Buffer: ["buffer/", "Buffer"], + process: "process/browser", + }), + ], + }); +}; diff --git a/package.json b/package.json index 28b72b0..a376e5a 100644 --- a/package.json +++ b/package.json @@ -34,21 +34,21 @@ "html-minifier": "^4.0.0", "husky": "^3.0.2", "livereload": "^0.8.0", + "mathjs": "^7.0.0", "moment-timezone": "^0.5.26", "node-sass": "^4.13.1", "nunjucks-cli": "^1.0.0", "prettier": "^1.18.2", - "react": "^16.8.6", + "react": "^17.0.2", "react-app-polyfill": "^1.0.1", "react-dev-utils": "^9.0.1", - "react-dom": "^16.8.6", + "react-dom": "^17.0.2", "react-notify-toast": "^0.5.0", "serve": "^11.1.0", "web3": "2.0.0-alpha.1", "webpack": "^4.39.1", "webpack-cli": "^3.3.6", - "webpack-merge": "^4.2.1", - "mathjs": "^7.0.0" + "webpack-merge": "^4.2.1" }, "scripts": { "docker-preview": "yarn run build-preview && docker build . -t preview.trustlines.foundation", @@ -73,12 +73,24 @@ "format": "eslint ./src/**/*.js ./webpack.*.js --fix", "prettier:big-tabs": "prettier --write --print-width 120 --no-semi --single-quote --tab-width 4 './src/**/*.js'", "lint": "eslint ./src/**/*.js", - "deploy": "WHOAMI= yarn run build && gh-pages --dist dist --branch gh-pages -m 'Auto commit updates [ci skip]'" + "deploy": "WHOAMI= yarn run build && gh-pages --dist dist --branch gh-pages -m 'Auto commit updates [ci skip]'", + "start": "gatsby develop" }, "husky": { "hooks": { "pre-commit": "npm run lint" } }, - "dependencies": {} + "dependencies": { + "autoprefixer": "^10.3.1", + "chart.js": "2.8.0", + "gatsby": "^3.11.1", + "gatsby-plugin-postcss": "^4.11.0", + "gatsby-plugin-react-helmet": "^4.11.0", + "jquery": "^3.6.0", + "postcss": "^8.3.6", + "react-helmet": "^6.1.0", + "react-youtube": "^7.13.1", + "tailwindcss": "^2.2.7" + } } diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..fa8aabd --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,3 @@ +module.exports = () => ({ + plugins: [require("tailwindcss")], +}); diff --git a/src/features/auction/api/chart.js b/src/features/auction/api/chart.js new file mode 100644 index 0000000..e376f0d --- /dev/null +++ b/src/features/auction/api/chart.js @@ -0,0 +1,331 @@ +import $ from "jquery"; +import moment from "moment-timezone"; +import Chart from "chart.js"; + +import { formatTLNAmount } from "../../common/utils/math"; + +import "../../../styles/chart.css"; + +const loadingMessage = $("#loading-message"); +const chartAddons = $(".chart-addons"); +const chartContainer = $(".chart-container"); + +function getMessage(title, body) { + return ` +
+
+

+ ${title} +
+ + + +

+
${body}
+
+
+ `; +} + +function getTooltipRow(chartState, dataPoint, point) { + const row = []; + row.push( + `${moment(dataPoint.xLabel).format( + "MMM D, YYYY, h:mm:ss a" + )} ${moment.tz.guess()}` + ); + row.push( + `${moment(dataPoint.xLabel) + .tz("UTC") + .format("MMM D, YYYY, h:mm:ss a")} UTC` + ); + if (point.address) { + row.push(`Bidder: ${point.address}`); + } + row.push(`Slot Price: ${formatTLNAmount(point.slotPrice)}`); + return row; +} + +function calculateNowLabelAdjustment(currentBlocktimeInMs, mostMiddleElement) { + if (mostMiddleElement && currentBlocktimeInMs > mostMiddleElement.x) { + return 17; + } + return -17; +} + +function buildNowLabel(currentBlocktimeInMs, priceFunction) { + return { + drawTime: "afterDatasetsDraw", + annotations: [ + { + type: "line", + mode: "vertical", + scaleID: "x-axis-0", + value: currentBlocktimeInMs, + borderColor: "rgb(23,64,120)", + borderWidth: 2, + label: { + enabled: true, + position: "bottom", + content: "Now", + xAdjust: calculateNowLabelAdjustment( + currentBlocktimeInMs, + priceFunction[Math.round((priceFunction.length - 1) / 2)] + ), + }, + }, + ], + }; +} + +function renderChart(bids, priceFunction, chartState) { + let verticalLineAnnotation; + if (chartState.remainingSeconds === 0 || chartState.state !== "Started") { + verticalLineAnnotation = {}; + } else { + verticalLineAnnotation = buildNowLabel( + chartState.currentBlocktimeInMs, + priceFunction + ); + } + const plugin = { + id: "custom_canvas_background_color", + beforeDraw: chart => { + const ctx = chart.canvas.getContext("2d"); + ctx.save(); + ctx.globalCompositeOperation = "destination-over"; + ctx.fillStyle = "black"; + ctx.fillRect(0, 0, chart.width, chart.height); + ctx.restore(); + }, + }; + + const ctx = window.document.getElementById("bids").getContext("2d"); + const chart = new Chart(ctx, { + type: "line", + // plugins: [plugin], + data: { + datasets: [ + { + label: "Price Function", + data: priceFunction, + borderColor: "#09E0A3", + fill: false, + pointRadius: 0, + pointHitRadius: 1, + pointHoverRadius: 0, + }, + { + type: "bubble", + label: "Bid Price", + data: bids, + borderColor: "#7657ED", + borderWidth: 2, + backgroundColor: "#BEB6EC", + pointHitRadius: 1, + fill: false, + }, + ], + }, + options: { + maintainAspectRatio: false, + responsive: true, + legend: { + display: false, + }, + annotation: verticalLineAnnotation, + scales: { + xAxes: [ + { + id: "x-axis-0", + type: "time", + time: { + unit: "day", + displayFormats: { + day: "MMM DD", + }, + }, + }, + ], + yAxes: [ + { + id: "y-axis-0", + type: "logarithmic", + ticks: { + callback: function(value, index) { + const formattedValue = formatTLNAmount(value); + if (formattedValue.startsWith(1) || index === 0) { + return formattedValue; + } + return ""; + }, + }, + }, + ], + }, + hover: { + mode: "x", + intersect: false, + animationDuration: 0, + }, + tooltips: { + mode: "x", + intersect: false, + enabled: false, + custom: function(tooltip) { + $(this._chart.canvas).css("cursor", "pointer"); + const positionY = this._chart.canvas.offsetTop; + const positionX = this._chart.canvas.offsetLeft; + $(".chartjs-tooltip").css({ + opacity: 0, + }); + if (!tooltip || !tooltip.opacity) { + return; + } + if (tooltip.dataPoints.length > 0) { + let slotPriceSet = false; + const tooltipContent = []; + const offsetY = tooltip.dataPoints[0].y; + const offsetX = tooltip.dataPoints[0].x; + for (const dataPoint of tooltip.dataPoints) { + const point = this._data.datasets[dataPoint.datasetIndex].data[ + dataPoint.index + ]; + if (!point.address) { + if (slotPriceSet) { + continue; + } + slotPriceSet = true; + } + tooltipContent.push( + getTooltipRow(chartState, dataPoint, point).join("
") + ); + } + const $tooltip = $("#tooltip"); + $tooltip.html( + tooltipContent.join('
') + ); + const showTooltipAboveCursor = + offsetY > this._chart.canvas.offsetHeight / 2; + if (showTooltipAboveCursor) { + $tooltip.css({ + opacity: 1, + top: positionY + offsetY - 5 + "px", + left: positionX + offsetX + "px", + }); + $tooltip.addClass("chartjs-tooltip-above"); + $tooltip.addClass("chart-js-tooltop-arrow-bottom"); + $tooltip.removeClass("chartjs-tooltip-arrow-top"); + } else { + $tooltip.css({ + opacity: 1, + top: positionY + offsetY + 5 + "px", + left: positionX + offsetX + "px", + }); + $tooltip.removeClass("chartjs-tooltip-above"); + $tooltip.removeClass("chart-js-tooltop-arrow-bottom"); + $tooltip.addClass("chartjs-tooltip-arrow-top"); + } + } + }, + }, + }, + }); + Chart.defaults.global.defaultFontFamily = "Arial"; + Chart.defaults.global.defaultFontSize = 12; + + chartState.chart = chart; +} + +function fetchAuctionDataAndRender(chartState, animationDuration = 800) { + $.ajax({ + url: `${process.env.AUCTION_API_URL}`, + success: result => { + if (result.state === "Not Deployed") { + loadingMessage.show(); + loadingMessage.html( + getMessage( + "Not yet deployed", + "The auction has not been deployed yet" + ) + ); + chartAddons.hide(); + chartContainer.hide(); + return; + } + chartState.mergeRestResult(result); + + loadingMessage.hide(); + chartAddons.show(); + chartContainer.show(); + + const bidPrice = []; + const priceFunction = []; + for (const bid of result.bids) { + bidPrice.push({ + address: bid.bidder, + bidValue: parseInt(bid.bidValue, 16), + slotPrice: parseInt(bid.slotPrice, 16), + y: parseInt(bid.bidValue, 16), + x: bid.timestamp * 1000, + }); + } + for (const functionPoint of result.priceFunction) { + priceFunction.push({ + slotPrice: parseInt(functionPoint.slotPrice, 16), + y: parseInt(functionPoint.slotPrice, 16), + x: functionPoint.timestamp * 1000, + }); + } + let chart; + if (animationDuration > 0) { + renderChart(bidPrice, priceFunction, chartState); + } else { + chart = chartState.chart; + chart.data.datasets[0].data = priceFunction; + chart.data.datasets[1].data = bidPrice; + if (result.remainingSeconds > 0) { + if ( + chart.options.annotation && + chart.options.annotation.annotations + ) { + chart.options.annotation.annotations[0].value = + chartState.currentBlocktimeInMs; + chart.options.annotation.annotations[0].label.xAdjust = calculateNowLabelAdjustment( + chartState.currentBlocktimeInMs, + priceFunction[Math.round((priceFunction.length - 1) / 2)] + ); + } else { + chart.options.annotation = buildNowLabel( + chartState.currentBlocktimeInMs, + priceFunction + ); + } + } else { + chart.options.annotation = {}; + } + } + + chartState.updateChart({ duration: animationDuration }); + }, + error: function(err) { + loadingMessage.show(); + loadingMessage.html( + getMessage( + " Oops, something went wrong.", + "There was an Error while retrieving auction data" + ) + ); + chartAddons.hide(); + chartContainer.hide(); + console.error(err); + }, + }); +} + +export default function initChart(chartState) { + fetchAuctionDataAndRender(chartState, 800); + setInterval(() => { + fetchAuctionDataAndRender(chartState, 0); + }, 20000); +} diff --git a/src/features/auction/api/chartState.js b/src/features/auction/api/chartState.js new file mode 100644 index 0000000..576d182 --- /dev/null +++ b/src/features/auction/api/chartState.js @@ -0,0 +1,28 @@ +export default class ChartState { + updateChart(params = {}) { + this.chart.update(params); + } + + decrementTimers() { + if (this.remainingSeconds) { + this.remainingSeconds = this.remainingSeconds - 1; + } + this.secondsBeforeStart = + +process.env.AUCTION_START_TIMESTAMP - Math.round(new Date() / 1000); + } + + mergeRestResult(result) { + this.takenSlotsCount = result.takenSlotsCount; + this.freeSlotsCount = result.freeSlotsCount; + this.maxSlotsCount = result.maxSlotsCount; + this.minSlotsCount = result.minSlotsCount; + this.state = result.state; + this.lowestSlotPriceInWEI = result.lowestSlotPriceInWEI; + this.currentPriceInWEI = result.currentPriceInWEI; + this.initialPriceInWEI = result.initialPriceInWEI; + this.currentBlocktimeInMs = result.currentBlocktimeInMs; + if (!this.remainingSeconds) { + this.remainingSeconds = result.remainingSeconds; + } + } +} diff --git a/src/features/auction/api/web3.js b/src/features/auction/api/web3.js new file mode 100644 index 0000000..175adcb --- /dev/null +++ b/src/features/auction/api/web3.js @@ -0,0 +1,171 @@ +import getWeb3, { sendContractTransaction } from "../../common/api/web3"; +import AuctionABI from "../../../js/abi/auction.json"; +import TokenABI from "../../../js/abi/token.json"; + +export const AuctionState = { + DEPLOYED: "DeployedState", + STARTED: "StartedState", + DEPOSIT_PENDING: "DepositPendingState", + ENDED: "EndedState", + FAILED: "FailedState", +}; + +const auctionStateMapping = [ + AuctionState.DEPLOYED, + AuctionState.STARTED, + AuctionState.DEPOSIT_PENDING, + AuctionState.ENDED, + AuctionState.FAILED, +]; + +export async function fetchAuctionState() { + return auctionStateMapping[ + await getAuctionContract() + .methods.auctionState() + .call() + ]; +} + +export async function bid(address, onSign, onConfirmation) { + const auctionContract = getAuctionContract(); + const contractFunctionCall = auctionContract.methods.bid(); + + return sendContractTransaction( + contractFunctionCall, + address, + onSign, + onConfirmation + ); +} + +export async function approve(address, amount, onSign, onConfirmation) { + const tokenContract = getTokenContract(); + const contractFunctionCall = tokenContract.methods.approve( + process.env.AUCTION_ADDRESS, + amount + ); + + return sendContractTransaction( + contractFunctionCall, + address, + onSign, + onConfirmation + ); +} + +export async function withdraw(address, onSign, onConfirmation) { + const auctionContract = getAuctionContract(); + const contractFunctionCall = auctionContract.methods.withdraw(); + + return sendContractTransaction( + contractFunctionCall, + address, + onSign, + onConfirmation + ); +} + +export async function fetchValueToWithdraw(address) { + const auctionContract = getAuctionContract(); + const auctionState = await fetchAuctionState(); + if (auctionState === AuctionState.ENDED) { + return (await auctionContract.methods.bids(address).call()).sub( + await auctionContract.methods.lowestSlotPrice().call() + ); + } else if (auctionState === AuctionState.FAILED) { + return auctionContract.methods.bids(address).call(); + } else { + throw new Error( + `Cannot get value to withdraw with current auction state ${auctionState}` + ); + } +} + +export async function fetchTokenBalance(address) { + return getTokenContract() + .methods.balanceOf(address) + .call(); +} + +export async function fetchAllowance(address) { + return getTokenContract() + .methods.allowance(address, process.env.AUCTION_ADDRESS) + .call(); +} + +export async function fetchCurrentPrice() { + return getAuctionContract() + .methods.currentPrice() + .call(); +} + +export async function isWhitelisted(address) { + return getAuctionContract() + .methods.whitelist(address) + .call(); +} + +export async function hasBid(address) { + return !(await getPaidSlotPriceByAddress(address)).eq(0); +} + +export async function getPaidSlotPriceByAddress(address) { + return getAuctionContract() + .methods.bids(address) + .call(); +} + +export async function getPaidSlotPriceByReceipt(receipt) { + const bidEventsInBlock = await getBidEventsInBlock(receipt.blockNumber); + const bidEventsByTransaction = filterEventsByTransaction( + bidEventsInBlock, + receipt.transactionHash + ); + throwIfNoSingleEvent(bidEventsByTransaction); + return parseBidEventTokenAmount(bidEventsByTransaction[0]); +} + +async function getBidEventsInBlock(blockNumber) { + const merkleDropContract = getAuctionContract(); + const eventFilter = { + fromBlock: blockNumber, + toBlock: blockNumber, + }; + + return await merkleDropContract.getPastEvents("BidSubmitted", eventFilter); +} + +function filterEventsByTransaction(events, transactionHash) { + return events.filter(e => e.transactionHash === transactionHash); +} + +function throwIfNoSingleEvent(events) { + if (events.length !== 1) { + throw new ParseWithdrawEventError( + "Could not find a single event for given receipt." + ); + } +} + +function parseBidEventTokenAmount(bidEvent) { + return bidEvent.returnValues.slotPrice.toString(); +} + +function getAuctionContract() { + const web3 = getWeb3(); + return new web3.eth.Contract(AuctionABI, process.env.AUCTION_ADDRESS); +} + +function getTokenContract() { + const web3 = getWeb3(); + return new web3.eth.Contract(TokenABI, process.env.TLN_ADDRESS); +} + +export const PARSE_BID_EVENT_ERROR_CODE = "PARSE_BID_EVENT_ERROR"; + +class ParseWithdrawEventError extends Error { + constructor(...args) { + super(...args); + this.code = PARSE_BID_EVENT_ERROR_CODE; + } +} diff --git a/src/features/auction/components/bid-box.jsx b/src/features/auction/components/bid-box.jsx new file mode 100644 index 0000000..5bad633 --- /dev/null +++ b/src/features/auction/components/bid-box.jsx @@ -0,0 +1,372 @@ +import React, { useState, useCallback } from "react"; + +import { useAccount } from "../../common/hooks/account"; +import { useChainState, CHAIN_STATE } from "../../common/hooks/chain-state"; +import { BidderState, useBidderState } from "../hooks/bidder"; + +import { + getDefaultAccount, + requestPermission, + TRANSACTION_REVERTED_ERROR_CODE, + USER_REJECTED_ERROR_CODE, +} from "../../common/api/web3"; +import * as auctionWeb3 from "../api/web3"; + +import MainHeader from "./bid-components/MainHeader"; +import MessageBlock from "./bid-components/MessageBlock"; +import ActionButton from "./bid-components/ActionButton"; +import Error from "./bid-components/Error"; +import TLNLink from "./bid-components/TLNLink"; + +import CurrentPrice from "./current-price"; +import AcceptTermsAndConditions from "./bid-screens/AcceptTermsAndConditions"; +import ConnectWallet from "./bid-screens/ConnectWallet"; +import MakeBid from "./bid-screens/MakeBid"; +import NoAllowance from "./bid-screens/NoAllowance"; +import WaitForConfirmation from "./bid-screens/WaitForConfirmation"; +import SuccessfulBid from "./bid-screens/SuccessfulBid"; +import TransactionError from "./bid-screens/TransactionError"; +import AlreadyBid from "./bid-screens/AlreadyBid"; +import NotStarted from "./bid-screens/NotStarted"; +import WithdrawBid from "./bid-screens/WithdrawBid"; +import SuccessfulWithdraw from "./bid-screens/SuccessfulWithdraw"; +import NothingToWithdraw from "./bid-screens/NothingToWithdraw"; +import DepositPending from "./bid-screens/DepositPending"; +import Loading from "./bid-screens/Loading"; + +const MAX_UINT = + "115792089237316195423570985008687907853269984665640564039457584007913129639935"; + +const STATE = { + ACCEPT_TERMS_AND_CONDITION: "AcceptTermsAndConditionState", + CONNECT_WALLET: "ConnectWalletState", + READY_TO_TRANSACT_WITH_CONTRACT: "ReadyToTransactState", + WAITING_FOR_TRANSACTION_CONFIRMATION: "WaitingForConfirmationState", + WAITING_FOR_WEB3_BROWSER_ACTION: "WaitingForWeb3BrowserAction", + SUCCESSFUL_BID: "SuccessfulBidState", + SUCCESSFUL_WITHDRAW: "SuccessfulWithdrawState", + ERROR: "ErrorState", + TRANSACTION_ERROR: "TransactionErrorState", +}; + +export default function BidBox() { + const web3Account = useAccount(); + const chainState = useChainState(); + const bidderState = useBidderState(web3Account); + + const [internalState, setInternalState] = useState( + STATE.ACCEPT_TERMS_AND_CONDITION + ); + const [paidSlotPrice, setPaidSlotPrice] = useState(0); + + const [txHash, setTxHash] = useState(""); + const [confirmations, setConfirmations] = useState(0); + const [errorMessage, setErrorMessage] = useState(""); + + const reset = useCallback(() => { + setInternalState(STATE.ACCEPT_TERMS_AND_CONDITION); + }, []); + + const showError = useCallback( + (errorMessage, options = { state: STATE.ERROR }) => { + setErrorMessage(errorMessage); + setInternalState(options.state); + }, + [] + ); + + const setPaidSlotPriceByReceipt = useCallback(async receipt => { + const paidSlotPrice = await auctionWeb3.getPaidSlotPriceByReceipt(receipt); + setPaidSlotPrice(paidSlotPrice); + }, []); + + const handleAcceptTermsAndCondition = useCallback(async () => { + const account = await getDefaultAccount(); + if (account) { + setInternalState(STATE.READY_TO_TRANSACT_WITH_CONTRACT); + } else { + setInternalState(STATE.CONNECT_WALLET); + } + }, []); + + const connect = useCallback(async () => { + setInternalState(STATE.WAITING_FOR_WEB3_BROWSER_ACTION); + const success = await requestPermission(); + if (success) { + setInternalState(STATE.READY_TO_TRANSACT_WITH_CONTRACT); + } else { + setInternalState(STATE.CONNECT_WALLET); + } + }, []); + + const handleApproveConfirmation = useCallback( + (confirmationNumber, receipt) => { + // Workaround to access current hash + setTxHash(currentHash => { + // Only process incoming confirmations if it is about current transaction + if (receipt.transactionHash === currentHash) { + if ( + confirmationNumber === parseInt(process.env.REACT_APP_CONFIRMATIONS) + ) { + setInternalState(STATE.READY_TO_TRANSACT_WITH_CONTRACT); + } + setConfirmations(confirmationNumber); + } + return currentHash; + }); + }, + [setTxHash, setInternalState] + ); + + const approve = useCallback(async () => { + setInternalState(STATE.WAITING_FOR_WEB3_BROWSER_ACTION); + try { + // TODO only approve current auction price + await auctionWeb3.approve( + await getDefaultAccount(), + MAX_UINT, + hash => { + setTxHash(hash); + setInternalState(STATE.WAITING_FOR_TRANSACTION_CONFIRMATION); + }, + handleApproveConfirmation + ); + } catch (error) { + console.error(error); + if (error.code === TRANSACTION_REVERTED_ERROR_CODE) { + showError("Your transaction has been reverted.", { + state: STATE.TRANSACTION_ERROR, + }); + } else if (error.code === USER_REJECTED_ERROR_CODE) { + setInternalState(STATE.READY_TO_TRANSACT_WITH_CONTRACT); + console.log("User rejected"); + } else { + showError("Something went wrong."); + } + } + }, [handleApproveConfirmation, showError]); + + const handleBidConfirmation = useCallback( + (confirmationNumber, receipt) => { + // Workaround to access current hash + setTxHash(currentHash => { + // Only process incoming confirmations if it is about current transaction + if (receipt.transactionHash === currentHash) { + if ( + confirmationNumber === parseInt(process.env.REACT_APP_CONFIRMATIONS) + ) { + setPaidSlotPriceByReceipt(receipt); + setInternalState(STATE.SUCCESSFUL_BID); + } + setConfirmations(confirmationNumber); + } + return currentHash; + }); + }, + [setPaidSlotPriceByReceipt] + ); + + const makeBid = useCallback(async () => { + setInternalState(STATE.WAITING_FOR_WEB3_BROWSER_ACTION); + try { + await auctionWeb3.bid( + await getDefaultAccount(), + hash => { + setTxHash(hash); + setInternalState(STATE.WAITING_FOR_TRANSACTION_CONFIRMATION); + }, + handleBidConfirmation + ); + } catch (error) { + console.error(error); + if (error.code === TRANSACTION_REVERTED_ERROR_CODE) { + showError("Your transaction has been reverted.", { + state: STATE.TRANSACTION_ERROR, + }); + } else if (error.code === USER_REJECTED_ERROR_CODE) { + setInternalState(STATE.READY_TO_TRANSACT_WITH_CONTRACT); + console.log("User rejected"); + } else { + showError("Something went wrong."); + } + } + }, [handleBidConfirmation, showError]); + + const handleWithdrawConfirmation = useCallback( + (confirmationNumber, receipt) => { + // Workaround to access current hash + setTxHash(currentHash => { + // Only process incoming confirmations if it is about current transaction + if (receipt.transactionHash === currentHash) { + if ( + confirmationNumber === parseInt(process.env.REACT_APP_CONFIRMATIONS) + ) { + setInternalState(STATE.SUCCESSFUL_WITHDRAW); + } + setConfirmations(confirmationNumber); + } + return currentHash; + }); + }, + [] + ); + + const withdraw = useCallback(async () => { + setInternalState(STATE.WAITING_FOR_WEB3_BROWSER_ACTION); + try { + await auctionWeb3.withdraw( + await getDefaultAccount(), + hash => { + setTxHash(hash); + setInternalState(STATE.WAITING_FOR_TRANSACTION_CONFIRMATION); + }, + handleWithdrawConfirmation + ); + } catch (error) { + console.error(error); + if (error.code === TRANSACTION_REVERTED_ERROR_CODE) { + showError("Your transaction has been reverted.", { + state: STATE.TRANSACTION_ERROR, + }); + } else if (error.code === USER_REJECTED_ERROR_CODE) { + setInternalState(STATE.READY_TO_TRANSACT_WITH_CONTRACT); + console.log("User rejected"); + } else { + showError("Something went wrong."); + } + } + }, [handleWithdrawConfirmation, showError]); + + switch (chainState) { + case CHAIN_STATE.CONNECTING: + return ; + case CHAIN_STATE.DISCONNECTED: + return ( + + You can not participate directly from this website as no Web3 browser + was detected. To participate, install the MetaMask plugin, or use a + Web3 enabled browser. + + ); + case CHAIN_STATE.WRONG_CHAIN: + return ( + + Please connect to the {process.env.REACT_APP_CHAIN_NAME} + + ); + default: + } + + if (internalState === STATE.READY_TO_TRANSACT_WITH_CONTRACT) { + switch (bidderState) { + case BidderState.LOADING: + return ; + case BidderState.NO_ALLOWANCE: + return ; + case BidderState.NOT_WHITELISTED: + return ( + + The selected account {web3Account} is not whitelisted. + + ); + case BidderState.NOT_STARTED: + return ; + case BidderState.ENDED: + return ( + The auction has already ended. + ); + case BidderState.WRONG_ALLOWANCE: + return ( + + The allowance for this account is lower than the current auction + price. You will need to fix this manually, as this website can not + handle this case. + + ); + case BidderState.NO_ETH: + return ( + + The currently selected account doesn't have enough funds to pay for + transactions. You will need to increase the amount of ETH on the + account to proceed. + + ); + case BidderState.NOT_ENOUGH_TOKENS: + return ( + + The currently selected account doesn't have enough{" "} + TLN to bid at the current price of{" "} + . To proceed, you will need more TLN on the account + to bid at the current rate. You can also wait for the price of the + auction to go down and bid at a lower price. The minimum bid price + is 5,000 TLN. + + ); + case BidderState.ALREADY_BID: + return ; + case BidderState.READY_TO_BID: + return ; + case BidderState.READY_TO_WITHDRAW: + return ; + case BidderState.NOTHING_TO_WITHDRAW: + return ; + case BidderState.WAITING_DEPOSIT: + return ; + case BidderState.Error: + return ( + + We could not determine the current auction state. + + ); + default: + console.error("Unexpectedly reached default case."); + return Internal error; + } + } + + switch (internalState) { + case STATE.ACCEPT_TERMS_AND_CONDITION: + return ( + + ); + case STATE.CONNECT_WALLET: + return ; + case STATE.WAITING_FOR_TRANSACTION_CONFIRMATION: + return ; + case STATE.WAITING_FOR_WEB3_BROWSER_ACTION: + return ( +
+ + Please follow the instructions of your Web3 enabled browser. + +
+ ); + + case STATE.SUCCESSFUL_BID: + return ; + + case STATE.SUCCESSFUL_WITHDRAW: + return ; + + case STATE.ERROR: + return ( +
+ {errorMessage} + +
+ ); + + case STATE.TRANSACTION_ERROR: + return ( + + ); + + default: + console.error("Unexpectedly reached default branch."); + return Internal error; + } +} diff --git a/src/features/auction/components/bid-components/ActionButton.js b/src/features/auction/components/bid-components/ActionButton.js new file mode 100644 index 0000000..1253364 --- /dev/null +++ b/src/features/auction/components/bid-components/ActionButton.js @@ -0,0 +1,14 @@ +import React from "react"; +import { Button } from "../../../common/components/button"; + +export default function ActionButton(props) { + return ( + + ); +} diff --git a/src/features/auction/components/bid-components/AuctionLink.js b/src/features/auction/components/bid-components/AuctionLink.js new file mode 100644 index 0000000..239a1e3 --- /dev/null +++ b/src/features/auction/components/bid-components/AuctionLink.js @@ -0,0 +1,14 @@ +import React from "react"; + +export default function AuctionLink(props) { + return ( + + {props.children} + + ); +} diff --git a/src/features/auction/components/bid-components/Error.js b/src/features/auction/components/bid-components/Error.js new file mode 100644 index 0000000..f62e766 --- /dev/null +++ b/src/features/auction/components/bid-components/Error.js @@ -0,0 +1,17 @@ +import React from "react"; +import { Card } from "../../../common/components/card"; + +export default function Error({ title, children }) { + return ( + +
+
+
+

{title}

+
+
{children}
+
+
+
+ ); +} diff --git a/src/features/auction/components/bid-components/MainHeader.js b/src/features/auction/components/bid-components/MainHeader.js new file mode 100644 index 0000000..90029d1 --- /dev/null +++ b/src/features/auction/components/bid-components/MainHeader.js @@ -0,0 +1,13 @@ +import React from "react"; + +export default function MainHeader({ text, faIcon }) { + // The key is needed to force a rerender + return ( +
+ + + + {text} +
+ ); +} diff --git a/src/features/auction/components/bid-components/MessageBlock.js b/src/features/auction/components/bid-components/MessageBlock.js new file mode 100644 index 0000000..cd149d0 --- /dev/null +++ b/src/features/auction/components/bid-components/MessageBlock.js @@ -0,0 +1,17 @@ +import React from "react"; +import { Card } from "../../../common/components/card"; + +export default function MessageBlock(props) { + return ( + +
+
+
+

{props.title}

+
+
{props.children}
+
+
+
+ ); +} diff --git a/src/features/auction/components/bid-components/TLNLink.js b/src/features/auction/components/bid-components/TLNLink.js new file mode 100644 index 0000000..2a45f32 --- /dev/null +++ b/src/features/auction/components/bid-components/TLNLink.js @@ -0,0 +1,13 @@ +import React from "react"; + +export default function TLNLink(props) { + return ( + + {props.children} + + ); +} diff --git a/src/features/auction/components/bid-components/TermsAndConditions.js b/src/features/auction/components/bid-components/TermsAndConditions.js new file mode 100644 index 0000000..51fdd88 --- /dev/null +++ b/src/features/auction/components/bid-components/TermsAndConditions.js @@ -0,0 +1,751 @@ +import React from "react"; + +// The terms and conditions are duplicated in src/view/auction/_terms-conditions.njk +export default function TermsAndConditions() { + return ( +
+

Background

+

The Auction

+ +

+ The Trustlines Validator auction is a Dutch auction held to discover the + fair opportunity cost value, i.e. price of a Validator Slot. The + Ethereum address of successful bidders in a successful Auction will be + included in the Validator Set. +

+ +
    +
  1. + Acceptable tokens for a bid: The Auction is a smart + contract on the Ethereum Mainchain. Note that the smart contract will + not accept any other tokens than the Trustlines Network Token as a + bid. +
  2. +
  3. + Participation: To be able to participate in the + Auction, your Ethereum address must have been whitelisted by the + Foundation prior to the start of the Auction. Every whitelisted + Ethereum address can participate in the auction only once.{" "} + + Do not send your Trustlines Network Tokens to the auction contract + since this will result in you losing your tokens. Instead, use the + bid feature on the Trustlines auction page. + +
  4. +
  5. + Timing: The Auction ends after 14 days starting from + the Start Date or when the 36th successful bid is received by the + smart contract. Whichever comes first. +
  6. +
  7. + Threshold: If fewer than 15 Ethereum addresses have + successfully participated at the time the Auction ends, it will fail. + No Validator slots will be awarded and all participants can withdraw + their bids. If 15 or more successful bids were received, the Auction + will succeed. +
  8. +
  9. + Limited Validator Slots: 36 Validator Slots are up + for auction. +
  10. +
+ +

How the Auction works

+ +

+ The Auction begins when a transaction is sent by the Foundation at the + Start Date. After this transaction, the smart contract operates + independently and the Foundation will neither have nor retain any + control or influence over it. +

+ +

+ Once initialized, the Auction starts at a price of 50,000,000 Trustlines + Network Tokens per Validator Slot. Then the price will continuously + decrease over time as defined by the following parameters & functions: +

+ +
+        t = elapsed_time_in_ms / auction_duration_in_days, price = price_start *
+        (1+t) / (1+t+decay), decay = (t^3) / 746571428571
+      
+ +

whereby:

+ +
    +
  • + elapsed_time_in_ms is the elapsed time in ms since + the auction started +
  • +
  • + auction_duration_in_days equals 14 days +
  • +
  • + price_start equals 50,000,000 Trustlines Network + Tokens +
  • +
+ +

+ When the price falls to a point that a Validator Candidate thinks is + fair, they can submit their bid over the trustlines auction{" "} + page. If and when + the transaction is successfully included in the Ethereum Mainchain, the + Validator Candidate has obtained a Validator Slot in the event of a + successful Auction (see Threshold). +

+

+ If the Auction is successful, all Validator Slots are priced equal, i.e. + being equal to the price of the last successful bid received by the + smart contract. Every participant can withdraw the difference between + the end Validator Slot price and their bid. +

+

+ The final Validator Slot price of each Validator Slot cannot be + withdrawn from the smart contract and will be transferred to the Deposit + Locker (more info in the definition of the Validator Auction) smart + contract and kept there as a Stake for 9 months starting from the date + the Auction ends (see Timing). Once the 9 months have passed, Validators + can withdraw their Stake provided it doesn’t get slashed due to + equivocation. +

+ +

Validator technical capabilities

+ +

+ If a Validator Candidate successfully bid for a slot in the Auction, + he/she will be included in the Validator Set. This will allow the + Validator to run a validator node which can create blocks on the + Trustlines Blockchain. Additionally to validating and creating blocks + (including the genesis block) they will also be able to operate the + Trustlines Bridge which is used to exchange Trustlines Network Tokens + from the Ethereum Mainchain to Trustlines Network Coins on the + Trustlines Blockchain. Other than these described capabilities there are + no other unique capabilities connected solely to a Validator slot.{" "} +

+ +

Why participate

+ +

+ The Trustlines Blockchain is a minimal viable Proof of Stake sidechain + to the Ethereum Mainchain and is governed by a greenfield governance + model in which Validators can gain technical and governance experience + with such an experimental consensus model. +

+

+ Besides the Validator technical capabilities mentioned above, Validators + will also receive block rewards (as to be defined within the chain spec) + in the form of Trustlines Network Coins if they actively run their + validator node and take part in block creation. They will also be in a + position to charge transaction fees for transactions that are included + within the blocks a Validator creates. +

+

+ Note that receiving block rewards and transaction fees is dependent on a + community of Validators emerging and the potential users of the + Trustlines Blockchain. The Foundation cannot guarantee that a Trustlines + Blockchain including the Chain Spec as proposed by the Foundation + actually emerges out of the actions of these third party stakeholders. + Hence, participating in the Auction and thereby agreeing to these Terms + and Conditions does not entail a right or an entitlement in any way + whatsoever to either becoming and/or remaining a Validator or receiving + block rewards and/or transaction fees. +

+ +

General remarks, recommendations and good practice

+ +

+ Bidding Trustlines Network Tokens in the Auction does not guarantee a + Validator Slot. Especially, your bid/transaction will be rejected: +

+ +
    +
  • If you bid from an Ethereum address that isn’t whitelisted
  • +
  • If you bid less than the current Auction price
  • +
  • + If you bid before the Auction has started or after the auction has + ended +
  • +
  • + If you have already bid in the Auction (multiple bids will not result + in more than one slot. Only 1 slot per Ethereum address). +
  • +
+ +

Additionally:

+ +
    +
  • + The Auction might not meet the threshold of 15bidders, in which case + the Auction has failed and the Trustlines Network Tokens can be + withdrawn from the smart contract by its respective Auction + participant. +
  • +
  • + The current Validators might choose not to fork the Trustlines + Blockchain with the new validator set. This requires at least 50% of + the current Validator Set to do so. +
  • +
  • + Users of the Trustlines Blockchain or projects building on top of it + might align on a different Validator Set, therefore choosing a + different fork of the Trustlines Blockchain rendering the Validator + Set determined by the auction obsolete. Validators will still be able + to withdraw their Stake after the 9-month staking period. +
  • +
+ +

+ The functioning of the Ethereum mainchain is out of the control of the + Foundation, so the Foundation cannot be held liable for any malfunction, + breakdown, or abandonment of the Ethereum protocol and/or the Ethereum + Mainchain. +

+ +

+ All actions are voluntary, there is no obligation to take part in the + Auction. +

+ +

+ Validators can be removed from the Validator Set by the majority of the + Trustlines Blockchain community by hard forking. Validators removed from + the Validator set in such a way will neither earn block rewards nor + transaction fees going forward. The Stake on the Ethereum Mainchain will + remain locked for the remainder of the 9-month Staking period and will + be slashable if equivocation can be proved. +

+ +

Definitions

+ +

+ Affiliate(s) means any and all persons and or entities + directly or indirectly controlling, controlled by or under common + control with such person, where control may be by either management + authority, contract or equity interest.{" "} +

+ +

+ Ethereum Mainchain or Ethereum blockchain means the + smart contract protocol, virtual machine and decentralized network + including all its related components and protocol-related projects both + present and future, which​ ​began​ ​operation​ ​(Genesis​ ​Block)​ ​on​ + ​July​ ​30th,​ ​2015. +

+ +

+ Ether (ETH) means the cryptocurrency native to the + Ethereum mainchain, i.e. the blockchain token​ ​of​ ​Ethereum. +

+ +

+ Trustlines Network is short for “The Trustlines Network + ecosystem”, which is an ecosystem of entities, individuals, and code + that aims to promote financial & economic inclusion of all people + through decentralized and open source systems. +

+ +

+ Trustlines Protocol represents a set of rules, + processes and definitions forged into deployable code. It will comprise + several technical components used to calculate paths and store + transactions, i.e. smart contracts, the Trustlines Blockchain and the + code for the relay servers. It aims to provide a base layer to enable + interactions within the Trustlines Network. +

+ +

+ Trustlines Blockchain means a purpose built sidechain + to the Ethereum mainchain that is intended to serve as a database and + storage for transactions to support an implementation of the Trustlines + Protocol. +

+ +

+ Foundation means​ ​the Foundation,​ ​a​ ​Liechtenstein + non-profit​ foundation​ ​registered​ ​in​ Ruggell, Liechtenstein. +

+ +

+ Final Validator slot price is the price of the last + successful bid that has been successfully sent to the auction contract + and is included in the Ethereum Mainchain. +

+ +

+ Proof of Stake (PoS) means a type of consensus + algorithm by which a cryptocurrency blockchain network aims to achieve + distributed consensus. In PoS-based cryptocurrency blockchain networks + the creator of the next block is chosen via various combinations of + random selection. In contrast, the algorithm of proof-of-work-based + cryptocurrencies such as bitcoin uses mining; that is, the solving of + computationally intensive puzzles to validate transactions and create + new blocks. +

+ +

+ Sidechain means a designation for a blockchain that + runs in parallel to a primary blockchain. Entries from the primary + blockchain can be linked to and from the sidechain; this allows the + sidechain to otherwise operate independently of the primary blockchain. +

+ +

+ Validator means an individual or entity who or which + has deployed and operates the Trustlines Blockchain client in order to + validate and relay transactions on the Trustlines Blockchain and is part + of the Validator Set. +

+ +

+ Auction means the Trustlines Validator auction governed + by these terms and conditions. This auction is being hosted via a + combination of Smart Contracts on the Ethereum Mainchain in order to + discover the fair opportunity cost value (i.e. price of a Validator + Slot) and thereby the amount of TLNto be staked in order to obtain a + Validator Slot on the Trustlines Blockchain. +

+ +

+ Validator Candidate means an individual or entity whose + Ethereum address which he/she/it has provided to the Foundation and has + been whitelisted in the Auction smart contract by the Foundation in + order to take part in the Auction. +

+ +

+ Validator Slot one of the Validator positions being + auctioned in this Auction. +

+ +

+ Validator Set means a list of the Ethereum addresses of + the Validator Candidates that have successfully obtained a Validator + Slot. Said list being maintained in a specific smart contract as defined + within the chain spec. +

+ +

+ Terms mean these terms and conditions forming the + Agreement between you, the Validator Candidate and the Foundation. +

+ +

+ Disputes means any dispute, claim, suit, action,cause + of action, demand or proceeding arising out​ ​or​ ​in​ ​connection​ + ​with​ ​this​ ​Agreement,​ ​or​ ​the​ ​breach,​ ​termination​ ​or​ + ​invalidity​ ​thereof. +

+ +

+ Stake means the amount of TLN deposited in the Deposit + Locker smart contract and equaling the final Auction Validator Slot + price. +

+ +

+ + + Trustlines Bridge + + {" "} + means a combination of one smart contract deployed on the Ethereum + Mainchain, one smart contract deployed on the Trustlines Blockchain, and + a tool (tlbc - bridge) that Validators run. It allows to transfer + Trustlines Network Tokens from the Ethereum Mainchain into the + Trustlines chain as native Trustlines Network Coins. The exact code and + specifications of the Trustlines Bridge are still in development. +

+ +

+ + + Trustlines Network Tokens (TLN) + + {" "} + means tokens within the ERC20{" "} + + TLN token + {" "} + contract on the Ethereum Mainchain, which can be converted to TLC by + sending them to the Trustlines Bridge. +

+ +

+ Trustlines Network Coins (TLC) means similar to the + Ethereum Mainchain, the Trustlines Blockchain requires transaction fees + to be paid in a cryptocurrency native to the blockchain. These tokens + will be called Trustlines Network Coins. The exact code and + specifications of the TLC is still in development. +

+ +

+ Chain spec means a JSON file which specifies rules of a + blockchain. +

+ +

+ JSON means an open-standard file format that uses + human-readable text to transmit data objects consisting of + attribute-value pairs and array data types. +

+ +

+ Smart Contract(s) means the specific smart contracts + used for this Auction as can be viewed on{" "} + + https://etherscan.io/address/{process.env.AUCTION_ADDRESS} + +

+ +

+ Equivocation We define equivocation of a (present, + former, or future) validator as using their private key to sign two or + more different blocks with the same step number, irrespective of the + block's height, validity, or other fields. +

+ +

+ Step number/time slot Validators are assigned time + slots in a{" "} + + round robin + {" "} + fashion in which they can produce a block which increases the Blockchain + length +

+ +

Acceptance of terms and conditions

+ +

+ By participating in the Auction, you accept these terms and conditions. +

+ +

Representations and warranties of the Validator Candidate

+ +

+ By participating in the Auction, the Validator Candidate represents and + warrants that: +

+ +
    +
  1. You have read, understand and accept these Terms;
  2. +
  3. + You understand the restrictions and risks associated with taking part + in the Auction as set forth in these Terms, and acknowledge and assume + all such restrictions and risks; +
  4. +
  5. + You have a sufficient understanding of the functionality, usage, + storage, transmission mechanisms and intricacies associated with + cryptographic tokens and blockchain-based software systems; +
  6. +
  7. + You have obtained sufficient information about the Trustlines + Protocol, the Trustlines Network, the Auction, the Validator technical + capabilities, the Trustlines Blockchain and the Foundation to make an + informed decision whether or not to take part in the Auction; +
  8. +
  9. + You understand and accept that none of the information contained in + these Terms is intended to form the basis for a solicitation or + recommendation for an investment of any kind whatsoever; +
  10. +
  11. + You understand and accept that you waive your rights to have any + Dispute resolved in a court, and that you waive your rights to a jury + trial. Instead, any Disputes will be settled through amicable + negotiations and/or through binding arbitration. +
  12. +
  13. + You are not registered on any of the following sanction lists; United + Nations Sanctions (UN), US Consolidated Sanctions OFAC, Specially + Designated Nationals (SDN), EU Financial Sanctions, UK Financial + Sanctions (HMT), Australian Sanctions, Switzerland Sanction List - + SECO, INTERPOL Wanted List, Consolidated Canadian Autonomous Sanctions + List, Office of the Superintendent of Financial Institutions (Canada), + Bureau of Industry and Security (US), Department of State, AECA + Debarred List (US), Department of State, Nonproliferation Sanctions + (US), or any other internationally recognised sanction list. +
  14. +
  15. + Any and all of your actions, whether directly or indirectly related to + the Auction, the Validator technical capabilities, the Validator Slots + and the Trustlines Protocol comply with applicable laws and + regulations in your jurisdiction, including, but not limited to, (i) + legal capacity and any other threshold requirements in your + jurisdiction (ii) and any foreign exchange or regulatory restrictions + applicable to such actions. +
  16. +
+ +

Review and testing of the Smart Contracts

+ +

+ The Smart Contracts have been, on a reasonable effort basis, reviewed, + tested, and approved by technical experts. The technical experts have + confirmed to the Foundation that the Auction Smart Contracts have, with + regard to both accuracy and security, been programmed according to the + current state of the art. The Validator Candidate, however, understands + and accepts that smart contract technology is still in an early + development stage and its application is of an experimental nature that + carries significant operational, technological, financial, regulatory, + and reputational risks. Accordingly, while the conducted review and + testing raises the level of security and accuracy, in theory, the + Validator Candidate understands and accepts that the review and testing + does not amount to any form of warranty, including direct or indirect + warranties that the Smart Contracts are fit for a particular purpose or + do not contain any weaknesses, vulnerabilities or bugs which could + cause, inter alia, the complete loss of the Ether sent to the Smart + Contracts and/or held in the Smart Contract. +

+ +

Associated risks

+ +

+ By participating in the Auction, you expressly acknowledge and assume + the following risks: +

+ +
    +
  • + Risk​ ​of​ ​losing​ a Validator Slot, TLN bid or Stake​ due​ ​to​ + ​loss​ ​of​ ​private​ ​key(s); +
  • +
  • + Risks​ ​associated​ ​with​ ​the​ ​Ethereum​ blockchain: any + malfunction, breakdown or abandonment of the Ethereum blockchain may + have a material adverse effect on the Smart Contracts; +
  • +
  • + Risk​ ​of​ ​mining​ ​attacks: the Auction Smart Contract is + susceptible to attacks by miners in the course of validating bids on + the Ethereum blockchain, including, but not limited, to double-spend + attacks, majority mining power attacks, and selfish-mining attacks. + Any successful attacks present a risk to the Auction including, but + not limited to, accurate execution and recording of bids; +
  • +
  • + Risk​ ​of​ ​hacking​ ​and​ ​security​ ​weaknesses: hackers or other + malicious groups or organizations may attempt to interfere with the + Auction in a variety of ways, including, but not limited to, malware + attacks, denial of service attacks, consensus-based attacks, Sybil + attacks, smurfing, and spoofing; +
  • +
  • + Risk​ ​of​ ​uninsured​ ​Losses: unlike bank accounts or accounts at + financial institutions, the TLN held in the Auction Smart Contract is + uninsured unless you specifically obtain private insurance to insure + them. Thus, in the event of loss or loss of utility value, there is no + public insurer or private insurance arranged by the Foundation, to + offer recourse to you;{" "} +
  • +
  • + Risks​ ​associated​ ​with​ ​uncertain​ ​regulations​ ​and​ + ​enforcement​ ​actions: the regulatory status of, including but not + limited to the Auction, the Validator Slots, and distributed ledger + technology is unclear or unsettled in many jurisdictions. It is + difficult to predict how or whether regulatory agencies may apply + existing regulation with respect to such technology and its + applications. It is likewise difficult to predict how or whether + legislatures or regulatory agencies may implement changes to law and + regulations affecting distributed ledger technology and its + applications, including but not limited to the Auction and the + Validator Slots. Regulatory actions could negatively impact the whole + Trustlines Protocol in various ways, including, for purposes of + illustration only, that some or all of the parties involved in the + Trustlines Protocol in general might require licensing or is subject + to existing legislation; +
  • +
  • + Risks​ ​arising​ ​from​ ​taxation; the tax characterization of + acquiring a Validator Slot and/or acting as a Validator is uncertain. + You must seek your own tax advice in connection with your partaking in + the Auction, which may result in adverse tax consequences to you, + including withholding taxes, income taxes, and tax reporting + requirements. +
  • +
  • + Unanticipated​ ​risks: blockchain technology and the Trustlines + Protocol are a new and limitedly tested technology. In addition to the + risks referred to above, there are other risks associated with your + partaking in the Trustlines Auction, including unanticipated risks. + Such risks may further materialize as unanticipated variations or + combinations of the risks previously referred to. +
  • +
+ +

Indemnification

+ +

+ To the fullest extent permitted by applicable law, you will indemnify, + defend and hold harmless the Foundation and its Affiliates from and + against all claims, demands, actions, damages, losses, costs and + expenses (including attorneys’ fees) that arise from or relate to (i) + you participating in the Auction; (ii) your obligations, representations + and warranties under these Terms, (iii) your violation of these Terms, + and/or (iv) your violation of any rights of any other person or entity, + directly and indirectly, related to the Auction. +

+ +

Exclusion of Warranties

+ +

+ To the full extent permitted by law, no warranty, guarantee or similar + assurance whatsoever is expressed or implied with regard to the + Trustlines Protocol and its components. The smart contracts are used at + the sole risk of the User (Users included but not limited to a Vaidator + Candidate) and on an ‘as is’, ‘under development’ and ‘as available’ + basis. +

+ +

+ Participating in the Auction and thereby agreeing to these Terms does + neither entail a right nor an entitlement in any way whatsoever to + either becoming and/or remaining a Validator or receiving rewards and/or + fees. +

+ +

No Reliance

+ +

+ The Validator Candidate has had an opportunity to (i) review these + Terms, (ii) ask questions and receive answers from the Foundation + concerning these Terms and the Auction, and (iii) obtain any additional + information concerning the Auction, the Smart Contracts and the + Foundation to the extent necessary for Validator Candidate in order to + make an informed decision to enter into these Terms and to take part in + the Auction. The Validator Candidate acknowledges that in making a + decision to take part in the Auction, the Validator Candidate has relied + solely upon these Terms and independent investigations made by the + Validator Candidate. The Validator Candidate is not relying on the + Foundation with respect to the legal, tax and other economic factors + involved in entering into these Terms and understands that it is solely + responsible for reviewing the legal, tax and other economic + considerations involved with taking part in the Auction with its own + legal, tax and other advisers. +

+ +

Limitation of Liability

+ +

+ The Validator Candidate acknowledges and agrees that, to the fullest + extent permitted by any applicable law, it will not hold the Foundation + and its Affiliates liable for any direct, indirect, special, incidental, + consequential or exemplary damages (including but not limited to loss of + income, revenue and profits, or goodwill, or data) or injury whatsoever + caused by or related to his/her partaking (or inability to partake) in + the Auction or the use (or inability to use) of the Smart Contracts + under any cause of action whatsoever of any kind in any jurisdiction. +

+

+ The Validator Candidate acknowledges and agrees that, to the fullest + extent permitted by any applicable law, the risk of taking part in the + Auction rests entirely with the Validator Candidate.{" "} +

+

+ The limitations set forth in the Clause do not and will not limit or + exclude liability of the Foundation for fraud or intentional, willful or + reckless misconduct. +

+ +

Release

+ +

+ To the fullest extent permitted by applicable law, the Validator + Candidate releases the Foundation and its Affiliates from + responsibility, liability, claims, demands and/or damages (actual and + consequential) of every kind and nature, known and unknown (including, + but not limited to, claims of negligence), arising out of or related to + disputes between the Parties and out of or related to the acts or + omissions of any third parties. +

+ +

Disputes: Binding Arbitration

+ +

+ Any Dispute shall first be endeavoured to be settled through amicable + negotiations in good faith by the Parties. If the Validator Candidate + and the Foundation cannot agree how to resolve the Dispute within thirty + (30) days after the date a notice is received by the applicable Party, + then either you or the Foundation may, as appropriate, commence + arbitration proceedings. The Dispute shall subsequently (and + exclusively) be submitted to three arbitrators. The nomination of + arbitrators and the rules of arbitration shall be in accordance with the + Rules of Arbitration of Liechtenstein (“Schiedsordnung der + Liechtensteinischen Industrie- und Handelskammer”). The seat of the + arbitral tribunal shall be Ruggell, Liechtenstein. Language of the + proceedings shall be English. The arbitral award is final and binding + upon the parties. The arbitration fees will be borne by the losing party + unless otherwise awarded by the arbitral tribunal. The Parties undertake + to carry out the arbitral award in accordance with the modalities of + said reward. +

+ +

Notices

+ +

+ Notice to the Foundation shall be sent by email to the Foundation at + contact@ trustlines.foundation +

+

+ Notice to you shall be by email to the email address you provide to us. +

+

+ Your notice must include (i) your name, postal address, email address + and telephone number, (ii) a description in reasonable detail of the + nature or basis of any possible Dispute, and (iii) the specific relief + that you are seeking. +

+ +

Governing Law and Venue

+ +

+ These Terms will be governed by and construed in accordance with the + laws of Liechtenstein, without regard to conflict of law rules or + principles that would cause the application of the laws of any other + jurisdiction.{" "} +

+

+ Any Dispute that is not or cannot be subject to arbitration will be + resolved by the courts of Liechtenstein. +

+ +

Severability

+ +

+ If any term, clause or provision of these Terms is held unlawful, void + or unenforceable, then that term, clause or provision will be severable + from these Terms and will not affect the validity or enforceability of + any remaining part of that term, clause or provision, or any other term, + clause or provision of these Terms. Such unlawful, void or unenforceable + clause or provisions shall be replaced by valid and enforceable clause + or provisions, which most closely achieve the commercial intent and + purpose of this Agreement. +

+ +

Miscellaneous

+ +

+ These Terms constitute the entire agreement between the Parties relating + to your acquisition of Auction from the Foundation and supersede any + other agreements, statements or information provided by the Foundation + and/or its Affiliates. the Foundation may assign its rights and + obligations under these Terms. the Foundation’s failure to exercise or + enforce any right or provision of these Terms will not be construed or + understood as a waiver of such right or provision. the Foundation will + not be liable for any delay or failure to perform any obligation under + these Terms where the delay or failure results from any cause beyond our + reasonable control. This Agreement and the (trans)actions envisaged + therein does not create any form of partnership, joint venture, or any + other similar relationship between the Parties. Except as otherwise + provided herein, these Terms are intended solely for the benefit of the + Parties and are not intended to confer third-party beneficiary rights + upon any other person or entity. +

+ +

 

+

 

+
+ ); +} diff --git a/src/features/auction/components/bid-screens/AcceptTermsAndConditions.js b/src/features/auction/components/bid-screens/AcceptTermsAndConditions.js new file mode 100644 index 0000000..adb4eb2 --- /dev/null +++ b/src/features/auction/components/bid-screens/AcceptTermsAndConditions.js @@ -0,0 +1,59 @@ +import React, { useCallback, useState } from "react"; +import ActionButton from "../bid-components/ActionButton"; +import TermsAndConditionsModal from "../modal"; +import TermsAndConditions from "../terms"; +import { Card } from "../../../common/components/card"; + +export default function AcceptTermsAndConditions({ onAccept, onReject }) { + const [isVisibleTermsAndCondition, setIsVisibleTermsAndCondition] = useState( + false + ); + + const showTermsAndConditionsModal = useCallback( + () => setIsVisibleTermsAndCondition(true), + [] + ); + + const acceptTermsAndCondition = useCallback(async () => { + setIsVisibleTermsAndCondition(false); + onAccept && onAccept(); + }, [onAccept]); + + const rejectTermsAndCondition = useCallback(() => { + setIsVisibleTermsAndCondition(false); + onReject && onReject(); + }, [onReject]); + + return ( +
+
+ + 1 + +

+ Connect to a Wallet +

+
+
+ To proceed you must read and accept our terms & conditions. +
+
+ { + showTermsAndConditionsModal(true); + }} + /> +
+ {isVisibleTermsAndCondition && ( + + + + )} +
+ ); +} diff --git a/src/features/auction/components/bid-screens/AlreadyBid.js b/src/features/auction/components/bid-screens/AlreadyBid.js new file mode 100644 index 0000000..79602dd --- /dev/null +++ b/src/features/auction/components/bid-screens/AlreadyBid.js @@ -0,0 +1,25 @@ +import React, { useEffect, useState } from "react"; +import { formatTLNAmount } from "../../../common/utils/math"; +import Error from "../bid-components/Error"; +import * as auctionWeb3 from "../../api/web3"; + +export default function AlreadyBid({ web3Account }) { + const [paidSlotPrice, setPaidSlotPrice] = useState(0); + + useEffect(() => { + async function fetchPaidSlotPrice() { + const paidSlotPrice = await auctionWeb3.getPaidSlotPriceByAddress( + web3Account + ); + setPaidSlotPrice(paidSlotPrice); + } + fetchPaidSlotPrice(); + }, [web3Account]); + + return ( + + The selected account {web3Account} has already bid{" "} + {formatTLNAmount(paidSlotPrice)}. You can only bid once in the auction. + + ); +} diff --git a/src/features/auction/components/bid-screens/ConnectWallet.js b/src/features/auction/components/bid-screens/ConnectWallet.js new file mode 100644 index 0000000..781f9b1 --- /dev/null +++ b/src/features/auction/components/bid-screens/ConnectWallet.js @@ -0,0 +1,22 @@ +import React from "react"; +import MessageBlock from "../bid-components/MessageBlock"; +import ActionButton from "../bid-components/ActionButton"; + +export default function ConnectWallet({ onConnect }) { + return ( +
+
+ + To proceed, connect a compatible Web3 wallet. + +
+
+ +
+
+ ); +} diff --git a/src/features/auction/components/bid-screens/DepositPending.js b/src/features/auction/components/bid-screens/DepositPending.js new file mode 100644 index 0000000..ee5bb3b --- /dev/null +++ b/src/features/auction/components/bid-screens/DepositPending.js @@ -0,0 +1,20 @@ +import React from "react"; +import MessageBlock from "../bid-components/MessageBlock"; +import Screen from "./Screen"; + +export default function DepositPending() { + return ( + + + The auction is ended, but the deposits are not yet locked. +
+ Please wait a few minutes for the deposits to be safely locked before + you can withdraw your overbid from the auction. +
+
+
+ ); +} diff --git a/src/features/auction/components/bid-screens/Loading.js b/src/features/auction/components/bid-screens/Loading.js new file mode 100644 index 0000000..9d7ec74 --- /dev/null +++ b/src/features/auction/components/bid-screens/Loading.js @@ -0,0 +1,15 @@ +import React from "react"; + +function Loading({ title, children }) { + return ( +
+

{title}

+
+
+
+
{children}
+
+ ); +} + +export default Loading; diff --git a/src/features/auction/components/bid-screens/MakeBid.js b/src/features/auction/components/bid-screens/MakeBid.js new file mode 100644 index 0000000..938921d --- /dev/null +++ b/src/features/auction/components/bid-screens/MakeBid.js @@ -0,0 +1,19 @@ +import React from "react"; +import MessageBlock from "../bid-components/MessageBlock"; +import ActionButton from "../bid-components/ActionButton"; +import Screen from "./Screen"; +import AuctionLink from "../bid-components/AuctionLink"; +import CurrentPrice from "../current-price"; + +export default function MakeBid({ makeBid }) { + return ( + + + You can now make a bid in the{" "} + Trustlines Validator Auction.
+ Current slot price is . +
+ +
+ ); +} diff --git a/src/features/auction/components/bid-screens/NoAllowance.js b/src/features/auction/components/bid-screens/NoAllowance.js new file mode 100644 index 0000000..7090f14 --- /dev/null +++ b/src/features/auction/components/bid-screens/NoAllowance.js @@ -0,0 +1,20 @@ +import React from "react"; +import MessageBlock from "../bid-components/MessageBlock"; +import ActionButton from "../bid-components/ActionButton"; +import Screen from "./Screen"; +import TLNLink from "../bid-components/TLNLink"; +import AuctionLink from "../bid-components/AuctionLink"; + +export default function NoAllowance({ web3Account, approve }) { + return ( + + + To proceed, you must approve the transfer of{" "} + Trustlines Network Tokens on your behalf by the{" "} + auction contract. You are currently using the + address {web3Account} to approve the transfer. + + + + ); +} diff --git a/src/features/auction/components/bid-screens/NotStarted.js b/src/features/auction/components/bid-screens/NotStarted.js new file mode 100644 index 0000000..788b8f3 --- /dev/null +++ b/src/features/auction/components/bid-screens/NotStarted.js @@ -0,0 +1,20 @@ +import React from "react"; +import Error from "../bid-components/Error"; +import moment from "moment-timezone"; + +const DATE_FORMAT = "MMM D, YYYY, h:mm:ss a"; + +export default function NotStarted() { + const timestamp = process.env.AUCTION_START_TIMESTAMP; + return ( + + Please be a little more patient, the auction has not started yet.
+ The auction will start on{" "} + {moment + .unix(timestamp) + .tz("UTC") + .format(DATE_FORMAT)}{" "} + UTC ({moment.unix(timestamp).format(DATE_FORMAT)} {moment.tz.guess()}) +
+ ); +} diff --git a/src/features/auction/components/bid-screens/NothingToWithdraw.js b/src/features/auction/components/bid-screens/NothingToWithdraw.js new file mode 100644 index 0000000..b46b587 --- /dev/null +++ b/src/features/auction/components/bid-screens/NothingToWithdraw.js @@ -0,0 +1,13 @@ +import React from "react"; +import MessageBlock from "../bid-components/MessageBlock"; +import AuctionLink from "../bid-components/AuctionLink"; + +export default function NothingToWithdraw() { + return ( + + You do not have anything to withdraw from the{" "} + Trustlines Validator Auction.
+ You are good to go! +
+ ); +} diff --git a/src/features/auction/components/bid-screens/Screen.js b/src/features/auction/components/bid-screens/Screen.js new file mode 100644 index 0000000..fd8bf45 --- /dev/null +++ b/src/features/auction/components/bid-screens/Screen.js @@ -0,0 +1,11 @@ +import React from "react"; +import MainHeader from "../bid-components/MainHeader"; + +export default function Screen({ title, children, faIcon }) { + return ( +
+ + {children} +
+ ); +} diff --git a/src/features/auction/components/bid-screens/SuccessfulBid.js b/src/features/auction/components/bid-screens/SuccessfulBid.js new file mode 100644 index 0000000..cbba79d --- /dev/null +++ b/src/features/auction/components/bid-screens/SuccessfulBid.js @@ -0,0 +1,30 @@ +import React from "react"; +import MessageBlock from "../bid-components/MessageBlock"; +import Screen from "./Screen"; +import { formatTLNAmount } from "../../../common/utils/math"; +import * as blockexplorer from "../../../common/utils/blockexplorer"; + +export default function SuccessfulBid({ txHash, paidSlotPrice }) { + return ( + + + {`You submitted a bid for a slot price of ${formatTLNAmount( + paidSlotPrice + )}`} +
+ You can check your transaction on{" "} + + Etherscan + + . +
+
+ ); +} diff --git a/src/features/auction/components/bid-screens/SuccessfulWithdraw.js b/src/features/auction/components/bid-screens/SuccessfulWithdraw.js new file mode 100644 index 0000000..72a9ab2 --- /dev/null +++ b/src/features/auction/components/bid-screens/SuccessfulWithdraw.js @@ -0,0 +1,23 @@ +import React from "react"; +import MessageBlock from "../bid-components/MessageBlock"; +import * as blockexplorer from "../../../common/utils/blockexplorer"; + +export default function SuccessfulWithdraw({ txHash }) { + return ( + + You have successfully withdrawn your overbid from the auction +
+ You can check your transaction on{" "} + + Etherscan + + .
+ You are good to go! +
+ ); +} diff --git a/src/features/auction/components/bid-screens/TransactionError.js b/src/features/auction/components/bid-screens/TransactionError.js new file mode 100644 index 0000000..7b3c368 --- /dev/null +++ b/src/features/auction/components/bid-screens/TransactionError.js @@ -0,0 +1,25 @@ +import React from "react"; +import MessageBlock from "../bid-components/MessageBlock"; +import ActionButton from "../bid-components/ActionButton"; +import Screen from "./Screen"; +import * as blockexplorer from "../../../common/utils/blockexplorer"; + +export default function TransactionError({ errorMessage, txHash, onTryAgain }) { + return ( + + + {errorMessage}
+ Check what went wrong on{" "} + + Etherscan + + . +
+ +
+ ); +} diff --git a/src/features/auction/components/bid-screens/WaitForConfirmation.js b/src/features/auction/components/bid-screens/WaitForConfirmation.js new file mode 100644 index 0000000..c494ee5 --- /dev/null +++ b/src/features/auction/components/bid-screens/WaitForConfirmation.js @@ -0,0 +1,24 @@ +import React from "react"; +import MessageBlock from "../bid-components/MessageBlock"; +import Screen from "./Screen"; +import * as blockexplorer from "../../../common/utils/blockexplorer"; + +export default function WaitForConfirmation({ txHash }) { + return ( + + + Your transaction has been sent and we are waiting for confirmation. +
+ You can check the status on{" "} + + Etherscan + + . +
+
+ ); +} diff --git a/src/features/auction/components/bid-screens/WithdrawBid.js b/src/features/auction/components/bid-screens/WithdrawBid.js new file mode 100644 index 0000000..9c503e0 --- /dev/null +++ b/src/features/auction/components/bid-screens/WithdrawBid.js @@ -0,0 +1,33 @@ +import React, { useEffect, useState } from "react"; +import MessageBlock from "../bid-components/MessageBlock"; +import ActionButton from "../bid-components/ActionButton"; +import Screen from "./Screen"; +import AuctionLink from "../bid-components/AuctionLink"; +import { formatTLNAmount } from "../../../common/utils/math"; +import * as auctionWeb3 from "../../api/web3"; + +export default function WithdrawBid({ withdraw, account }) { + const [valueToWithdraw, setValueToWithdraw] = useState(undefined); + useEffect(() => { + async function fetchValueToWithdraw() { + const value = await auctionWeb3.fetchValueToWithdraw(account); + setValueToWithdraw(value); + } + fetchValueToWithdraw(); + }, [account]); + + return ( + + + You can withdraw your overbid from the{" "} + Trustlines Validator Auction.
+ {valueToWithdraw + ? "You have " + + formatTLNAmount(valueToWithdraw.toString()) + + " to withdraw" + : ""} +
+ +
+ ); +} diff --git a/src/features/auction/components/chart-key.jsx b/src/features/auction/components/chart-key.jsx new file mode 100644 index 0000000..eba9a17 --- /dev/null +++ b/src/features/auction/components/chart-key.jsx @@ -0,0 +1,18 @@ +import React from "react"; +import { Card } from "../../common/components/card"; +export function ChartKey(props) { + return ( + +
+
+
+
Price
+
+
+
+
Bid
+
+
+
+ ); +} diff --git a/src/features/auction/components/chart-legend.jsx b/src/features/auction/components/chart-legend.jsx new file mode 100644 index 0000000..37388d5 --- /dev/null +++ b/src/features/auction/components/chart-legend.jsx @@ -0,0 +1,49 @@ +import React from "react"; + +import { CurrentPrice } from "./current-price"; +import { Status } from "./status"; +import { SlotBox } from "./slot-box"; +import { Card } from "../../common/components/card"; + +export function ChartLegend({ chartState = {} }) { + const { + state: status, + lowestSlotPriceInWEI, + currentPriceInWEI, + initialPriceInWEI, + freeSlotsCount, + maxSlotsCount, + takenSlotsCount, + remainingSeconds, + secondsBeforeStart, + } = chartState; + + return ( +
+ +
+
+ +
+
+ +
+
+ + + +
+
+
+
+ ); +} diff --git a/src/features/auction/components/chart.jsx b/src/features/auction/components/chart.jsx new file mode 100644 index 0000000..14535de --- /dev/null +++ b/src/features/auction/components/chart.jsx @@ -0,0 +1,26 @@ +import React from "react"; + +import initChart from "../api/chart"; +import { ChartKey } from "./chart-key"; + +export function Chart(props) { + React.useEffect(() => { + if (props.chartState) { + initChart(props.chartState); + } + }, [props.chartState]); + + return ( +
+
+ +
+
+
+ +
+
+
+
+ ); +} diff --git a/src/features/auction/components/current-price.jsx b/src/features/auction/components/current-price.jsx new file mode 100644 index 0000000..54e36d2 --- /dev/null +++ b/src/features/auction/components/current-price.jsx @@ -0,0 +1,54 @@ +import React from "react"; + +import { formatTLNAmount } from "../../common/utils/math"; + +export function CurrentPrice({ + status, + lowestSlotPriceInWEI, + currentPriceInWEI, + initialPriceInWEI, +}) { + const [label, value] = getCurrentPriceLabelAndValue({ + status, + lowestSlotPriceInWEI, + currentPriceInWEI, + initialPriceInWEI, + }); + + if (!label) { + return null; + } + + return ( +
+
{label}
+
+ {value} +
+
+ ); +} + +function getCurrentPriceLabelAndValue({ + status, + lowestSlotPriceInWEI, + currentPriceInWEI, + initialPriceInWEI, +}) { + let label, value; + + if (status === "Finished" || status === "Deposit Pending") { + label = "Lowest Slot Price"; + value = lowestSlotPriceInWEI; + } else if (status === "Started") { + label = "Current Slot Price"; + value = currentPriceInWEI; + } else if (status === "Not Started") { + label = "Initial Slot Price"; + value = initialPriceInWEI; + } else { + return []; + } + + return [label, formatTLNAmount(value)]; +} diff --git a/src/features/auction/components/hero.jsx b/src/features/auction/components/hero.jsx new file mode 100644 index 0000000..1e1ff2f --- /dev/null +++ b/src/features/auction/components/hero.jsx @@ -0,0 +1,51 @@ +import React from "react"; + +import auctionGraph from "../images/auction-graph.svg"; +import { Card } from "../../common/components/card"; +import { ChartKey } from "./chart-key"; + +export function AuctionHero() { + return ( +
+
+
+
+

+ The Trustlines +
+ Validator Auction +

+

+ The auction is used to determine the validator set for the + Trustlines Blockchain. The set is valid for nine months. To + participate in the auction,{" "} + + a whitelisted Ethereum address + {" "} + has to bid with Trustlines Network Tokens (TLN) after the auction + has started and before it has ended. +

+
+
+
+ + Please be sure to not send TLN directly to the auction contract! + +
+
+
+ +
+
+ auction graph +
+
+
+
+ ); +} diff --git a/src/features/auction/components/modal.jsx b/src/features/auction/components/modal.jsx new file mode 100644 index 0000000..10f377b --- /dev/null +++ b/src/features/auction/components/modal.jsx @@ -0,0 +1,72 @@ +import React, { useState, useEffect } from "react"; +import { Button } from "../../common/components/button"; + +const SCROLL_THRESHOLD = 5; + +const elementScrolledToBottom = element => { + return ( + element.scrollHeight - element.scrollTop <= + element.clientHeight + SCROLL_THRESHOLD + ); +}; + +function TermsAndConditionsModal({ onReject, onAccept, children }) { + const [scrolledToModalBottom, setScrolledToModalBottom] = useState(false); + const termsAndConditionsModalReference = React.createRef(); + + const checkAndSetScrolledToModalBottom = () => { + if (elementScrolledToBottom(termsAndConditionsModalReference.current)) { + setScrolledToModalBottom(true); + } + }; + + useEffect(() => { + checkAndSetScrolledToModalBottom(); + window.addEventListener("resize", checkAndSetScrolledToModalBottom); + return () => { + window.removeEventListener("resize", checkAndSetScrolledToModalBottom); + }; + }); + + return ( + <> +
+
+
+
+
+

+ Terms and Conditions +

+
+
+
+ {children} +
+
+
+
+
+
+
+ + ); +} + +export default TermsAndConditionsModal; diff --git a/src/features/auction/components/slot-box.jsx b/src/features/auction/components/slot-box.jsx new file mode 100644 index 0000000..a496755 --- /dev/null +++ b/src/features/auction/components/slot-box.jsx @@ -0,0 +1,14 @@ +import React from "react"; + +export function SlotBox({ label, value }) { + return ( +
+
+ {value} +
+
+ {label} +
+
+ ); +} diff --git a/src/features/auction/components/status.jsx b/src/features/auction/components/status.jsx new file mode 100644 index 0000000..36f31ed --- /dev/null +++ b/src/features/auction/components/status.jsx @@ -0,0 +1,45 @@ +import React from "react"; + +export function Status({ status, remainingSeconds, secondsBeforeStart }) { + const [timeString, label] = getTimeStringAndLabel({ + status, + remainingSeconds, + secondsBeforeStart, + }); + + return ( +
+ {timeString &&
Auction {timeString}
} + {label &&
{label}
} +
+ ); +} + +function getTimeStringAndLabel({ + status, + remainingSeconds, + secondsBeforeStart, +}) { + let timeString = status; + let label; + if (timeString === "Started") { + timeString = formatSecondsToString(remainingSeconds); + label = "Remaining Time"; + } else if (timeString === "Not Started" && secondsBeforeStart > 0) { + timeString = formatSecondsToString(secondsBeforeStart); + label = "Until Auction starts"; + } + + return [timeString, label]; +} + +function formatSecondsToString(seconds) { + if (!seconds) { + return undefined; + } + const s = seconds % 60; + const m = Math.floor((seconds % 3600) / 60); + const h = Math.floor((seconds % 86400) / 3600); + const d = Math.floor(seconds / 86400); + return `${d}d ${h}h ${m}m ${s}s`; +} diff --git a/src/features/auction/components/terms.jsx b/src/features/auction/components/terms.jsx new file mode 100644 index 0000000..51fdd88 --- /dev/null +++ b/src/features/auction/components/terms.jsx @@ -0,0 +1,751 @@ +import React from "react"; + +// The terms and conditions are duplicated in src/view/auction/_terms-conditions.njk +export default function TermsAndConditions() { + return ( +
+

Background

+

The Auction

+ +

+ The Trustlines Validator auction is a Dutch auction held to discover the + fair opportunity cost value, i.e. price of a Validator Slot. The + Ethereum address of successful bidders in a successful Auction will be + included in the Validator Set. +

+ +
    +
  1. + Acceptable tokens for a bid: The Auction is a smart + contract on the Ethereum Mainchain. Note that the smart contract will + not accept any other tokens than the Trustlines Network Token as a + bid. +
  2. +
  3. + Participation: To be able to participate in the + Auction, your Ethereum address must have been whitelisted by the + Foundation prior to the start of the Auction. Every whitelisted + Ethereum address can participate in the auction only once.{" "} + + Do not send your Trustlines Network Tokens to the auction contract + since this will result in you losing your tokens. Instead, use the + bid feature on the Trustlines auction page. + +
  4. +
  5. + Timing: The Auction ends after 14 days starting from + the Start Date or when the 36th successful bid is received by the + smart contract. Whichever comes first. +
  6. +
  7. + Threshold: If fewer than 15 Ethereum addresses have + successfully participated at the time the Auction ends, it will fail. + No Validator slots will be awarded and all participants can withdraw + their bids. If 15 or more successful bids were received, the Auction + will succeed. +
  8. +
  9. + Limited Validator Slots: 36 Validator Slots are up + for auction. +
  10. +
+ +

How the Auction works

+ +

+ The Auction begins when a transaction is sent by the Foundation at the + Start Date. After this transaction, the smart contract operates + independently and the Foundation will neither have nor retain any + control or influence over it. +

+ +

+ Once initialized, the Auction starts at a price of 50,000,000 Trustlines + Network Tokens per Validator Slot. Then the price will continuously + decrease over time as defined by the following parameters & functions: +

+ +
+        t = elapsed_time_in_ms / auction_duration_in_days, price = price_start *
+        (1+t) / (1+t+decay), decay = (t^3) / 746571428571
+      
+ +

whereby:

+ +
    +
  • + elapsed_time_in_ms is the elapsed time in ms since + the auction started +
  • +
  • + auction_duration_in_days equals 14 days +
  • +
  • + price_start equals 50,000,000 Trustlines Network + Tokens +
  • +
+ +

+ When the price falls to a point that a Validator Candidate thinks is + fair, they can submit their bid over the trustlines auction{" "} + page. If and when + the transaction is successfully included in the Ethereum Mainchain, the + Validator Candidate has obtained a Validator Slot in the event of a + successful Auction (see Threshold). +

+

+ If the Auction is successful, all Validator Slots are priced equal, i.e. + being equal to the price of the last successful bid received by the + smart contract. Every participant can withdraw the difference between + the end Validator Slot price and their bid. +

+

+ The final Validator Slot price of each Validator Slot cannot be + withdrawn from the smart contract and will be transferred to the Deposit + Locker (more info in the definition of the Validator Auction) smart + contract and kept there as a Stake for 9 months starting from the date + the Auction ends (see Timing). Once the 9 months have passed, Validators + can withdraw their Stake provided it doesn’t get slashed due to + equivocation. +

+ +

Validator technical capabilities

+ +

+ If a Validator Candidate successfully bid for a slot in the Auction, + he/she will be included in the Validator Set. This will allow the + Validator to run a validator node which can create blocks on the + Trustlines Blockchain. Additionally to validating and creating blocks + (including the genesis block) they will also be able to operate the + Trustlines Bridge which is used to exchange Trustlines Network Tokens + from the Ethereum Mainchain to Trustlines Network Coins on the + Trustlines Blockchain. Other than these described capabilities there are + no other unique capabilities connected solely to a Validator slot.{" "} +

+ +

Why participate

+ +

+ The Trustlines Blockchain is a minimal viable Proof of Stake sidechain + to the Ethereum Mainchain and is governed by a greenfield governance + model in which Validators can gain technical and governance experience + with such an experimental consensus model. +

+

+ Besides the Validator technical capabilities mentioned above, Validators + will also receive block rewards (as to be defined within the chain spec) + in the form of Trustlines Network Coins if they actively run their + validator node and take part in block creation. They will also be in a + position to charge transaction fees for transactions that are included + within the blocks a Validator creates. +

+

+ Note that receiving block rewards and transaction fees is dependent on a + community of Validators emerging and the potential users of the + Trustlines Blockchain. The Foundation cannot guarantee that a Trustlines + Blockchain including the Chain Spec as proposed by the Foundation + actually emerges out of the actions of these third party stakeholders. + Hence, participating in the Auction and thereby agreeing to these Terms + and Conditions does not entail a right or an entitlement in any way + whatsoever to either becoming and/or remaining a Validator or receiving + block rewards and/or transaction fees. +

+ +

General remarks, recommendations and good practice

+ +

+ Bidding Trustlines Network Tokens in the Auction does not guarantee a + Validator Slot. Especially, your bid/transaction will be rejected: +

+ +
    +
  • If you bid from an Ethereum address that isn’t whitelisted
  • +
  • If you bid less than the current Auction price
  • +
  • + If you bid before the Auction has started or after the auction has + ended +
  • +
  • + If you have already bid in the Auction (multiple bids will not result + in more than one slot. Only 1 slot per Ethereum address). +
  • +
+ +

Additionally:

+ +
    +
  • + The Auction might not meet the threshold of 15bidders, in which case + the Auction has failed and the Trustlines Network Tokens can be + withdrawn from the smart contract by its respective Auction + participant. +
  • +
  • + The current Validators might choose not to fork the Trustlines + Blockchain with the new validator set. This requires at least 50% of + the current Validator Set to do so. +
  • +
  • + Users of the Trustlines Blockchain or projects building on top of it + might align on a different Validator Set, therefore choosing a + different fork of the Trustlines Blockchain rendering the Validator + Set determined by the auction obsolete. Validators will still be able + to withdraw their Stake after the 9-month staking period. +
  • +
+ +

+ The functioning of the Ethereum mainchain is out of the control of the + Foundation, so the Foundation cannot be held liable for any malfunction, + breakdown, or abandonment of the Ethereum protocol and/or the Ethereum + Mainchain. +

+ +

+ All actions are voluntary, there is no obligation to take part in the + Auction. +

+ +

+ Validators can be removed from the Validator Set by the majority of the + Trustlines Blockchain community by hard forking. Validators removed from + the Validator set in such a way will neither earn block rewards nor + transaction fees going forward. The Stake on the Ethereum Mainchain will + remain locked for the remainder of the 9-month Staking period and will + be slashable if equivocation can be proved. +

+ +

Definitions

+ +

+ Affiliate(s) means any and all persons and or entities + directly or indirectly controlling, controlled by or under common + control with such person, where control may be by either management + authority, contract or equity interest.{" "} +

+ +

+ Ethereum Mainchain or Ethereum blockchain means the + smart contract protocol, virtual machine and decentralized network + including all its related components and protocol-related projects both + present and future, which​ ​began​ ​operation​ ​(Genesis​ ​Block)​ ​on​ + ​July​ ​30th,​ ​2015. +

+ +

+ Ether (ETH) means the cryptocurrency native to the + Ethereum mainchain, i.e. the blockchain token​ ​of​ ​Ethereum. +

+ +

+ Trustlines Network is short for “The Trustlines Network + ecosystem”, which is an ecosystem of entities, individuals, and code + that aims to promote financial & economic inclusion of all people + through decentralized and open source systems. +

+ +

+ Trustlines Protocol represents a set of rules, + processes and definitions forged into deployable code. It will comprise + several technical components used to calculate paths and store + transactions, i.e. smart contracts, the Trustlines Blockchain and the + code for the relay servers. It aims to provide a base layer to enable + interactions within the Trustlines Network. +

+ +

+ Trustlines Blockchain means a purpose built sidechain + to the Ethereum mainchain that is intended to serve as a database and + storage for transactions to support an implementation of the Trustlines + Protocol. +

+ +

+ Foundation means​ ​the Foundation,​ ​a​ ​Liechtenstein + non-profit​ foundation​ ​registered​ ​in​ Ruggell, Liechtenstein. +

+ +

+ Final Validator slot price is the price of the last + successful bid that has been successfully sent to the auction contract + and is included in the Ethereum Mainchain. +

+ +

+ Proof of Stake (PoS) means a type of consensus + algorithm by which a cryptocurrency blockchain network aims to achieve + distributed consensus. In PoS-based cryptocurrency blockchain networks + the creator of the next block is chosen via various combinations of + random selection. In contrast, the algorithm of proof-of-work-based + cryptocurrencies such as bitcoin uses mining; that is, the solving of + computationally intensive puzzles to validate transactions and create + new blocks. +

+ +

+ Sidechain means a designation for a blockchain that + runs in parallel to a primary blockchain. Entries from the primary + blockchain can be linked to and from the sidechain; this allows the + sidechain to otherwise operate independently of the primary blockchain. +

+ +

+ Validator means an individual or entity who or which + has deployed and operates the Trustlines Blockchain client in order to + validate and relay transactions on the Trustlines Blockchain and is part + of the Validator Set. +

+ +

+ Auction means the Trustlines Validator auction governed + by these terms and conditions. This auction is being hosted via a + combination of Smart Contracts on the Ethereum Mainchain in order to + discover the fair opportunity cost value (i.e. price of a Validator + Slot) and thereby the amount of TLNto be staked in order to obtain a + Validator Slot on the Trustlines Blockchain. +

+ +

+ Validator Candidate means an individual or entity whose + Ethereum address which he/she/it has provided to the Foundation and has + been whitelisted in the Auction smart contract by the Foundation in + order to take part in the Auction. +

+ +

+ Validator Slot one of the Validator positions being + auctioned in this Auction. +

+ +

+ Validator Set means a list of the Ethereum addresses of + the Validator Candidates that have successfully obtained a Validator + Slot. Said list being maintained in a specific smart contract as defined + within the chain spec. +

+ +

+ Terms mean these terms and conditions forming the + Agreement between you, the Validator Candidate and the Foundation. +

+ +

+ Disputes means any dispute, claim, suit, action,cause + of action, demand or proceeding arising out​ ​or​ ​in​ ​connection​ + ​with​ ​this​ ​Agreement,​ ​or​ ​the​ ​breach,​ ​termination​ ​or​ + ​invalidity​ ​thereof. +

+ +

+ Stake means the amount of TLN deposited in the Deposit + Locker smart contract and equaling the final Auction Validator Slot + price. +

+ +

+ + + Trustlines Bridge + + {" "} + means a combination of one smart contract deployed on the Ethereum + Mainchain, one smart contract deployed on the Trustlines Blockchain, and + a tool (tlbc - bridge) that Validators run. It allows to transfer + Trustlines Network Tokens from the Ethereum Mainchain into the + Trustlines chain as native Trustlines Network Coins. The exact code and + specifications of the Trustlines Bridge are still in development. +

+ +

+ + + Trustlines Network Tokens (TLN) + + {" "} + means tokens within the ERC20{" "} + + TLN token + {" "} + contract on the Ethereum Mainchain, which can be converted to TLC by + sending them to the Trustlines Bridge. +

+ +

+ Trustlines Network Coins (TLC) means similar to the + Ethereum Mainchain, the Trustlines Blockchain requires transaction fees + to be paid in a cryptocurrency native to the blockchain. These tokens + will be called Trustlines Network Coins. The exact code and + specifications of the TLC is still in development. +

+ +

+ Chain spec means a JSON file which specifies rules of a + blockchain. +

+ +

+ JSON means an open-standard file format that uses + human-readable text to transmit data objects consisting of + attribute-value pairs and array data types. +

+ +

+ Smart Contract(s) means the specific smart contracts + used for this Auction as can be viewed on{" "} + + https://etherscan.io/address/{process.env.AUCTION_ADDRESS} + +

+ +

+ Equivocation We define equivocation of a (present, + former, or future) validator as using their private key to sign two or + more different blocks with the same step number, irrespective of the + block's height, validity, or other fields. +

+ +

+ Step number/time slot Validators are assigned time + slots in a{" "} + + round robin + {" "} + fashion in which they can produce a block which increases the Blockchain + length +

+ +

Acceptance of terms and conditions

+ +

+ By participating in the Auction, you accept these terms and conditions. +

+ +

Representations and warranties of the Validator Candidate

+ +

+ By participating in the Auction, the Validator Candidate represents and + warrants that: +

+ +
    +
  1. You have read, understand and accept these Terms;
  2. +
  3. + You understand the restrictions and risks associated with taking part + in the Auction as set forth in these Terms, and acknowledge and assume + all such restrictions and risks; +
  4. +
  5. + You have a sufficient understanding of the functionality, usage, + storage, transmission mechanisms and intricacies associated with + cryptographic tokens and blockchain-based software systems; +
  6. +
  7. + You have obtained sufficient information about the Trustlines + Protocol, the Trustlines Network, the Auction, the Validator technical + capabilities, the Trustlines Blockchain and the Foundation to make an + informed decision whether or not to take part in the Auction; +
  8. +
  9. + You understand and accept that none of the information contained in + these Terms is intended to form the basis for a solicitation or + recommendation for an investment of any kind whatsoever; +
  10. +
  11. + You understand and accept that you waive your rights to have any + Dispute resolved in a court, and that you waive your rights to a jury + trial. Instead, any Disputes will be settled through amicable + negotiations and/or through binding arbitration. +
  12. +
  13. + You are not registered on any of the following sanction lists; United + Nations Sanctions (UN), US Consolidated Sanctions OFAC, Specially + Designated Nationals (SDN), EU Financial Sanctions, UK Financial + Sanctions (HMT), Australian Sanctions, Switzerland Sanction List - + SECO, INTERPOL Wanted List, Consolidated Canadian Autonomous Sanctions + List, Office of the Superintendent of Financial Institutions (Canada), + Bureau of Industry and Security (US), Department of State, AECA + Debarred List (US), Department of State, Nonproliferation Sanctions + (US), or any other internationally recognised sanction list. +
  14. +
  15. + Any and all of your actions, whether directly or indirectly related to + the Auction, the Validator technical capabilities, the Validator Slots + and the Trustlines Protocol comply with applicable laws and + regulations in your jurisdiction, including, but not limited to, (i) + legal capacity and any other threshold requirements in your + jurisdiction (ii) and any foreign exchange or regulatory restrictions + applicable to such actions. +
  16. +
+ +

Review and testing of the Smart Contracts

+ +

+ The Smart Contracts have been, on a reasonable effort basis, reviewed, + tested, and approved by technical experts. The technical experts have + confirmed to the Foundation that the Auction Smart Contracts have, with + regard to both accuracy and security, been programmed according to the + current state of the art. The Validator Candidate, however, understands + and accepts that smart contract technology is still in an early + development stage and its application is of an experimental nature that + carries significant operational, technological, financial, regulatory, + and reputational risks. Accordingly, while the conducted review and + testing raises the level of security and accuracy, in theory, the + Validator Candidate understands and accepts that the review and testing + does not amount to any form of warranty, including direct or indirect + warranties that the Smart Contracts are fit for a particular purpose or + do not contain any weaknesses, vulnerabilities or bugs which could + cause, inter alia, the complete loss of the Ether sent to the Smart + Contracts and/or held in the Smart Contract. +

+ +

Associated risks

+ +

+ By participating in the Auction, you expressly acknowledge and assume + the following risks: +

+ +
    +
  • + Risk​ ​of​ ​losing​ a Validator Slot, TLN bid or Stake​ due​ ​to​ + ​loss​ ​of​ ​private​ ​key(s); +
  • +
  • + Risks​ ​associated​ ​with​ ​the​ ​Ethereum​ blockchain: any + malfunction, breakdown or abandonment of the Ethereum blockchain may + have a material adverse effect on the Smart Contracts; +
  • +
  • + Risk​ ​of​ ​mining​ ​attacks: the Auction Smart Contract is + susceptible to attacks by miners in the course of validating bids on + the Ethereum blockchain, including, but not limited, to double-spend + attacks, majority mining power attacks, and selfish-mining attacks. + Any successful attacks present a risk to the Auction including, but + not limited to, accurate execution and recording of bids; +
  • +
  • + Risk​ ​of​ ​hacking​ ​and​ ​security​ ​weaknesses: hackers or other + malicious groups or organizations may attempt to interfere with the + Auction in a variety of ways, including, but not limited to, malware + attacks, denial of service attacks, consensus-based attacks, Sybil + attacks, smurfing, and spoofing; +
  • +
  • + Risk​ ​of​ ​uninsured​ ​Losses: unlike bank accounts or accounts at + financial institutions, the TLN held in the Auction Smart Contract is + uninsured unless you specifically obtain private insurance to insure + them. Thus, in the event of loss or loss of utility value, there is no + public insurer or private insurance arranged by the Foundation, to + offer recourse to you;{" "} +
  • +
  • + Risks​ ​associated​ ​with​ ​uncertain​ ​regulations​ ​and​ + ​enforcement​ ​actions: the regulatory status of, including but not + limited to the Auction, the Validator Slots, and distributed ledger + technology is unclear or unsettled in many jurisdictions. It is + difficult to predict how or whether regulatory agencies may apply + existing regulation with respect to such technology and its + applications. It is likewise difficult to predict how or whether + legislatures or regulatory agencies may implement changes to law and + regulations affecting distributed ledger technology and its + applications, including but not limited to the Auction and the + Validator Slots. Regulatory actions could negatively impact the whole + Trustlines Protocol in various ways, including, for purposes of + illustration only, that some or all of the parties involved in the + Trustlines Protocol in general might require licensing or is subject + to existing legislation; +
  • +
  • + Risks​ ​arising​ ​from​ ​taxation; the tax characterization of + acquiring a Validator Slot and/or acting as a Validator is uncertain. + You must seek your own tax advice in connection with your partaking in + the Auction, which may result in adverse tax consequences to you, + including withholding taxes, income taxes, and tax reporting + requirements. +
  • +
  • + Unanticipated​ ​risks: blockchain technology and the Trustlines + Protocol are a new and limitedly tested technology. In addition to the + risks referred to above, there are other risks associated with your + partaking in the Trustlines Auction, including unanticipated risks. + Such risks may further materialize as unanticipated variations or + combinations of the risks previously referred to. +
  • +
+ +

Indemnification

+ +

+ To the fullest extent permitted by applicable law, you will indemnify, + defend and hold harmless the Foundation and its Affiliates from and + against all claims, demands, actions, damages, losses, costs and + expenses (including attorneys’ fees) that arise from or relate to (i) + you participating in the Auction; (ii) your obligations, representations + and warranties under these Terms, (iii) your violation of these Terms, + and/or (iv) your violation of any rights of any other person or entity, + directly and indirectly, related to the Auction. +

+ +

Exclusion of Warranties

+ +

+ To the full extent permitted by law, no warranty, guarantee or similar + assurance whatsoever is expressed or implied with regard to the + Trustlines Protocol and its components. The smart contracts are used at + the sole risk of the User (Users included but not limited to a Vaidator + Candidate) and on an ‘as is’, ‘under development’ and ‘as available’ + basis. +

+ +

+ Participating in the Auction and thereby agreeing to these Terms does + neither entail a right nor an entitlement in any way whatsoever to + either becoming and/or remaining a Validator or receiving rewards and/or + fees. +

+ +

No Reliance

+ +

+ The Validator Candidate has had an opportunity to (i) review these + Terms, (ii) ask questions and receive answers from the Foundation + concerning these Terms and the Auction, and (iii) obtain any additional + information concerning the Auction, the Smart Contracts and the + Foundation to the extent necessary for Validator Candidate in order to + make an informed decision to enter into these Terms and to take part in + the Auction. The Validator Candidate acknowledges that in making a + decision to take part in the Auction, the Validator Candidate has relied + solely upon these Terms and independent investigations made by the + Validator Candidate. The Validator Candidate is not relying on the + Foundation with respect to the legal, tax and other economic factors + involved in entering into these Terms and understands that it is solely + responsible for reviewing the legal, tax and other economic + considerations involved with taking part in the Auction with its own + legal, tax and other advisers. +

+ +

Limitation of Liability

+ +

+ The Validator Candidate acknowledges and agrees that, to the fullest + extent permitted by any applicable law, it will not hold the Foundation + and its Affiliates liable for any direct, indirect, special, incidental, + consequential or exemplary damages (including but not limited to loss of + income, revenue and profits, or goodwill, or data) or injury whatsoever + caused by or related to his/her partaking (or inability to partake) in + the Auction or the use (or inability to use) of the Smart Contracts + under any cause of action whatsoever of any kind in any jurisdiction. +

+

+ The Validator Candidate acknowledges and agrees that, to the fullest + extent permitted by any applicable law, the risk of taking part in the + Auction rests entirely with the Validator Candidate.{" "} +

+

+ The limitations set forth in the Clause do not and will not limit or + exclude liability of the Foundation for fraud or intentional, willful or + reckless misconduct. +

+ +

Release

+ +

+ To the fullest extent permitted by applicable law, the Validator + Candidate releases the Foundation and its Affiliates from + responsibility, liability, claims, demands and/or damages (actual and + consequential) of every kind and nature, known and unknown (including, + but not limited to, claims of negligence), arising out of or related to + disputes between the Parties and out of or related to the acts or + omissions of any third parties. +

+ +

Disputes: Binding Arbitration

+ +

+ Any Dispute shall first be endeavoured to be settled through amicable + negotiations in good faith by the Parties. If the Validator Candidate + and the Foundation cannot agree how to resolve the Dispute within thirty + (30) days after the date a notice is received by the applicable Party, + then either you or the Foundation may, as appropriate, commence + arbitration proceedings. The Dispute shall subsequently (and + exclusively) be submitted to three arbitrators. The nomination of + arbitrators and the rules of arbitration shall be in accordance with the + Rules of Arbitration of Liechtenstein (“Schiedsordnung der + Liechtensteinischen Industrie- und Handelskammer”). The seat of the + arbitral tribunal shall be Ruggell, Liechtenstein. Language of the + proceedings shall be English. The arbitral award is final and binding + upon the parties. The arbitration fees will be borne by the losing party + unless otherwise awarded by the arbitral tribunal. The Parties undertake + to carry out the arbitral award in accordance with the modalities of + said reward. +

+ +

Notices

+ +

+ Notice to the Foundation shall be sent by email to the Foundation at + contact@ trustlines.foundation +

+

+ Notice to you shall be by email to the email address you provide to us. +

+

+ Your notice must include (i) your name, postal address, email address + and telephone number, (ii) a description in reasonable detail of the + nature or basis of any possible Dispute, and (iii) the specific relief + that you are seeking. +

+ +

Governing Law and Venue

+ +

+ These Terms will be governed by and construed in accordance with the + laws of Liechtenstein, without regard to conflict of law rules or + principles that would cause the application of the laws of any other + jurisdiction.{" "} +

+

+ Any Dispute that is not or cannot be subject to arbitration will be + resolved by the courts of Liechtenstein. +

+ +

Severability

+ +

+ If any term, clause or provision of these Terms is held unlawful, void + or unenforceable, then that term, clause or provision will be severable + from these Terms and will not affect the validity or enforceability of + any remaining part of that term, clause or provision, or any other term, + clause or provision of these Terms. Such unlawful, void or unenforceable + clause or provisions shall be replaced by valid and enforceable clause + or provisions, which most closely achieve the commercial intent and + purpose of this Agreement. +

+ +

Miscellaneous

+ +

+ These Terms constitute the entire agreement between the Parties relating + to your acquisition of Auction from the Foundation and supersede any + other agreements, statements or information provided by the Foundation + and/or its Affiliates. the Foundation may assign its rights and + obligations under these Terms. the Foundation’s failure to exercise or + enforce any right or provision of these Terms will not be construed or + understood as a waiver of such right or provision. the Foundation will + not be liable for any delay or failure to perform any obligation under + these Terms where the delay or failure results from any cause beyond our + reasonable control. This Agreement and the (trans)actions envisaged + therein does not create any form of partnership, joint venture, or any + other similar relationship between the Parties. Except as otherwise + provided herein, these Terms are intended solely for the benefit of the + Parties and are not intended to confer third-party beneficiary rights + upon any other person or entity. +

+ +

 

+

 

+
+ ); +} diff --git a/src/features/auction/components/withdraw.jsx b/src/features/auction/components/withdraw.jsx new file mode 100644 index 0000000..026bf49 --- /dev/null +++ b/src/features/auction/components/withdraw.jsx @@ -0,0 +1,59 @@ +import React from "react"; + +import BidBox from "./bid-box"; + +const CONTENTS = [ + { + text: + "A validator auction is a smart contract on Ethereum. The Trustlines Foundation triggers the start of the auction at a provisioned date and time by sending a transaction.", + }, + { + text: + "The auction defines a price per validator slot that continuously decreases according to a specified function.", + }, + { + text: + "Only accounts that have been whitelisted by the Foundation are eligible to participate. Each account can participate only once.", + }, + { + text: + "The auction ends with a certain number of bids, or after two weeks from the start time, whichever comes first. The number of participants at the end defines the result of the auction.", + }, + { + text: + "If the auction fails, all participants can withdraw the entirety of the TLN they used to bid. They can withdraw their bid by calling the auction contract's `withdraw` function.", + }, + { + text: + 'If the auction is successful, the last bid\'s price is defined as the "slot price." Every participant can withdraw the difference between the slot price and their bid. All TLN used in successful bids will be transferred to the deposit locker contract and kept there as a stake for the duration of the validator period.', + }, +]; + +export function AuctionWithdraw() { + return ( +
+
+
+
+

+ Withdraw excess from your bid +

+
+
+ +
+
+
+ {CONTENTS.map((content, i) => ( +
+ {i !== 0 ? ( +
+ ) : null} +

{content.text}

+
+ ))} +
+
+
+ ); +} diff --git a/src/features/auction/hooks/bidder.js b/src/features/auction/hooks/bidder.js new file mode 100644 index 0000000..cabdba8 --- /dev/null +++ b/src/features/auction/hooks/bidder.js @@ -0,0 +1,100 @@ +import { useState, useEffect } from "react"; +import * as auctionWeb3 from "../../../js/auction/bidbox/web3"; +import { AuctionState } from "../../../js/auction/bidbox/web3"; +import { fetchBalance } from "../../../js/common/web3"; + +export const BidderState = { + LOADING: "loading", + NOT_STARTED: "notStarted", + READY_TO_WITHDRAW: "readyToWithdraw", + NOTHING_TO_WITHDRAW: "nothingToWithdraw", + WAITING_DEPOSIT: "waitingDeposit", + NOT_WHITELISTED: "notWhitelisted", + NO_ALLOWANCE: "noAllowance", + WRONG_ALLOWANCE: "wrongAllowance", + NOT_ENOUGH_TOKENS: "notEnoughTokens", + NO_ETH: "noETH", + READY_TO_BID: "readyToBid", + ALREADY_BID: "alreadyBid", + ERROR: "error", +}; + +export function useBidderState(account) { + const [bidderState, setBidderState] = useState(BidderState.LOADING); + + useEffect(() => { + if (!account) { + setBidderState(BidderState.LOADING); + console.log("No account selected"); + return; + } + + // box variable to make it available in inner function + const env = { + intervalId: 0, + }; + + // define async function because effect function can not be async + async function startCheckState() { + async function checkState() { + if (!account) { + setBidderState(BidderState.LOADING); + console.log("No account selected"); + return; + } + + const auctionState = await auctionWeb3.fetchAuctionState(); + let currentPrice = 0; + if (AuctionState.STARTED === auctionState) { + currentPrice = await auctionWeb3.fetchCurrentPrice(); + if (!currentPrice) { + setBidderState(BidderState.LOADING); + console.log("Current price not loaded"); + return; + } + } + + if (!(await auctionWeb3.isWhitelisted(account))) { + setBidderState(BidderState.NOT_WHITELISTED); + } else if ( + [AuctionState.ENDED, AuctionState.FAILED].includes(auctionState) + ) { + if ((await fetchBalance(account)) === "0") { + setBidderState(BidderState.NO_ETH); + } else if ((await auctionWeb3.fetchValueToWithdraw(account)).gt(0)) { + setBidderState(BidderState.READY_TO_WITHDRAW); + } else { + setBidderState(BidderState.NOTHING_TO_WITHDRAW); + } + } else if (auctionState === AuctionState.DEPOSIT_PENDING) { + setBidderState(BidderState.WAITING_DEPOSIT); + } else if (await auctionWeb3.hasBid(account)) { + setBidderState(BidderState.ALREADY_BID); + } else if ((await fetchBalance(account)) === "0") { + setBidderState(BidderState.NO_ETH); + } else if ((await auctionWeb3.fetchAllowance(account)).eq(0)) { + setBidderState(BidderState.NO_ALLOWANCE); + } else if ( + (await auctionWeb3.fetchAllowance(account)).lt(currentPrice) + ) { + setBidderState(BidderState.WRONG_ALLOWANCE); + } else if (auctionState === AuctionState.DEPLOYED) { + setBidderState(BidderState.NOT_STARTED); + } else if ( + (await auctionWeb3.fetchTokenBalance(account)).lt(currentPrice) + ) { + setBidderState(BidderState.NOT_ENOUGH_TOKENS); + } else { + setBidderState(BidderState.READY_TO_BID); + } + } + + env.chainCheckIntervalId = setInterval(checkState, 500); + checkState(); + } + + startCheckState(); + return () => clearInterval(env.chainCheckIntervalId); + }, [account]); + return bidderState; +} diff --git a/src/features/auction/hooks/current-price.js b/src/features/auction/hooks/current-price.js new file mode 100644 index 0000000..e79f276 --- /dev/null +++ b/src/features/auction/hooks/current-price.js @@ -0,0 +1,27 @@ +import { useState, useEffect } from "react"; + +import * as auctionWeb3 from "../../../js/auction/bidbox/web3"; + +export function useCurrentPrice() { + const [currentPrice, setCurrentPrice] = useState(null); + + useEffect(() => { + // box variable to make it available in inner function + const env = { + intervalId: 0, + }; + + // define async function because effect function can not be async + async function periodicallyFetchCurrentPrice() { + async function fetchPrice() { + setCurrentPrice(await auctionWeb3.fetchCurrentPrice()); + } + + env.intervalId = setInterval(fetchPrice, 10000); + fetchPrice(); + } + periodicallyFetchCurrentPrice(); + return () => clearInterval(env.intervalId); + }, []); + return currentPrice; +} diff --git a/src/features/auction/hooks/use-auction-api.js b/src/features/auction/hooks/use-auction-api.js new file mode 100644 index 0000000..8770507 --- /dev/null +++ b/src/features/auction/hooks/use-auction-api.js @@ -0,0 +1,24 @@ +import React from "react"; + +export default function useAuctionAPI() { + const [isFetching, setIsFetching] = React.useState(false); + const [result, setResult] = React.useState(null); + + React.useEffect(() => { + setIsFetching(true); + + fetch(process.env.AUCTION_API_URL) + .then(async result => { + const json = await result.json(); + setResult(json); + }) + .catch(error => { + console.error(error); + }) + .finally(() => { + setIsFetching(false); + }); + }, []); + + return [result, isFetching]; +} diff --git a/src/features/auction/images/auction-graph.svg b/src/features/auction/images/auction-graph.svg new file mode 100644 index 0000000..068e17b --- /dev/null +++ b/src/features/auction/images/auction-graph.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/features/common/abi/auction.json b/src/features/common/abi/auction.json new file mode 100644 index 0000000..e83dd3e --- /dev/null +++ b/src/features/common/abi/auction.json @@ -0,0 +1,494 @@ +[ + { + "constant": true, + "inputs": [], + "name": "lowestSlotPrice", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "bid", + "outputs": [], + "payable": true, + "stateMutability": "payable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "closeAuction", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "withdraw", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "depositBids", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "maximalNumberOfParticipants", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "auctionDurationInDays", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "closeTime", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "address" + } + ], + "name": "bids", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "startAuction", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "secondsSinceStart", + "type": "uint256" + } + ], + "name": "priceAtElapsedTime", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "startTime", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "addressesToWhitelist", + "type": "address[]" + } + ], + "name": "addToWhitelist", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "auctionState", + "outputs": [ + { + "name": "", + "type": "uint8" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "owner", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "address" + } + ], + "name": "whitelist", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "currentPrice", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "depositLocker", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "uint256" + } + ], + "name": "bidders", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "bidToken", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "startPrice", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "minimalNumberOfParticipants", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "name": "_startPriceInWei", + "type": "uint256" + }, + { + "name": "_auctionDurationInDays", + "type": "uint256" + }, + { + "name": "_minimalNumberOfParticipants", + "type": "uint256" + }, + { + "name": "_maximalNumberOfParticipants", + "type": "uint256" + }, + { + "name": "_depositLocker", + "type": "address" + }, + { + "name": "_bidToken", + "type": "address" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "payable": true, + "stateMutability": "payable", + "type": "fallback" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "bidder", + "type": "address" + }, + { + "indexed": false, + "name": "bidValue", + "type": "uint256" + }, + { + "indexed": false, + "name": "slotPrice", + "type": "uint256" + }, + { + "indexed": false, + "name": "timestamp", + "type": "uint256" + } + ], + "name": "BidSubmitted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "whitelistedAddress", + "type": "address" + } + ], + "name": "AddressWhitelisted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "startPrice", + "type": "uint256" + }, + { + "indexed": false, + "name": "auctionDurationInDays", + "type": "uint256" + }, + { + "indexed": false, + "name": "minimalNumberOfParticipants", + "type": "uint256" + }, + { + "indexed": false, + "name": "maximalNumberOfParticipants", + "type": "uint256" + } + ], + "name": "AuctionDeployed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "startTime", + "type": "uint256" + } + ], + "name": "AuctionStarted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "closeTime", + "type": "uint256" + }, + { + "indexed": false, + "name": "lowestSlotPrice", + "type": "uint256" + }, + { + "indexed": false, + "name": "totalParticipants", + "type": "uint256" + } + ], + "name": "AuctionDepositPending", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "closeTime", + "type": "uint256" + }, + { + "indexed": false, + "name": "lowestSlotPrice", + "type": "uint256" + }, + { + "indexed": false, + "name": "totalParticipants", + "type": "uint256" + } + ], + "name": "AuctionEnded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "closeTime", + "type": "uint256" + }, + { + "indexed": false, + "name": "numberOfBidders", + "type": "uint256" + } + ], + "name": "AuctionFailed", + "type": "event" + } +] diff --git a/src/features/common/abi/merkle-drop.json b/src/features/common/abi/merkle-drop.json new file mode 100644 index 0000000..66974b6 --- /dev/null +++ b/src/features/common/abi/merkle-drop.json @@ -0,0 +1,323 @@ +[ + { + "constant": true, + "inputs": [ + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "time", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "roundUp", + "type": "bool" + } + ], + "name": "decayedEntitlementAtTime", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "initialBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "bytes32[]", + "name": "proof", + "type": "bytes32[]" + } + ], + "name": "withdrawFor", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "remainingValue", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "bytes32[]", + "name": "proof", + "type": "bytes32[]" + } + ], + "name": "withdraw", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "deleteContract", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "withdrawn", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decayStartTime", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "bytes32[]", + "name": "proof", + "type": "bytes32[]" + } + ], + "name": "verifyEntitled", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "burnUnusableTokens", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decayDurationInSeconds", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "droppedToken", + "outputs": [ + { + "internalType": "contract ERC20Interface", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "root", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "spentTokens", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract ERC20Interface", + "name": "_droppedToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_initialBalance", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "_root", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "_decayStartTime", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_decayDurationInSeconds", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "originalValue", + "type": "uint256" + } + ], + "name": "Withdraw", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Burn", + "type": "event" + } +] diff --git a/src/features/common/abi/token.json b/src/features/common/abi/token.json new file mode 100644 index 0000000..fc1241f --- /dev/null +++ b/src/features/common/abi/token.json @@ -0,0 +1,189 @@ +[ + { + "constant": false, + "inputs": [ + { + "name": "spender", + "type": "address" + }, + { + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "sender", + "type": "address" + }, + { + "name": "recipient", + "type": "address" + }, + { + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "amount", + "type": "uint256" + } + ], + "name": "burn", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "recipient", + "type": "address" + }, + { + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "owner", + "type": "address" + }, + { + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "from", + "type": "address" + }, + { + "indexed": true, + "name": "to", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + } +] diff --git a/src/features/common/api/web3.js b/src/features/common/api/web3.js new file mode 100644 index 0000000..d2ed728 --- /dev/null +++ b/src/features/common/api/web3.js @@ -0,0 +1,121 @@ +import Web3 from "web3"; + +let web3; + +export default function getWeb3() { + if (!web3) { + connect(); + } + + return web3; +} + +export function connect() { + // Modern dapp browsers... + if (window.ethereum) { + console.log("Found modern dapp browser"); + web3 = new Web3(window.ethereum); + return true; + } + // Legacy dapp browsers... + else if (window.web3) { + console.log("Found legacy dapp browser"); + web3 = new Web3(window.web3.currentProvider); + return true; + } + // Non-dapp browsers... + else { + console.log( + "No Web3 enabled browser detected. You should consider trying MetaMask!" + ); + return false; + } +} + +export async function requestPermission() { + // Modern dapp browsers... + if (window.ethereum) { + try { + // Request account access if needed + await window.ethereum.enable(); + return true; + } catch (error) { + // User denied account access... + console.error(error); + return false; + } + } + // Legacy dapp browsers... + else if (window.web3) { + return true; + } + // Non-dapp browsers... + else { + throw Error("Can not ask for permission, no dapp browser found."); + } +} + +export async function fetchBalance(address) { + return web3.eth.getBalance(address); +} + +export async function verifyChainId(chainId) { + return (await web3.eth.getChainId()) === parseInt(chainId); +} + +/// Queries the default account. Returns undefined if wallet locked, or if website has no permission to query. +export async function getDefaultAccount() { + return (await web3.eth.getAccounts())[0]; +} + +/// Compares two hex encoded addresses ignoring the checksum +export function sameAddress(address1, address2) { + return address1.toLowerCase() === address2.toLowerCase(); +} + +export async function sendContractTransaction( + web3FunctionCall, + sender, + onSign, + onConfirmation +) { + console.log(sender); + try { + return await web3FunctionCall + .send({ + from: sender, + }) + .on("transactionHash", hash => onSign && onSign(hash)) + .on( + "confirmation", + (confirmationNumber, receipt) => + onConfirmation && onConfirmation(confirmationNumber, receipt) + ); + } catch (error) { + // As there seem to be no common error format, this is the best we can do + if (error.message.includes("revert")) { + throw new TransactionRevertedError(error.message); + } else if (error.message.includes("denied")) { + throw new UserRejectedError(error.message); + } else { + throw new Error(error.message); + } + } +} + +export const USER_REJECTED_ERROR_CODE = "USER_REJECTED_ERROR"; +export const TRANSACTION_REVERTED_ERROR_CODE = "TRANSACTION_REVERTED_ERROR"; + +class UserRejectedError extends Error { + constructor(...args) { + super(...args); + this.code = USER_REJECTED_ERROR_CODE; + } +} + +class TransactionRevertedError extends Error { + constructor(...args) { + super(...args); + this.code = TRANSACTION_REVERTED_ERROR_CODE; + } +} diff --git a/src/features/common/components/button.jsx b/src/features/common/components/button.jsx new file mode 100644 index 0000000..a92e8bf --- /dev/null +++ b/src/features/common/components/button.jsx @@ -0,0 +1,16 @@ +import React from "react"; + +export function Button(props) { + const colorClassNames = props.isDark + ? "bg-rich-black-lighter text-off-white hover:bg-off-white hover:text-rich-black" + : "bg-grey-lighter text-rich-black hover:bg-rich-black hover:text-white"; + + return ( + + ); +} diff --git a/src/features/common/components/card.jsx b/src/features/common/components/card.jsx new file mode 100644 index 0000000..186dbab --- /dev/null +++ b/src/features/common/components/card.jsx @@ -0,0 +1,7 @@ +import React from "react"; + +export const Card = props => { + const classes = `rounded-3xl flex flex-row justify-center items-center ${props.className}`; + + return
{props.children}
; +}; diff --git a/src/features/common/components/footer.jsx b/src/features/common/components/footer.jsx new file mode 100644 index 0000000..b02474d --- /dev/null +++ b/src/features/common/components/footer.jsx @@ -0,0 +1,49 @@ +import React from "react"; + +import { SocialLinks } from "./social-links"; +import { CircledArrowUp } from "./icons/circled-arrow-up"; +import footerGroups from "../content/footer-groups"; + +export function Footer() { + return ( +
+
+
+ {footerGroups.map(group => ( +
+
+ {group.label} +
+ {group.items.map((item, i) => ( + + {item.label} + + ))} +
+ ))} +
+
+
+
+ +
+
+ Copyright © 2021 Trustlines Foundation +
+
+
+ +
+
+
+
+ ); +} diff --git a/src/features/common/components/icons/arrow-down-right.jsx b/src/features/common/components/icons/arrow-down-right.jsx new file mode 100644 index 0000000..8683548 --- /dev/null +++ b/src/features/common/components/icons/arrow-down-right.jsx @@ -0,0 +1,27 @@ +import React from "react"; + +export function ArrowDownRight(props) { + return ( + + + + + ); +} diff --git a/src/features/common/components/icons/arrow-right.jsx b/src/features/common/components/icons/arrow-right.jsx new file mode 100644 index 0000000..3d02091 --- /dev/null +++ b/src/features/common/components/icons/arrow-right.jsx @@ -0,0 +1,16 @@ +import React from "react"; + +export function ArrowRight(props) { + return ( + + + + ); +} diff --git a/src/features/common/components/icons/at-sign.jsx b/src/features/common/components/icons/at-sign.jsx new file mode 100644 index 0000000..ff38027 --- /dev/null +++ b/src/features/common/components/icons/at-sign.jsx @@ -0,0 +1,27 @@ +import React from "react"; + +export function AtSign(props) { + return ( + + + + + ); +} diff --git a/src/features/common/components/icons/award.jsx b/src/features/common/components/icons/award.jsx new file mode 100644 index 0000000..e2e9ce2 --- /dev/null +++ b/src/features/common/components/icons/award.jsx @@ -0,0 +1,34 @@ +import React from "react"; + +export function Award(props) { + return ( + + + + + + + + + + + + ); +} diff --git a/src/features/common/components/icons/book-open.jsx b/src/features/common/components/icons/book-open.jsx new file mode 100644 index 0000000..9b64eb7 --- /dev/null +++ b/src/features/common/components/icons/book-open.jsx @@ -0,0 +1,27 @@ +import React from "react"; + +export function BookOpen(props) { + return ( + + + + + ); +} diff --git a/src/features/common/components/icons/branch.jsx b/src/features/common/components/icons/branch.jsx new file mode 100644 index 0000000..7936c36 --- /dev/null +++ b/src/features/common/components/icons/branch.jsx @@ -0,0 +1,42 @@ +import React from "react"; + +export function Branch() { + return ( + + + + + + + ); +} diff --git a/src/features/common/components/icons/burger-menu.jsx b/src/features/common/components/icons/burger-menu.jsx new file mode 100644 index 0000000..5f654d8 --- /dev/null +++ b/src/features/common/components/icons/burger-menu.jsx @@ -0,0 +1,19 @@ +import React from "react"; + +export function BurgerMenu() { + return ( + + + + + + + ); +} diff --git a/src/features/common/components/icons/check.jsx b/src/features/common/components/icons/check.jsx new file mode 100644 index 0000000..ba4b772 --- /dev/null +++ b/src/features/common/components/icons/check.jsx @@ -0,0 +1,21 @@ +import React from "react"; + +export function Check() { + return ( + + + + ); +} diff --git a/src/features/common/components/icons/circled-arrow-up.jsx b/src/features/common/components/icons/circled-arrow-up.jsx new file mode 100644 index 0000000..8de6ade --- /dev/null +++ b/src/features/common/components/icons/circled-arrow-up.jsx @@ -0,0 +1,29 @@ +import React from "react"; + +export function CircledArrowUp() { + return ( + + + + + + ); +} diff --git a/src/features/common/components/icons/close.jsx b/src/features/common/components/icons/close.jsx new file mode 100644 index 0000000..62b63e0 --- /dev/null +++ b/src/features/common/components/icons/close.jsx @@ -0,0 +1,28 @@ +import React from "react"; + +export function Close() { + return ( + + + + + + ); +} diff --git a/src/features/common/components/icons/copy.jsx b/src/features/common/components/icons/copy.jsx new file mode 100644 index 0000000..de76cc2 --- /dev/null +++ b/src/features/common/components/icons/copy.jsx @@ -0,0 +1,27 @@ +import React from "react"; + +export function Copy() { + return ( + + + + + ); +} diff --git a/src/features/common/components/icons/currencies.jsx b/src/features/common/components/icons/currencies.jsx new file mode 100644 index 0000000..6f186ad --- /dev/null +++ b/src/features/common/components/icons/currencies.jsx @@ -0,0 +1,39 @@ +import React from "react"; + +export function Currencies(props) { + return ( + + + + + + + + ); +} diff --git a/src/features/common/components/icons/file-text.jsx b/src/features/common/components/icons/file-text.jsx new file mode 100644 index 0000000..a497982 --- /dev/null +++ b/src/features/common/components/icons/file-text.jsx @@ -0,0 +1,45 @@ +import React from "react"; + +export function FileText(props) { + return ( + + + + + + + + ); +} diff --git a/src/features/common/components/icons/group.jsx b/src/features/common/components/icons/group.jsx new file mode 100644 index 0000000..1ee1332 --- /dev/null +++ b/src/features/common/components/icons/group.jsx @@ -0,0 +1,39 @@ +import React from "react"; + +export function Group(props) { + return ( + + + + + + + ); +} diff --git a/src/features/common/components/icons/heart.jsx b/src/features/common/components/icons/heart.jsx new file mode 100644 index 0000000..e6ce99a --- /dev/null +++ b/src/features/common/components/icons/heart.jsx @@ -0,0 +1,21 @@ +import React from "react"; + +export function Heart(props) { + return ( + + + + ); +} diff --git a/src/features/common/components/icons/loading.jsx b/src/features/common/components/icons/loading.jsx new file mode 100644 index 0000000..5928169 --- /dev/null +++ b/src/features/common/components/icons/loading.jsx @@ -0,0 +1,70 @@ +import React from "react"; + +export function Loading() { + return ( + + + + + + + + + + + ); +} diff --git a/src/features/common/components/icons/lock.jsx b/src/features/common/components/icons/lock.jsx new file mode 100644 index 0000000..b69dcb4 --- /dev/null +++ b/src/features/common/components/icons/lock.jsx @@ -0,0 +1,28 @@ +import React from "react"; + +export function Lock() { + return ( + + + + + ); +} diff --git a/src/features/common/components/icons/logo.jsx b/src/features/common/components/icons/logo.jsx new file mode 100644 index 0000000..77d9b57 --- /dev/null +++ b/src/features/common/components/icons/logo.jsx @@ -0,0 +1,98 @@ +import React from "react"; + +export function Logo() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/src/features/common/components/icons/mail-dot.jsx b/src/features/common/components/icons/mail-dot.jsx new file mode 100644 index 0000000..58c2af5 --- /dev/null +++ b/src/features/common/components/icons/mail-dot.jsx @@ -0,0 +1,28 @@ +import React from "react"; + +export function MailDot(props) { + return ( + + + + + + ); +} diff --git a/src/features/common/components/icons/mail.jsx b/src/features/common/components/icons/mail.jsx new file mode 100644 index 0000000..7d26008 --- /dev/null +++ b/src/features/common/components/icons/mail.jsx @@ -0,0 +1,28 @@ +import React from "react"; + +export function Mail(props) { + return ( + + + + + ); +} diff --git a/src/features/common/components/icons/message-box.jsx b/src/features/common/components/icons/message-box.jsx new file mode 100644 index 0000000..d9f5a96 --- /dev/null +++ b/src/features/common/components/icons/message-box.jsx @@ -0,0 +1,21 @@ +import React from "react"; + +export function MessageBox(props) { + return ( + + + + ); +} diff --git a/src/features/common/components/icons/paste.jsx b/src/features/common/components/icons/paste.jsx new file mode 100644 index 0000000..9e93147 --- /dev/null +++ b/src/features/common/components/icons/paste.jsx @@ -0,0 +1,27 @@ +import React from "react"; + +export function Paste() { + return ( + + + + + ); +} diff --git a/src/features/common/components/icons/play.jsx b/src/features/common/components/icons/play.jsx new file mode 100644 index 0000000..0ba4628 --- /dev/null +++ b/src/features/common/components/icons/play.jsx @@ -0,0 +1,27 @@ +import React from "react"; + +export function Play(props) { + return ( + + + + + + + ); +} diff --git a/src/features/common/components/icons/question-mark.jsx b/src/features/common/components/icons/question-mark.jsx new file mode 100644 index 0000000..0cbfc57 --- /dev/null +++ b/src/features/common/components/icons/question-mark.jsx @@ -0,0 +1,34 @@ +import React from "react"; + +export function QuestionMark(props) { + return ( + + + + + + ); +} diff --git a/src/features/common/components/icons/send.jsx b/src/features/common/components/icons/send.jsx new file mode 100644 index 0000000..f7bec31 --- /dev/null +++ b/src/features/common/components/icons/send.jsx @@ -0,0 +1,27 @@ +import React from "react"; + +export function Send(props) { + return ( + + + + + ); +} diff --git a/src/features/common/components/icons/settings.jsx b/src/features/common/components/icons/settings.jsx new file mode 100644 index 0000000..381d181 --- /dev/null +++ b/src/features/common/components/icons/settings.jsx @@ -0,0 +1,27 @@ +import React from "react"; + +export function Settings(props) { + return ( + + + + + ); +} diff --git a/src/features/common/components/icons/shield.jsx b/src/features/common/components/icons/shield.jsx new file mode 100644 index 0000000..645ad2c --- /dev/null +++ b/src/features/common/components/icons/shield.jsx @@ -0,0 +1,21 @@ +import React from "react"; + +export function Shield() { + return ( + + + + ); +} diff --git a/src/features/common/components/icons/smiley.jsx b/src/features/common/components/icons/smiley.jsx new file mode 100644 index 0000000..a5487c8 --- /dev/null +++ b/src/features/common/components/icons/smiley.jsx @@ -0,0 +1,42 @@ +import React from "react"; + +export function Smiley() { + return ( + + + + + + + ); +} diff --git a/src/features/common/components/icons/square-check.jsx b/src/features/common/components/icons/square-check.jsx new file mode 100644 index 0000000..337e213 --- /dev/null +++ b/src/features/common/components/icons/square-check.jsx @@ -0,0 +1,27 @@ +import React from "react"; + +export function SquareCheck(props) { + return ( + + + + + ); +} diff --git a/src/features/common/components/icons/target.jsx b/src/features/common/components/icons/target.jsx new file mode 100644 index 0000000..6e630db --- /dev/null +++ b/src/features/common/components/icons/target.jsx @@ -0,0 +1,33 @@ +import React from "react"; + +export function Traget(props) { + return ( + + + + + + ); +} diff --git a/src/features/common/components/icons/telegram.jsx b/src/features/common/components/icons/telegram.jsx new file mode 100644 index 0000000..f528f4e --- /dev/null +++ b/src/features/common/components/icons/telegram.jsx @@ -0,0 +1,16 @@ +import React from "react"; + +export function Telegram(props) { + return ( + + + + ); +} diff --git a/src/features/common/components/icons/twitter.jsx b/src/features/common/components/icons/twitter.jsx new file mode 100644 index 0000000..62b38cd --- /dev/null +++ b/src/features/common/components/icons/twitter.jsx @@ -0,0 +1,16 @@ +import React from "react"; + +export function Twitter(props) { + return ( + + + + ); +} diff --git a/src/features/common/components/icons/warning.jsx b/src/features/common/components/icons/warning.jsx new file mode 100644 index 0000000..8746aea --- /dev/null +++ b/src/features/common/components/icons/warning.jsx @@ -0,0 +1,33 @@ +import React from "react"; + +export function Warning(props) { + return ( + + + + + + ); +} diff --git a/src/features/common/components/icons/you-tube.jsx b/src/features/common/components/icons/you-tube.jsx new file mode 100644 index 0000000..4613711 --- /dev/null +++ b/src/features/common/components/icons/you-tube.jsx @@ -0,0 +1,20 @@ +import React from "react"; + +export function YouTube(props) { + return ( + + + + ); +} diff --git a/src/features/common/components/layout.jsx b/src/features/common/components/layout.jsx new file mode 100644 index 0000000..89a7751 --- /dev/null +++ b/src/features/common/components/layout.jsx @@ -0,0 +1,14 @@ +import React from "react"; + +import NavBar from "./navbar"; +import { Footer } from "./footer"; + +export function Layout(props) { + return ( +
+ + {props.children} +
+
+ ); +} diff --git a/src/features/common/components/link-button.jsx b/src/features/common/components/link-button.jsx new file mode 100644 index 0000000..33b2d5a --- /dev/null +++ b/src/features/common/components/link-button.jsx @@ -0,0 +1,20 @@ +import React from "react"; + +export function LinkButton(props) { + const colorClassNames = props.isDark + ? "bg-rich-black-lighter text-off-white" + : "bg-grey-lighter text-rich-black"; + + return ( + + ); +} diff --git a/src/features/common/components/navbar/index.js b/src/features/common/components/navbar/index.js new file mode 100644 index 0000000..f9f3358 --- /dev/null +++ b/src/features/common/components/navbar/index.js @@ -0,0 +1,15 @@ +import React from "react"; + +import NavbarDesktop from "./navbar-desktop"; +import NavbarMobile from "./navbar-mobile"; + +import navBarLinks from "../../content/nav-bar-links.json"; + +export default function Navbar() { + return ( + <> + + + + ); +} diff --git a/src/features/common/components/navbar/nav-item-desktop.js b/src/features/common/components/navbar/nav-item-desktop.js new file mode 100644 index 0000000..f990050 --- /dev/null +++ b/src/features/common/components/navbar/nav-item-desktop.js @@ -0,0 +1,19 @@ +import React from "react"; +import { Link } from "gatsby"; + +export default function NavItemDesktop({ to, label, subNavItems = [] }) { + return ( +
+
+ + {label} + +
+
+ ); +} diff --git a/src/features/common/components/navbar/nav-item-mobile.js b/src/features/common/components/navbar/nav-item-mobile.js new file mode 100644 index 0000000..f125710 --- /dev/null +++ b/src/features/common/components/navbar/nav-item-mobile.js @@ -0,0 +1,16 @@ +import React from "react"; +import { Link } from "gatsby"; + +export default function NavItemMobile({ to, label }) { + return ( +
+ + {label} + +
+ ); +} diff --git a/src/features/common/components/navbar/navbar-desktop.js b/src/features/common/components/navbar/navbar-desktop.js new file mode 100644 index 0000000..5aaafe0 --- /dev/null +++ b/src/features/common/components/navbar/navbar-desktop.js @@ -0,0 +1,24 @@ +import React from "react"; +import { Link } from "gatsby"; + +import NavItem from "./nav-item-desktop"; +import { Logo } from "../icons/logo"; + +export default function NavbarDesktop({ navItems = [] }) { + return ( +
+
+
+ + + +
+ {navItems.map(item => ( + + ))} +
+
+
+
+ ); +} diff --git a/src/features/common/components/navbar/navbar-mobile.js b/src/features/common/components/navbar/navbar-mobile.js new file mode 100644 index 0000000..bcc8fe6 --- /dev/null +++ b/src/features/common/components/navbar/navbar-mobile.js @@ -0,0 +1,60 @@ +import React, { useState } from "react"; +import { Link } from "gatsby"; + +import NavItemMobile from "./nav-item-mobile"; + +import { SocialLinks } from "../social-links"; +import { Logo } from "../icons/logo"; +import { Close } from "../icons/close"; +import { BurgerMenu } from "../icons/burger-menu"; + +export default function NavbarMobile({ navItems = [] }) { + const [isFullScreenNavOpen, setIsFullScreenNavOpen] = useState(false); + + return isFullScreenNavOpen ? ( +
+
+
+
+ + + +
+
+ +
+
+
+ {navItems.map((navItem, i) => ( +
setIsFullScreenNavOpen(false)} + > + +
+ ))} +
+ +
+
+
+
+ ) : ( +
+ + + + +
+ ); +} diff --git a/src/features/common/components/privacy-modal.jsx b/src/features/common/components/privacy-modal.jsx new file mode 100644 index 0000000..3630f06 --- /dev/null +++ b/src/features/common/components/privacy-modal.jsx @@ -0,0 +1,36 @@ +import React from "react"; +import { Button } from "./button"; +import { PrivacyPolicy } from "./privacy-policy"; + +function PrivacyPolicyModal(props) { + const { onClose } = props; + + return ( + <> +
+
+
+
+
+ +
+
+ +
+
+
+
+
+ + ); +} + +export default PrivacyPolicyModal; diff --git a/src/features/common/components/privacy-policy.jsx b/src/features/common/components/privacy-policy.jsx new file mode 100644 index 0000000..a0b1f4e --- /dev/null +++ b/src/features/common/components/privacy-policy.jsx @@ -0,0 +1,402 @@ +import React from "react"; +export function PrivacyPolicy() { + return ( +
+

Privacy Policy

+

+ This Privacy Policy describes how your Personal Information is + collected, used, and shared by the Trustlines Foundation. Personal + Information means any information relating to an identified or + identifiable natural person, who may be identified, directly or + indirectly by reference to an identifier such as a name, an + identification number, location data, online information (e.g. an IP + address) or to one or more factors relating to that person. This notice + applies to Personal Information provided to us by visitors to{" "} + https://trustlines.network{" "} + (including subdomains{" "} + + https://forum.trustlines.network + + ,{" "} + + https://blog.trustlines.network/ + + ,{" "} + + https://docs.trustlines.network/ + {" "} + and{" "} + + https://dev.trustlines.network/ + + ) and/or{" "} + + https://trustlines.foundation + {" "} + (the “Site”), end users, beta testers, suppliers, contractors, job + applicants, or other direct or indirect affected persons of the + activities we carry out and functions we perform in running the Site. In + this notice “you” refers to any individual whose Personal Information we + hold or process. +

+ +

1. Basis on which we process personal data

+ +

+ Personal Information we hold about you will be processed either because: +

+ +
    +
  • + the processing is necessary in pursuit of a “legitimate interest” + (Art. 6 Para. 1 lit. f GDPR); a legitimate interest in this context + means a valid interest we have or a third party has in processing your + personal data which is not overridden by your interests in data + privacy and security; +
  • +
  • + you have consented to the processing for the specific purposes + described in this notice (Art. 6 Para. 1 lit. a GDPR); or +
  • +
  • + the processing serves to fulfill a contract to which the data subject + is a party or to carry out pre-contractual measures (Art. 6 Para. 1 + lit. b GDPR) +
  • +
  • + the processing is necessary in order for us to comply with our + obligations under a contract between you and us. +
  • +
+ +

2. Personal information we collect

+

+ When you visit the Site, we may automatically collect certain + information about you and your device, including information about your + web browser, IP address, time zone, and some of the cookies that are + installed on your device. Additionally, as you browse the Site, we may + collect information about the individual web pages or products that you + view, what websites or search terms referred you to the Site, and + information about how you interact with the Site. We refer to this + automatically-collected information as “Device Information.” We collect + Device Information using the following technologies: +

+
    +
  • + “Log files” track actions occurring on the Site, and collect data + including your IP address, browser type, Internet service provider, + referring/exit pages, and date/time stamps. +
  • +
+ +

+ Additionally, if you sign up for a newsletter through the Site, we + collect your name and email address. When registering for the use of our + personalized services, we may collect additional contact details such as + your name, username, location and email address. When registering via + your Github account, we may additionally have access to your Github + username. When registering via your Twitter account, we may additionally + have access to your Twitter account information such as your tweets, + settings and blocked users. (We're not storing this information from + Twitter, the email address is what is used for the account creation at + the forum, the app is a Twitter app using the API and that's why it asks + for such a list of items.) We refer to all this information above as + “Subscription Information.” In the course of our interactions, we may + also collect a record and details of any correspondence or + communications between you and us relating to any information request or + job application submitted to us. We refer to this information as + “Communication Information”. When we talk about “Personal Information” + in this Privacy Policy, we are talking both about Device Information, + Subscription Information and Communication Information. +

+ +

3. How do we use your personal information?

+

+ Your Subscription Information will only be used to send you the + subscribed newsletter by email. Your name is given so that we can + address you personally in the newsletter and, if necessary, identify you + if you want to exercise your rights as a data subject. To receive the + newsletter, it is sufficient to enter your email address. When you + register to receive our newsletter, the data you provide will only be + used for this purpose. Subscribers can also be informed via email about + circumstances that are relevant to the service or registration (e.g. + changes to the newsletter offer or technical conditions). For an + effective registration, we need a valid email address. In order to check + that a registration is actually made by the owner of an email address, + we use the "double opt-in" procedure. For this purpose, we log the order + of the newsletter, the sending of a confirmation email and the receipt + of the response requested. Further data is not collected. The data will + only be used for sending the newsletter and will not be passed on to + third parties. +

+ +

+ We use or may use the Subscription Information regarding our other + personalized services and Communication Information that we collect + generally to communicate with you and to provide any other information + you have requested or respond to any other correspondence. If you are + registered with us, you can access content and services that we only + offer to registered users. Registered users also have the option of + changing or deleting the data provided when registering at any time. If + users leave comments or forum posts on our website, the time of their + creation and the user name previously selected by the user are saved in + addition to this information. This serves our security as we can be + prosecuted for illegal content on our website, even if it was created by + users. +

+ +

+ We use the Device Information that we collect to help us screen for + potential risk and fraud (in particular, your IP address), and more + generally to improve and optimize our Site (ensuring a problem-free + connection to the website, evaluating system security and stability and + for other administrative purposes). (This processing for our legitimate + interests in managing the information we provide via our Site and to + assess how people use the Site.) We do not use your Personal Information + to draw conclusions about you personally. Personal Information may be + statistically evaluated by us in order to optimize our website and the + technology behind it. +

+ +

4. Sharing your personal information

+

+ We share your Personal Information with third parties such as processors + to help us use your Personal Information, as described above. We may + disclose your Personal Information to other third-parties that enable us + to provide the Site and any connected services including technical + support providers who may have access to personal data. These may + include software and website developers, IT support and mail management + service providers. In all cases, our agreement with the relevant third + party will contain appropriate provisions regarding the use and security + of your Personal Information. Finally, we may also share your Personal + Information to comply with applicable laws and regulations, to respond + to a subpoena, search warrant or other lawful request for information we + receive, or to otherwise protect our rights. +

+ +

5. Do not track

+

+ Please note that we do not alter our Site’s data collection and use + practices when we see a Do Not Track signal from your browser. +

+ +

6. Transfering your information outside Europe

+

+ We will not transfer any data in a systematic way outside of the EEA but + there may be circumstances in which certain personal information is + transferred outside of the EEA, in particular: +

+
    +
  • + if you live outside of the EEA, we may communicate with you during the + course of our interactions and such communications may include + Personal Information (such as Subscription Information); +
  • +
  • + we may have certain third-party service suppliers who are based + outside the EEA or who store data outside of the EEA; +
  • +
  • + if you communicate with us while you are outside of the EEA there may + be some data transfer to you or your device; +
  • +
  • + from time to time your information may be stored in devices which are + used by affiliates engaged by the Trustlines Foundation when outside + of the EEA. +
  • +
+

+ If we transfer your information outside of the EEA in a systematic way, + and the third country or international organisation in question has not + been deemed by the EU Commission to have adequate data protection laws, + we will provide appropriate safeguards and your privacy rights will + continue to be enforceable against us as outlined in this notice. +

+ +

7. Your rights

+

+ In accordance with the GDPR and data protection laws applicable to you, + the following rights may be available to you: The right (1) to be + informed about our data processing activities (Art. 15 GDPR) (2) of + access to the Personal Information we hold about you (Art. 15 GDPR) (3) + to correction of your Personal Information if it is incomplete or + inaccurate (Art. 16 GDPR) (4) to deletion of your Personal Information + (Art. 17 GDPR) or (5) to restriction or limitation of the processing of + your Personal Information (Art. 18 GDPR) and (6) to object to the + processing of your Personal Information (Art. 21 GDPR) and (7) to + receive copies of your Personal Information in a commonly used form and + to transfer it directly to a third party (Art. 20 GDPR). If we are + relying on your consent as the basis on which we are processing your + Personal Information, you have the right at any time to withdraw your + consent with future effect. Regarding your newsletter subscription, + there is a corresponding link in every newsletter. You can also + unsubscribe directly from this website at any time or inform us of your + revocation. Any request made under (2) above (a subject access request) + or any other notification in respect of your rights must be sent to us + by email to: contact@trustlines.foundation or by post to: Trustlines + Foundation, Industriering 40, 9491 Ruggell, Liechtenstein. +

+

+ If you are of the opinion that the Trustline Foundation’s data + processing activities infringe upon applicable data protection + regulations or your personal data protection rights, you have the rights + to report this to the competent supervisory authority in Liechtenstein. +

+

+ Datenschutzstelle Liechtenstein +
+ Städtle 38 +
+ 9490 Vaduz +
+ Liechtenstein +

+ +

8. Data retention

+

+ If you sign up for a newsletter through the Site or we acquire your + Personal Information due to another interaction with the Trustlines + Foundation, we will generally maintain your Personal Information for our + records for twelve months unless you ask us to delete this information + before such time has passed. We may retain your Personal Information for + more than twelve months if we consider it is necessary to enable us to + satisfactorily provide the information on the Site or administer the + Site or to enable us to comply with any statutory, legal or contractual + obligation we may have. We review the Personal Information (and the + categories of personal data) we are holding on a regular basis to ensure + the data we are holding is still accurate and is still relevant to the + Trustlines Foundation and its Site. If we discover that certain data we + are holding is no longer necessary or accurate, we will take reasonable + steps to correct or delete this data as may be required. Generally the + Personal Information will be deleted as soon as it is no longer required + for the purpose of the collection. Regarding Device Information this is + generally the case when the respective session has ended. The server log + files will be deleted after 7 days. +

+ +

9. Security

+

+ We will take all reasonable steps to ensure that appropriate technical + and organisational measures are carried out in order to safeguard your + Personal Information and protect against unlawful access and accidental + loss or damage. These measures may include (as necessary): +

+
    +
  • compliance with our internal IT security procedures;
  • +
  • protecting our servers by both hardware and software firewalls;
  • +
  • + locating our data processing storage facilities in secure locations; +
  • +
  • + using appropriate support tool and mailing list management providers; +
  • +
  • + when necessary, disposing of or deleting your data in a secure manner; + and +
  • +
  • regularly backing up and encrypting all data we hold.
  • +
+

+ We will ensure that all contractors, staff or individuals engaged by us + are aware of their privacy and data security obligations. We will take + reasonable steps to ensure that the employees of third parties working + on our behalf or providing services to us are aware of their privacy and + data security obligations. Unfortunately, the transmission of + information via the internet is not completely secure. Although we will + do our best to protect your personal data, we cannot guarantee the + security of your data transmitted by you to us by email. Once we have + received your information, we will use the procedures and security + features referred to in this Privacy Policy to try to prevent + unauthorised access. +

+ +

10. Web analysis tool Matomo

+

+ We analyze the use of our website with the open source web analysis tool + Matomo (formerly Piwik; data protection declaration can be found at{" "} + + https://matomo.org/privacy-policy/ + + ). For this purpose, the automatically collected IP addresses are + anonymized before the evaluation. The analysis is therefore based on + anonymized data records and there is no personal evaluation. The web + analysis is used exclusively to optimize the website in terms of + user-friendliness and to provide useful information about our services. + This data is not merged with other personal data sources or passed on to + third parties. +

+ +

+ In addition to the data mentioned, Matomo also uses so-called “cookies” + (see below). +

+ +

+ +

+ +

11. Cookies

+

+ We use cookies on our website to make our offer user-friendly (Matomo, + see above). Cookies are small files that your browser automatically + creates and that are stored on your end device (laptop, tablet, + smartphone, etc.) when you visit our website. The cookies remain stored + until you delete them. This enables us to recognize your browser the + next time you visit. +

+ +

+ If you do not want this, you can set up your browser so that it informs + you about the setting of cookies and allows them in individual cases. + However, we would like to point out that deactivation means that you may + not be able to use all functions of our website. +

+ +

12. Changes

+

+ We may update this privacy policy from time to time in order to reflect, + for example, changes to our practices or for other operational, legal or + regulatory reasons. +

+ +

13. Contact us

+

+ For more information about our privacy practices, if you have questions, + or if you would like to make a complaint, please contact us by e-mail at{" "} + + contact@trustlines.foundation + +

+ +

Imprint

+

+ Trustlines Foundation +
+ Legal seat: Vaduz +
+ Registered ad correspondence address: +
+ Industriering 40 +
+ 9491 Ruggell +
+ Liechtenstein +
+ contact@trustlines.foundation +

+

+ Handelsregisternummer: +
+ FL-0002.592.935-7 ‍ +

+ +

 

+

 

+
+ ); +} diff --git a/src/features/common/components/seo.jsx b/src/features/common/components/seo.jsx new file mode 100644 index 0000000..abf5e55 --- /dev/null +++ b/src/features/common/components/seo.jsx @@ -0,0 +1,63 @@ +import React from "react"; +import { Helmet } from "react-helmet"; + +export function SEO({ + description = "", + lang = "en", + meta = [], + keywords = [], + title = "", +}) { + return ( + 0 + ? { + name: `keywords`, + content: keywords.join(`, `), + } + : [] + ) + .concat(meta)} + title={title} + titleTemplate={`%s | Trustlines Foundation`} + /> + ); +} diff --git a/src/features/common/components/social-links.jsx b/src/features/common/components/social-links.jsx new file mode 100644 index 0000000..0c8b3e8 --- /dev/null +++ b/src/features/common/components/social-links.jsx @@ -0,0 +1,39 @@ +import React from "react"; + +import { Telegram } from "./icons/telegram"; +import { Twitter } from "./icons/twitter"; +import { YouTube } from "./icons/you-tube"; + +import * as URLS from "../content/social-links.json"; + +const LINKS = [ + { + icon: , + href: URLS.twitter, + }, + { + icon: , + href: URLS.youTube, + }, + { + icon: , + href: URLS.telegram, + }, +]; + +export function SocialLinks() { + return ( +
+ {LINKS.map((link, i) => ( + + {link.icon} + + ))} +
+ ); +} diff --git a/src/features/common/components/toggle-switch.jsx b/src/features/common/components/toggle-switch.jsx new file mode 100644 index 0000000..5ff08d4 --- /dev/null +++ b/src/features/common/components/toggle-switch.jsx @@ -0,0 +1,22 @@ +import React from "react"; + +export default function ToggleSwitch(props) { + return ( +
+
+ + +
+ +
+ ); +} diff --git a/src/features/common/constants.js b/src/features/common/constants.js new file mode 100644 index 0000000..4bb45fd --- /dev/null +++ b/src/features/common/constants.js @@ -0,0 +1,2 @@ +export const TLN_BASE = 1000000000000000000; +export const SCROLL_THRESHOLD = 5; diff --git a/src/features/common/content/footer-groups.json b/src/features/common/content/footer-groups.json new file mode 100644 index 0000000..baff2f9 --- /dev/null +++ b/src/features/common/content/footer-groups.json @@ -0,0 +1,66 @@ +[ + { + "label": "Websites", + "items": [ + { + "label": "Blog", + "href": "https://blog.trustlines.network/" + }, + { + "label": "Docs", + "href": "https://docs.trustlines.network/" + }, + { + "label": "Trustlines Network", + "href": "https://trustlines.network/" + } + ] + }, + { + "label": "Community", + "items": [ + { + "label": "Forum", + "href": "https://forum.trustlines.network/" + }, + { + "label": "Newsletter", + "href": "http://eepurl.com/gHqYyX" + }, + { + "label": "Telegram", + "href": "https://t.me/trustlines_network" + } + ] + }, + { + "label": "Developers", + "items": [ + { + "label": "GitHub", + "href": "https://github.com/trustlines-protocol" + }, + { + "label": "Dev Docs", + "href": "https://dev.trustlines.network/" + }, + { + "label": "Technical Chat", + "href": "https://gitter.im/trustlines/community" + } + ] + }, + { + "label": "Legal", + "items": [ + { + "label": "Privacy Policy & Legal Imprint", + "href": "" + }, + { + "label": "Merkle Drop Terms & Conditions", + "href": "" + } + ] + } +] diff --git a/src/features/common/content/nav-bar-links.json b/src/features/common/content/nav-bar-links.json new file mode 100644 index 0000000..5f9fd46 --- /dev/null +++ b/src/features/common/content/nav-bar-links.json @@ -0,0 +1,22 @@ +[ + { + "label": "Protocol", + "to": "/protocol" + }, + { + "label": "Auction", + "to": "/auction" + }, + { + "label": "Merkle Drop", + "to": "/merkle-drop" + }, + { + "label": "Grants", + "to": "/grants" + }, + { + "label": "Contact", + "to": "/contact" + } +] diff --git a/src/features/common/content/social-links.json b/src/features/common/content/social-links.json new file mode 100644 index 0000000..882b471 --- /dev/null +++ b/src/features/common/content/social-links.json @@ -0,0 +1,6 @@ +{ + "twitter": "https://twitter.com/TrustlinesFound", + "youTube": "https://www.youtube.com/channel/UCdNvItQZDL8Qj0HJGIM_AYA", + "telegram": "https://t.me/trustlines_network", + "linkedIn": "" +} diff --git a/src/features/common/hooks/account.js b/src/features/common/hooks/account.js new file mode 100644 index 0000000..c56ca45 --- /dev/null +++ b/src/features/common/hooks/account.js @@ -0,0 +1,44 @@ +import { useState, useEffect } from "react"; +import getWeb3, { getDefaultAccount } from "../api/web3"; + +export function useAccount() { + const [account, setAccount] = useState(""); + + useEffect(() => { + // box variable to make it available in inner function + const env = { + checkAccountIntervalId: 0, + }; + + // define async function because effect function can not be async + async function checkAccount() { + try { + if (getWeb3()) { + // function that periodically checks the chain state + async function check() { + try { + setAccount(await getDefaultAccount()); + } catch (e) { + console.error(e); + // Stop the periodical address check + clearInterval(env.checkAccountIntervalId); + setAccount(undefined); + } + } + + env.checkAccountIntervalId = setInterval(check, 500); + check(); + } else { + setAccount(undefined); + } + } catch (error) { + console.log(error); + setAccount(undefined); + } + } + + checkAccount(); + return () => clearInterval(env.checkAccountIntervalId); + }, []); + return account; +} diff --git a/src/features/common/hooks/chain-state.js b/src/features/common/hooks/chain-state.js new file mode 100644 index 0000000..197c4a8 --- /dev/null +++ b/src/features/common/hooks/chain-state.js @@ -0,0 +1,56 @@ +import { useState, useEffect } from "react"; +import getWeb3, { verifyChainId } from "../api/web3"; + +export const CHAIN_STATE = { + CONNECTING: "connecting", + CONNECTED: "connected", + WRONG_CHAIN: "wrongChain", + CHAIN_UNKNOWN: "chainUnknown", + DISCONNECTED: "disconnected", +}; + +export function useChainState() { + const [chainState, setChainState] = useState(CHAIN_STATE.CONNECTING); + + useEffect(() => { + // box variable to make it available in inner function + const env = { + chainCheckIntervalId: 0, + }; + + // define async function because effect function can not be async + async function connect() { + try { + if (getWeb3()) { + // function that periodically checks the chain state + async function checkChain() { + try { + if (await verifyChainId(process.env.REACT_APP_CHAIN_ID)) { + setChainState(CHAIN_STATE.CONNECTED); + } else { + setChainState(CHAIN_STATE.WRONG_CHAIN); + } + } catch (e) { + console.error(e); + // Stop the periodical chain check + clearInterval(env.chainCheckIntervalId); + setChainState(CHAIN_STATE.CHAIN_UNKNOWN); + } + } + + env.chainCheckIntervalId = setInterval(checkChain, 500); + checkChain(); + } else { + setChainState(CHAIN_STATE.DISCONNECTED); + } + } catch (error) { + console.log(error); + setChainState(CHAIN_STATE.DISCONNECTED); + } + } + + connect(); + return () => clearInterval(env.chainCheckIntervalId); + }, []); + return chainState; +} diff --git a/src/features/common/hooks/window.js b/src/features/common/hooks/window.js new file mode 100644 index 0000000..968faff --- /dev/null +++ b/src/features/common/hooks/window.js @@ -0,0 +1,26 @@ +import { useState, useEffect } from "react"; + +function getWindowDimensions() { + const { innerWidth: width, innerHeight: height } = window; + return { + width, + height, + }; +} + +export default function useWindowDimensions() { + const [windowDimensions, setWindowDimensions] = useState( + getWindowDimensions() + ); + + useEffect(() => { + function handleResize() { + setWindowDimensions(getWindowDimensions()); + } + + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, []); + + return windowDimensions; +} diff --git a/src/features/common/utils/blockexplorer.js b/src/features/common/utils/blockexplorer.js new file mode 100644 index 0000000..94b8051 --- /dev/null +++ b/src/features/common/utils/blockexplorer.js @@ -0,0 +1,3 @@ +export function generateTransactionUrl(tx_hash) { + return `${process.env.REACT_APP_EXPLORER_URL}tx/${tx_hash}`; +} diff --git a/src/features/common/utils/math.js b/src/features/common/utils/math.js new file mode 100644 index 0000000..b75d13e --- /dev/null +++ b/src/features/common/utils/math.js @@ -0,0 +1,21 @@ +import { TLN_BASE } from "../constants"; +import { unit, createUnit, format } from "mathjs"; + +createUnit({ TLN: { prefixes: "short", baseName: "TLN" } }); + +export function roundUp(price) { + return Math.ceil((price / TLN_BASE) * 1000) / 1000; +} + +export function parseTokenAmount(amount) { + return ( + parseInt(amount) / Math.pow(10, process.env.REACT_APP_TOKEN_DECIMALS) + ).toFixed(process.env.REACT_APP_SHOW_DECIMALS); +} + +export function formatTLNAmount(amount) { + const unitInInternationalSystem = format(unit(roundUp(amount), "TLN"), 4); + // We want a space in between the prefix and `TLN` so we slice it and add it back with a space + const unitWithoutBase = unitInInternationalSystem.slice(0, -3); + return unitWithoutBase + "TLN"; +} diff --git a/src/features/common/utils/newsletter.js b/src/features/common/utils/newsletter.js new file mode 100644 index 0000000..ab469ae --- /dev/null +++ b/src/features/common/utils/newsletter.js @@ -0,0 +1,22 @@ +export function signupNewsletter(email) { + const NEWSLETTER_URL = process.env.REACT_APP_MAILCHIMP_URL; + const myHeaders = new Headers(); + myHeaders.append("Content-Type", "application/json"); + const requestOptions = { + method: "POST", + mode: "cors", + headers: myHeaders, + body: JSON.stringify({ email }), + redirect: "follow", + }; + + fetch(NEWSLETTER_URL, requestOptions) + .then(response => response.json()) + .then(result => { + console.log({ result }); + }) + .catch(error => { + console.log(error); + alert("something went wrong"); + }); +} diff --git a/src/features/common/utils/validation.js b/src/features/common/utils/validation.js new file mode 100644 index 0000000..34a38fe --- /dev/null +++ b/src/features/common/utils/validation.js @@ -0,0 +1,3 @@ +export function isEmailValid(email) { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); +} diff --git a/src/features/contact/components/contact-form.jsx b/src/features/contact/components/contact-form.jsx new file mode 100644 index 0000000..aa0a335 --- /dev/null +++ b/src/features/contact/components/contact-form.jsx @@ -0,0 +1,216 @@ +import React, { useState, useCallback } from "react"; + +import { Card } from "../../common/components/card"; +import { LinkButton } from "../../common/components/link-button"; +import ToggleSwitch from "../../common/components/toggle-switch"; + +import { Mail } from "../../common/components/icons/mail"; +import { MessageBox } from "../../common/components/icons/message-box"; +import { AtSign } from "../../common/components/icons/at-sign"; +import { Send } from "../../common/components/icons/send"; +import { Check } from "../../common/components/icons/check"; + +import { signupNewsletter } from "../../common/utils/newsletter"; +import PrivacyPolicyModal from "../../common/components/privacy-modal"; + +export default function ContactForm(props) { + const [showSuccessMessage, setShowSuccessMessage] = useState(false); + const [showErrorMessage, setShowErrorMessage] = useState(false); + const [showPrivacyModal, setShowPrivacyModal] = useState(false); + const [acceptSignupNewsletter, setAcceptSignupNewsletter] = useState(false); + const [errors, setErrors] = useState(false); + const CONTACT_MAIL = process.env.GATSBY_CONTACT_MAIL; + const FORM_POST_URL = process.env.GATSBY_FORM_POST_URL; + + const submitForm = e => { + e.preventDefault(); + setShowSuccessMessage(false); + setShowErrorMessage(false); + setErrors(false); + + const data = new FormData(e.target); + + const jsonData = Object.fromEntries(data.entries()); + + const updatedJsonData = { + ...jsonData, + topic: "Trustlines Foundation", + name: jsonData.email, + }; + + const myHeaders = new Headers(); + myHeaders.append("Content-Type", "application/json"); + const requestOptions = { + method: "POST", + mode: "cors", + headers: myHeaders, + body: JSON.stringify(updatedJsonData), + redirect: "follow", + }; + + fetch(FORM_POST_URL, requestOptions) + .then(response => response.json()) + .then(result => { + if (result.success) { + document.getElementById("contactUs").reset(); + if (acceptSignupNewsletter) { + signupNewsletter(jsonData.email); + } + setShowSuccessMessage(true); + } else { + setErrors(result.errors); + setShowErrorMessage(true); + } + }) + .catch(error => { + console.log(error); + alert("something went wrong"); + }); + }; + + const togglePrivacyModal = useCallback(() => { + setShowPrivacyModal(!showPrivacyModal); + }, [showPrivacyModal]); + + return ( +
+ {showErrorMessage && ( +
+
+ There was a problem submitting your message: + {Object.values(errors).map((error, i) => { + return
{error}
; + })} +
+ +
+ )} + + {showSuccessMessage && ( +
+ Your message was submitted successfully. We'll get back to you + ASAP. + +
+ )} +
+ +
+
+

+ Drop us a line, inquire about open positions, or give us + feedback +

+
+
+
+
+
+ +
+