mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-16 03:40:37 +08:00
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:
+16
-4
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user