Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Add RAD faucet
Sebastian Martinez committed 4 years ago
commit f22d513f7938db81811b867374e2ba0e8a42f5a2
parent f47ae92a15ebf128e6796e4984d4fe61043b130e
10 files changed +313 -19
modified public/index.css
@@ -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;
}
modified src/App.svelte
@@ -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} />
added src/base/faucet/Faucet.ts
@@ -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);
+
}
added src/base/faucet/Index.svelte
@@ -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>
added src/base/faucet/Routes.svelte
@@ -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>
added src/base/faucet/Submit.svelte
@@ -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}
modified src/base/registrations/Update.svelte
@@ -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;
modified src/config.json
@@ -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)"]
  }
}
modified src/config.ts
@@ -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[] };
modified src/utils.ts
@@ -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;