Improve vesting discovery and error messages

When loading a ETH address as vesting contract we show a cryptic error
message, which non tech-savy end users don't understand, this commit
changes it to something more human readable.

We allow users to enter and load a contract that probably isn't a
vesting contract.

The `getInfo` function in the `vesting` folder is very prone to possible
errors, and benefits from some parallelization between all the method
calls.

We allow hereby to enter a contract address in search and once on the
contract profile page we provide a link to the detailed vesting
information if available.

Signed-off-by: Sebastian Martinez <me@sebastinez.dev>
This commit is contained in:
Sebastian Martinez
2023-01-10 14:15:08 +01:00
parent 8df7d66419
commit b2c82b06bd
12 changed files with 489 additions and 321 deletions
+16 -4
View File
@@ -16,10 +16,12 @@ import { base } from "@app/lib/router";
import { config } from "@app/lib/config";
import { getAddress, getResolver } from "@app/lib/registrar";
import { getAvatar, getSeed, getRegistration } from "@app/lib/registrar";
import { getInfo } from "@app/lib/vesting";
export enum AddressType {
Contract,
Org,
Vesting,
EOA,
}
@@ -127,6 +129,11 @@ export function formatAddress(input: string): string {
);
}
// Returns a shortened Ethereum transaction hash
export function formatTx(input: string): string {
return input.substring(0, 20) + "…";
}
export function formatCommit(oid: string): string {
return oid.substring(0, 7);
}
@@ -288,12 +295,13 @@ export function isFulfilled<T>(
return input.status === "fulfilled";
}
// Get the explorer link of an address, eg. Etherscan.
export function explorerLink(addr: string, wallet: Wallet): string {
// Get the explorer link of an address or tx, eg. Etherscan.
export function explorerLink(addrOrTx: string, wallet: Wallet): string {
const type = isAddress(addrOrTx) ? "address" : "tx";
if (wallet.network.name === "goerli") {
return `https://goerli.etherscan.io/address/${addr}`;
return `https://goerli.etherscan.io/${type}/${addrOrTx}`;
}
return `https://etherscan.io/address/${addr}`;
return `https://etherscan.io/${type}/${addrOrTx}`;
}
// Format a name.
@@ -349,6 +357,10 @@ export async function identifyAddress(
const bytes = ethers.utils.arrayify(code);
if (bytes.length > 0) {
const info = await getInfo(address, wallet);
if (info) {
return AddressType.Vesting;
}
return AddressType.Contract;
}
return AddressType.EOA;
+120 -41
View File
@@ -1,12 +1,13 @@
import type { Wallet } from "@app/lib/wallet";
import type { TransactionResponse } from "@ethersproject/abstract-provider";
import { ethers } from "ethers";
import { BigNumber, ethers } from "ethers";
import { writable } from "svelte/store";
import * as cache from "@app/lib/cache";
import * as session from "@app/lib/session";
import * as utils from "@app/lib/utils";
import ethereumContractAbis from "@app/lib/ethereum/contractAbis.json";
import { assert } from "@app/lib/error";
export interface VestingInfo {
token: string;
@@ -20,15 +21,24 @@ export interface VestingInfo {
vestingPeriod: string;
}
export const state = writable<
"idle" | "loading" | "withdrawingSign" | "withdrawing" | "withdrawn"
>("idle");
export type VestingState =
| { type: "idle" }
| { type: "loading" }
| { type: "error"; error: string }
| { type: "withdrawingSign" }
| { type: "withdrawing"; tx: TransactionResponse }
| { type: "withdrawn" };
export const state = writable<VestingState>({ type: "idle" });
export async function withdrawVested(
address: string,
wallet: Wallet,
): Promise<void> {
assert(wallet.signer);
if (!wallet.signer) {
state.set({ type: "error", error: "No signer available" });
return;
}
const contract = new ethers.Contract(
address,
@@ -37,50 +47,119 @@ export async function withdrawVested(
);
const signer = wallet.signer;
state.set("withdrawingSign");
state.set({ type: "withdrawingSign" });
const tx = await contract.connect(signer).withdrawVested();
try {
const tx: TransactionResponse = await contract
.connect(signer)
.withdrawVested();
state.set("withdrawing");
await tx.wait();
state.set({ type: "withdrawing", tx });
await tx.wait();
} catch (e) {
handleEtherErrorState(e, "Unknown error, check the dev console");
return;
}
session.state.refreshBalance(wallet);
state.set("withdrawn");
state.set({ type: "withdrawn" });
}
export async function getInfo(
address: string,
wallet: Wallet,
): Promise<VestingInfo> {
const contract = new ethers.Contract(
export const getInfo = cache.cached(
async (address: string, wallet: Wallet): Promise<VestingInfo | undefined> => {
const contract = getVestingContract(address, wallet);
let vestingInfo:
| [
string,
string,
BigNumber,
BigNumber,
BigNumber,
string,
string,
string,
]
| undefined = undefined;
let token: string | undefined = undefined;
try {
token = await contract.token();
if (!token) {
return undefined;
}
const tokenContract = new ethers.Contract(
token,
ethereumContractAbis.token,
wallet.provider,
);
vestingInfo = await Promise.all([
tokenContract.symbol(),
contract.beneficiary(),
contract.withdrawableBalance(),
contract.withdrawn(),
contract.totalVestingAmount(),
contract.vestingStartTime(),
contract.vestingPeriod(),
contract.cliffPeriod(),
]);
} catch (e) {
console.warn(e);
return undefined;
}
const [
symbol,
beneficiary,
withdrawable,
withdrawn,
total,
vestingStartTime,
vestingPeriod,
cliffPeriod,
] = vestingInfo;
return {
token,
symbol,
beneficiary,
withdrawableBalance: utils.formatBalance(withdrawable),
withdrawn: utils.formatBalance(withdrawn),
totalVesting: utils.formatBalance(total),
vestingStartTime,
vestingPeriod,
cliffPeriod,
};
},
address => address,
{ max: 1000 },
);
export function parseVestingPeriods(...timestamps: string[]): string {
const sum = timestamps
.map(timestamp => parseInt(timestamp))
.reduce((prev, curr) => prev + curr, 0);
return new Date(sum * 1000).toDateString();
}
export function getVestingContract(address: string, wallet: Wallet) {
return new ethers.Contract(
address,
ethereumContractAbis.vesting,
wallet.provider,
);
const token = await contract.token();
const beneficiary = await contract.beneficiary();
const withdrawable = await contract.withdrawableBalance();
const withdrawn = await contract.withdrawn();
const total = await contract.totalVestingAmount();
const vestingStartTime = await contract.vestingStartTime();
const vestingPeriod = await contract.vestingPeriod();
const cliffPeriod = await contract.cliffPeriod();
}
const tokenContract = new ethers.Contract(
token,
ethereumContractAbis.token,
wallet.provider,
);
const symbol = await tokenContract.symbol();
export function handleEtherErrorState(e: unknown, message: string): void {
const error =
typeof e === "object" &&
e !== null &&
"reason" in e &&
typeof e.reason === "string"
? e.reason
: message;
return {
token,
symbol,
beneficiary,
totalVesting: utils.formatBalance(total),
withdrawableBalance: utils.formatBalance(withdrawable),
withdrawn: utils.formatBalance(withdrawn),
vestingStartTime,
vestingPeriod,
cliffPeriod,
};
state.set({ type: "error", error });
console.warn(e);
}