Add RAD faucet

Signed-off-by: Sebastian Martinez <sebastinez@me.com>
This commit is contained in:
Sebastian Martinez
2021-11-09 15:57:24 +01:00
parent f47ae92a15
commit f22d513f79
10 changed files with 313 additions and 19 deletions
+9 -4
View File
@@ -247,10 +247,10 @@ a.address {
border-bottom-color: transparent;
}
input[type="text"], button {
input[type="text"], input[type="number"], button {
line-height: 1.6;
}
input[type="text"] {
input[type="text"], input[type="number"] {
outline: none;
border: none;
font-size: 1rem;
@@ -261,13 +261,18 @@ input[type="text"] {
padding: 1rem 1.5rem;
margin: 1rem;
}
input[type="text"]::placeholder {
input[type="text"]::placeholder, input[type="number"]::placeholder {
color: var(--color-secondary);
opacity: 1 !important;
}
input[type="text"].small {
input[type="text"].small, input[type="number"].small {
font-size: 0.875rem;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input.wide {
width: 44ch;
}
+2
View File
@@ -8,6 +8,7 @@
import Registrations from '@app/base/registrations/Routes.svelte';
import Orgs from '@app/base/orgs/Routes.svelte';
import Users from '@app/base/users/Routes.svelte';
import Faucet from '@app/base/faucet/Routes.svelte';
import Projects from '@app/base/projects/Routes.svelte';
import Resolver from '@app/base/resolver/Routes.svelte';
import Header from '@app/Header.svelte';
@@ -82,6 +83,7 @@
</Route>
<Registrations {config} session={$session} />
<Orgs {config} />
<Faucet {config} />
<Users {config} />
<Projects {config} />
<Resolver {config} />
+56
View File
@@ -0,0 +1,56 @@
import * as ethers from 'ethers';
import type { Config } from '@app/config';
import { assert } from '@app/error';
import type { TransactionResponse } from '@ethersproject/providers';
import { parseUnits } from '@ethersproject/units';
export async function withdraw(amount: number, config: Config): Promise<TransactionResponse> {
assert(config.signer);
const faucet = new ethers.Contract(
config.radToken.faucet,
config.abi.faucet,
config.signer
);
return faucet.withdraw(config.radToken.address, ethers.utils.parseUnits(amount.toString()));
}
export async function getMaxWithdrawAmount(config: Config): Promise<ethers.BigNumber> {
assert(config.signer);
const faucet = new ethers.Contract(
config.radToken.faucet,
config.abi.faucet,
config.signer
);
return faucet.maxWithdrawAmount();
}
export async function calculateTimeLock(amount: ethers.BigNumber, config: Config): Promise<ethers.BigNumber> {
assert(config.signer);
const faucet = new ethers.Contract(
config.radToken.faucet,
config.abi.faucet,
config.signer
);
return faucet.calculateTimeLock(parseUnits(amount.toString()));
}
export async function lastWithdrawalByUser(config: Config): Promise<ethers.BigNumber> {
assert(config.signer);
const address = config.signer.getAddress();
const faucet = new ethers.Contract(
config.radToken.faucet,
config.abi.faucet,
config.signer
);
return faucet.lastWithdrawalByUser(address);
}
+121
View File
@@ -0,0 +1,121 @@
<script lang="ts">
import type { Config } from "@app/config";
import { BigNumber } from "@ethersproject/bignumber";
import { formatEther, parseUnits } from "@ethersproject/units";
import type { ethers } from "ethers";
import { onMount } from "svelte";
import { navigate } from "svelte-routing";
import { getMaxWithdrawAmount, lastWithdrawalByUser, calculateTimeLock } from "./Faucet";
export let config: Config;
let amount = 0;
let maxWithdrawAmount: BigNumber;
let lastWithdrawal: BigNumber;
let error: string | undefined;
async function withdraw() {
const [state, message] = await isAbleToWithdraw(amountBN);
if (state === true) navigate("/faucet/submit", { state: { amount } });
else error = message;
}
async function isAbleToWithdraw(amount: ethers.BigNumber): Promise<[boolean, string?]> {
if (amount.isZero()) { return [false, "Not able to withdraw zero tokens"]; }
if (parseUnits(amountBN.toString()).gt(maxWithdrawAmount)) return [false, `Reduce amount, max withdrawal is ${formatEther(maxWithdrawAmount)}`];
let currentTime = new Date().getTime();
let timelock = await calculateTimeLock(amountBN, config);
let nextAvailableWithdraw = lastWithdrawal.add(timelock).mul(Math.pow(10,3));
if (nextAvailableWithdraw.gt(currentTime)) return [false, `Not ready to withdraw, return after ${new Date(nextAvailableWithdraw.toNumber()).toUTCString()}`];
return [true];
}
onMount(async () => {
maxWithdrawAmount = await getMaxWithdrawAmount(config);
lastWithdrawal = await lastWithdrawalByUser(config);
});
$: amountBN = amount ? BigNumber.from(amount) : BigNumber.from(0);
</script>
<style>
div.input-caption {
font-size: 1.25rem;
text-align: left;
margin-left: 1.5rem;
padding-left: 1.5rem;
color: var(--color-secondary);
}
div.input-main {
display: flex;
align-items: flex-start;
flex-direction: column;
margin: 1rem 1.5rem 0rem;
color: var(--color-secondary);
}
input[type="number"] {
margin: 0;
margin-right: 1.5rem;
}
.name {
display: flex;
flex-direction: row;
margin: 1rem;
margin-bottom: 0;
}
.error {
padding-left: 1rem;
padding-top: 1rem;
}
.description :global(p) {
padding: 0;
margin: 0;
}
.description.invalid {
color: var(--color-negative) !important;
}
</style>
<svelte:head>
<title>Radicle: Faucet</title>
</svelte:head>
<main class="off-centered">
<div>
{#if config.network.name == "homestead"}
<div class="input-caption">
To get RAD tokens on <strong>{config.network.name}</strong>, please
check the known exchanges.
</div>
{:else if !config.signer}
<div class="input-caption">
To get RAD tokens on <strong>{config.network.name}</strong>, please
connect your wallet.
</div>
{:else}
<div class="input-caption">
Obtain RAD tokens on <strong>{config.network.name}</strong>
</div>
<div class="input-main">
<div class="name">
<input
type="number"
placeholder="Set amount to withdraw"
bind:value={amount}
on:input={() => error = ""}
/>
<button disabled={false} class="primary" on:click={withdraw}>
Withdraw
</button>
</div>
{#if error}
<div class="error description invalid text-small faded">
{error}
</div>
{/if}
</div>
{/if}
</div>
</main>
+16
View File
@@ -0,0 +1,16 @@
<script lang="ts">
import { Route } from "svelte-routing";
import Index from "@app/base/faucet/Index.svelte";
import type { Config } from "@app/config";
import Submit from "./Submit.svelte";
export let config: Config;
</script>
<Route path="faucet">
<Index {config} />
</Route>
<Route path="faucet/submit">
<Submit {config} />
</Route>
+86
View File
@@ -0,0 +1,86 @@
<script lang="ts">
import { onMount } from "svelte";
import { navigate } from "svelte-routing";
import type { Config } from "@app/config";
import Loading from "@app/Loading.svelte";
import Modal from "@app/Modal.svelte";
import Err from "@app/Error.svelte";
import { Status, State } from "@app/utils";
import { withdraw } from "./Faucet";
import { session } from '@app/session';
export let config: Config;
let error: Error;
let state: State = {
status: Status.Failed,
error: "Error withdrawing, something happened.",
};
$: requester = ($session && $session.address);
const back = () => navigate(`/faucet`);
onMount(async () => {
try {
state.status = Status.Signing;
const tx = await withdraw(window.history.state.amount, config);
state.status = Status.Pending;
await tx.wait();
state.status = Status.Success;
} catch (e) {
console.error(e);
error = e;
state = { status: Status.Failed, error: e.message };
}
});
</script>
<style>
.loader {
display: flex;
flex-direction: column;
align-items: center;
}
</style>
{#if error}
<Err
title="Transaction failed"
message={error.message}
on:close={() => navigate("/faucet")}
/>
{:else}
<Modal>
<span slot="title">
{#if state.status === Status.Success}
<div>🎉</div>
{:else}
<div>🌐</div>
{/if}
Withdrawal
</span>
<span slot="subtitle">
{#if state.status === Status.Signing}
Signing transaction. Please confirm in your wallet.
{:else if state.status === Status.Pending}
Awaiting transaction.
{/if}
</span>
<span slot="body" class="loader">
{#if state.status === Status.Success}
The amount of {window.history.state.amount.toString()} RAD tokens has been successfully transfered to
<span class="highlight">{requester}</span>
{:else}
<Loading small center />
{/if}
</span>
<span slot="actions">
{#if state.status === Status.Success}
<button on:click={back} class="register"> Back </button>
{/if}
</span>
</Modal>
{/if}
+1 -13
View File
@@ -6,19 +6,7 @@
import type { Config } from '@app/config';
import Loading from '@app/Loading.svelte';
import Modal from '@app/Modal.svelte';
enum Status {
Signing,
Pending,
Success,
Failed,
}
type State =
{ status: Status.Signing }
| { status: Status.Pending }
| { status: Status.Success }
| { status: Status.Failed; error: string };
import { Status, State } from "@app/utils";
export let subdomain: string;
export let config: Config;
+8 -1
View File
@@ -42,7 +42,8 @@
"address": "0x80b68878442b6510D768Be1bd88712710B86eAcD"
},
"radToken": {
"address": "0x7b6CbebC5646D996d258dcD4ca1d334B282e9948"
"address": "0x7b6CbebC5646D996d258dcD4ca1d334B282e9948",
"faucet": "0x8d9BeE9cA51fd3beb5692F6b404Fe9f76E5F3d1b"
},
"orgFactory": {
"address": "0xF3D04e874D07d680e8b26332eEae5b9B1c263121"
@@ -128,6 +129,12 @@
"function withdrawableBalance() view returns (uint256)",
"function withdrawVested()"
],
"faucet": [
"function lastWithdrawalByUser(address) view returns (uint256)",
"function maxWithdrawAmount() view returns (uint256)",
"function calculateTimeLock(uint256) view returns (uint256)",
"function withdraw(address, uint256)"
],
"ens": ["function owner(bytes32 node) view returns (address)"]
}
}
+1 -1
View File
@@ -27,7 +27,7 @@ export type WalletConnectState =
export class Config {
network: { name: string; chainId: number };
registrar: { address: string; domain: string };
radToken: { address: string };
radToken: { address: string; faucet: string };
orgFactory: { address: string };
reverseRegistrar: { address: string };
orgs: { subgraph: string; contractHash: string; pinned: string[] };
+13
View File
@@ -39,6 +39,19 @@ export interface Token {
balance: BigNumber;
}
export enum Status {
Signing,
Pending,
Success,
Failed,
}
export type State =
{ status: Status.Signing }
| { status: Status.Pending }
| { status: Status.Success }
| { status: Status.Failed; error: string };
export async function isReverseRecordSet(address: string, domain: string, config: Config): Promise<boolean> {
const name = await config.provider.lookupAddress(address);
return name === domain;