Skip to content

Commit 9c40e2e

Browse files
committed
support approving tokens from Alchemy ; check allowance ; added more redeem time and amount ; show error if not enough balance in the wallet to lock
1 parent 05e3dec commit 9c40e2e

File tree

7 files changed

+131
-30
lines changed

7 files changed

+131
-30
lines changed

src/@store/arc/arcActions.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Address, DAO, IProposalCreateOptions, IProposalOutcome, ITransactionState, ITransactionUpdate, ReputationFromTokenScheme, Scheme, CL4RScheme } from "@daostack/arc.js";
1+
import Arc, { Address, DAO, IProposalCreateOptions, IProposalOutcome, ITransactionState, ITransactionUpdate, ReputationFromTokenScheme, Scheme, CL4RScheme, Token } from "@daostack/arc.js";
22
import { IAsyncAction } from "@store/async";
33
import { toWei, getArcByDAOAddress } from "lib/util";
44
import { IRedemptionState } from "lib/proposalHelpers";
@@ -281,3 +281,22 @@ export const redeemLocking = (cl4rScheme: CL4RScheme, beneficiary: string, locki
281281
cl4rScheme.redeem(beneficiary, lockingIds).subscribe(...observer);
282282
};
283283
};
284+
285+
/**
286+
* A generic function to approve spending of a token in a scheme.
287+
* The default allowance is 100,000 tokens.
288+
* @param {Address} spender
289+
* @param {Arc} arc
290+
* @param {string} token
291+
* @param {string} tokenSymbol
292+
* @param {function} setIsApproving
293+
* @param {number} amount
294+
*/
295+
export function approveTokens(spender: Address, arc: Arc, token: string, tokenSymbol: string, setIsApproving: any, amount = 100000) {
296+
return async (dispatch: Redux.Dispatch<any, any>, ) => {
297+
const tokenObj = new Token(token, arc);
298+
setIsApproving(true);
299+
const observer = operationNotifierObserver(dispatch, `Approve ${tokenSymbol}`, () => setIsApproving(false), () => setIsApproving(false));
300+
tokenObj.approveForStaking(spender, toWei(amount)).subscribe(...observer);
301+
};
302+
}

src/components/Scheme/CL4R/CL4R.scss

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,15 @@
7676
}
7777
input {
7878
height: 25px;
79-
margin-bottom: 20px;
8079
text-indent: 5px;
8180
}
81+
.lowBalanceLabel {
82+
color: $accent-2;
83+
margin-top: 3px;
84+
}
85+
.releasableLable {
86+
margin-top: 20px;
87+
}
8288
.lockButton {
8389
border: none;
8490
background-color: $accent-1;

src/components/Scheme/CL4R/CL4R.tsx

Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,31 @@ import * as css from "./CL4R.scss";
33
import gql from "graphql-tag";
44
import Loading from "components/Shared/Loading";
55
import withSubscription, { ISubscriptionProps } from "components/Shared/withSubscription";
6-
import { getArcByDAOAddress, standardPolling, getNetworkByDAOAddress, toWei } from "lib/util";
7-
import { Address, CL4RScheme, IDAOState, ISchemeState } from "@daostack/arc.js";
6+
import { getArcByDAOAddress, standardPolling, getNetworkByDAOAddress, toWei, ethErrorHandler, fromWei } from "lib/util";
7+
import { Address, CL4RScheme, IDAOState, ISchemeState, Token } from "@daostack/arc.js";
88
import { RouteComponentProps } from "react-router-dom";
99
import LockRow from "./LockRow";
1010
import PeriodRow from "./PeriodRow";
1111
import * as classNames from "classnames";
1212
import * as moment from "moment";
1313
import Countdown from "components/Shared/Countdown";
1414
import { first } from "rxjs/operators";
15+
import { combineLatest } from "rxjs";
1516
import { enableWalletProvider } from "arc";
16-
import { lock, releaseLocking, extendLocking, redeemLocking } from "@store/arc/arcActions";
17+
import { lock, releaseLocking, extendLocking, redeemLocking, approveTokens } from "@store/arc/arcActions";
1718
import { showNotification } from "@store/notifications/notifications.reducer";
1819
import { connect } from "react-redux";
1920
import Tooltip from "rc-tooltip";
2021
import { getCL4RParams, ICL4RParams } from "./CL4RHelper";
22+
import BN from "bn.js";
2123

2224
interface IDispatchProps {
2325
lock: typeof lock;
2426
releaseLocking: typeof releaseLocking;
2527
extendLocking: typeof extendLocking;
2628
redeemLocking: typeof redeemLocking;
2729
showNotification: typeof showNotification;
30+
approveTokens: typeof approveTokens;
2831
}
2932

3033
const mapDispatchToProps = {
@@ -33,9 +36,10 @@ const mapDispatchToProps = {
3336
extendLocking,
3437
redeemLocking,
3538
showNotification,
39+
approveTokens,
3640
};
3741

38-
type SubscriptionData = Array<any>;
42+
type SubscriptionData = [any, BN, BN];
3943
type IProps = IExternalProps & ISubscriptionProps<SubscriptionData> & IDispatchProps;
4044
type IExternalProps = {
4145
daoState: IDAOState;
@@ -50,10 +54,14 @@ const CL4R = (props: IProps) => {
5054
const [schemeParams, setSchemeParams] = React.useState({} as ICL4RParams);
5155
const [showYourLocks, setShowYourLocks] = React.useState(false);
5256
const [lockDuration, setLockDuration] = React.useState(1);
53-
const [lockAmount, setLockAmount] = React.useState();
57+
const [lockAmount, setLockAmount] = React.useState(0);
5458
const [cl4rScheme, setCL4RScheme] = React.useState<CL4RScheme>();
5559
const [isLocking, setIsLocking] = React.useState(false);
60+
const [isApprovingToken, setIsApprovingToken] = React.useState(false);
5661
const [currentTime, setCurrentTime] = React.useState(moment().unix());
62+
const isAllowance = data[1].gt(new BN(0));
63+
const isEnoughBalance = fromWei(data[2]) >= lockAmount;
64+
const cl4Rlocks = (data as any)[0].data.cl4Rlocks;
5765

5866
const getLockingBatch = React.useCallback((lockingTime: number, startTime: number, batchTime: number): number => {
5967
const timeElapsed = lockingTime - startTime;
@@ -87,6 +95,11 @@ const CL4R = (props: IProps) => {
8795
props.redeemLocking(cl4rScheme, props.currentAccountAddress, lockingId, setIsRedeeming);
8896
}, [cl4rScheme, schemeParams]);
8997

98+
const handleTokenApproving = React.useCallback(async () => {
99+
if (!await enableWalletProvider({ showNotification: props.showNotification }, getNetworkByDAOAddress(daoState.address))) { return; }
100+
props.approveTokens(props.scheme.address, getArcByDAOAddress(daoState.address), schemeParams.token, schemeParams.tokenSymbol, setIsApprovingToken);
101+
}, [schemeParams]);
102+
90103
React.useEffect(() => {
91104
const getSchemeInfo = async () => {
92105
const arc = getArcByDAOAddress(daoState.id);
@@ -121,7 +134,7 @@ const CL4R = (props: IProps) => {
121134
key={period}
122135
period={period}
123136
schemeParams={schemeParams}
124-
lockData={(data as any).data.cl4Rlocks}
137+
lockData={cl4Rlocks}
125138
cl4rScheme={cl4rScheme}
126139
currentLockingBatch={currentLockingBatch}
127140
isLockingEnded={isLockingEnded}
@@ -135,7 +148,7 @@ const CL4R = (props: IProps) => {
135148

136149
periods.reverse();
137150

138-
const lockings = ((data as any).data.cl4Rlocks?.map((lock: any) => {
151+
const lockings = (cl4Rlocks?.map((lock: any) => {
139152
return <LockRow
140153
key={lock.id}
141154
schemeParams={schemeParams}
@@ -170,7 +183,12 @@ const CL4R = (props: IProps) => {
170183

171184
const lockButtonClass = classNames({
172185
[css.lockButton]: true,
173-
[css.disabled]: !lockAmount || !lockDuration || isLocking,
186+
[css.disabled]: !lockAmount || !lockDuration || isLocking || !isEnoughBalance,
187+
});
188+
189+
const approveTokenButtonClass = classNames({
190+
[css.lockButton]: true,
191+
[css.disabled]: isApprovingToken,
174192
});
175193

176194
return (
@@ -183,7 +201,7 @@ const CL4R = (props: IProps) => {
183201
<div className={locksClass} onClick={() => setShowYourLocks(true)}>Your Locks</div>
184202
</div>
185203
{
186-
showYourLocks ? (data as any).data.cl4Rlocks.length > 0 ?
204+
showYourLocks ? cl4Rlocks.length > 0 ?
187205
<table>
188206
<thead>
189207
<tr>
@@ -218,16 +236,20 @@ const CL4R = (props: IProps) => {
218236
{!isLockingEnded && isLockingStarted && <div className={css.lockWrapper}>
219237
<div className={css.lockTitle}>New Lock</div>
220238
<div className={css.lockDurationLabel}>
221-
<span style={{marginRight: "5px"}}>Lock Duration</span>
239+
<span style={{ marginRight: "5px" }}>Lock Duration</span>
222240
<Tooltip trigger={["hover"]} overlay={`Period: ${schemeParams.batchTime} seconds`}><img width="15px" src="/assets/images/Icon/question-help.svg" /></Tooltip>
223241
</div>
224-
<select onChange={(e: any) => setLockDuration(e.target.value)}>
242+
<select onChange={(e: any) => setLockDuration(e.target.value)} disabled={!isAllowance}>
225243
{durations}
226244
</select>
227245
<span style={{ marginBottom: "5px" }}>Lock Amount ({schemeParams.tokenSymbol})</span>
228-
<input type="number" onChange={(e: any) => setLockAmount(e.target.value)} />
229-
{<span>Releasable: {moment().add(lockDuration * Number(schemeParams.batchTime), "seconds").format("DD.MM.YYYY HH:mm")}</span>}
230-
<button onClick={handleLock} className={lockButtonClass} disabled={!lockAmount || !lockDuration}>Lock</button>
246+
<input type="number" onChange={(e: any) => setLockAmount(e.target.value)} disabled={!isAllowance} />
247+
{!isEnoughBalance && <span className={css.lowBalanceLabel}>{`Not enough ${schemeParams.tokenSymbol}!`}</span>}
248+
{<span className={css.releasableLable}>Releasable: {moment().add(lockDuration * Number(schemeParams.batchTime), "seconds").format("DD.MM.YYYY HH:mm")}</span>}
249+
{isAllowance && <button onClick={handleLock} className={lockButtonClass} disabled={!lockAmount || !lockDuration}>Lock</button>}
250+
{!isAllowance && <Tooltip trigger={["hover"]} overlay={`Upon activation, the smart contract will be authorized to receive up to 100,000 ${schemeParams.tokenSymbol}`}>
251+
<button onClick={handleTokenApproving} className={approveTokenButtonClass} disabled={isApprovingToken}>Enable Locking</button>
252+
</Tooltip>}
231253
</div>}
232254
</div> : <Loading />
233255
);
@@ -241,6 +263,18 @@ const SubscribedCL4R = withSubscription({
241263
createObservable: async (props: IProps) => {
242264
if (props.currentAccountAddress) {
243265
const arc = getArcByDAOAddress(props.daoState.id);
266+
const schemeToken = gql`
267+
query SchemeInfo {
268+
controllerSchemes(where: {id: "${props.scheme.id.toLowerCase()}"}) {
269+
continuousLocking4ReputationParams {
270+
id
271+
token
272+
}
273+
}
274+
}
275+
`;
276+
const schemeTokenData = await arc.sendQuery(schemeToken);
277+
const tokenString = schemeTokenData.data.controllerSchemes[0].continuousLocking4ReputationParams.token;
244278
const locksQuery = gql`
245279
query Locks {
246280
cl4Rlocks(where: {scheme: "${props.scheme.id.toLowerCase()}", locker: "${props.currentAccountAddress}"}) {
@@ -250,15 +284,26 @@ const SubscribedCL4R = withSubscription({
250284
amount
251285
lockingTime
252286
period
253-
redeemed
254-
redeemedAt
255287
released
256288
releasedAt
257-
batchIndexRedeemed
289+
redeemed {
290+
id
291+
lock
292+
amount
293+
redeemedAt
294+
batchIndex
295+
}
258296
}
259297
}
260298
`;
261-
return arc.getObservable(locksQuery, standardPolling());
299+
const token = new Token(tokenString, arc);
300+
const allowance = token.allowance(props.currentAccountAddress, props.scheme.address);
301+
const balanceOf = token.balanceOf(props.currentAccountAddress);
302+
return combineLatest(
303+
arc.getObservable(locksQuery, standardPolling()),
304+
allowance.pipe(ethErrorHandler()),
305+
balanceOf.pipe(ethErrorHandler())
306+
);
262307
}
263308
},
264309
});

src/components/Scheme/CL4R/CL4RHelper.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,17 @@ export interface ICL4RLock {
2626
amount: string;
2727
lockingTime: string;
2828
period: string;
29-
redeemed: boolean;
30-
redeemedAt: string;
29+
redeemed: Array<ICL4RRedeem>;
3130
released: boolean;
3231
releasedAt: string;
33-
batchIndexRedeemed: string;
32+
}
33+
34+
export interface ICL4RRedeem {
35+
id: Address;
36+
lock: ICL4RLock;
37+
amount: string;
38+
redeemedAt: string;
39+
batchIndex: string;
3440
}
3541

3642
export const getCL4RParams = async (daoAddress: string, schemeId: string) => {

src/components/Scheme/CL4R/LockRow.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,16 @@ interface IProps {
1818
}
1919

2020
const LockRow = (props: IProps) => {
21-
const { lockData, schemeParams, handleRelease, handleExtend, getLockingBatch, endTime, currentLockingBatch, isLockingEnded } = props;
21+
const { lockData, schemeParams, handleRelease, handleExtend, getLockingBatch, currentLockingBatch, isLockingEnded } = props;
2222
const [isReleasing, setIsReleasing] = React.useState(false);
2323
const [isExtending, setIsExtending] = React.useState(false);
2424
const [lockDuration, setLockDuration] = React.useState(1);
25+
const lockingBatch = getLockingBatch(Number(lockData.lockingTime), Number(schemeParams.startTime), Number(schemeParams.batchTime));
2526

2627
// This is to avoid an option to extend more then the max locking batch.
2728
const extendDurations = [] as any;
2829
for (let duration = 1; duration <= Number(schemeParams.maxLockingBatches); duration++) {
29-
if ((moment().unix() + (duration * Number(schemeParams.batchTime)) <= endTime) && (duration + Number(lockData.period) <= Number(schemeParams.maxLockingBatches))) {
30+
if ((duration + Number(lockData.period) <= Number(schemeParams.maxLockingBatches)) && (lockingBatch + Number(lockData.period) + duration < schemeParams.batchesIndexCap)) {
3031
extendDurations.push(<option key={duration} value={duration} selected={duration === 1}>{duration}</option>);
3132
}
3233
}
@@ -39,8 +40,6 @@ const LockRow = (props: IProps) => {
3940
return moment().isSameOrAfter(releasable);
4041
}, [lockData]);
4142

42-
const lockingBatch = getLockingBatch(Number(lockData.lockingTime), Number(schemeParams.startTime), Number(schemeParams.batchTime));
43-
4443
const actionButtonClass = classNames({
4544
[css.actionButton]: true,
4645
[css.disabled]: isReleasing || isExtending,

src/components/Scheme/CL4R/PeriodRow.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,9 @@
2222
color: $gray-1;
2323
}
2424
}
25+
.redeemedLabel {
26+
display: flex;
27+
flex-direction: column;
28+
color: $gray-label;
29+
}
2530
}

src/components/Scheme/CL4R/PeriodRow.tsx

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from "react";
22
import * as css from "./PeriodRow.scss";
3-
import { ICL4RLock, ICL4RParams } from "./CL4RHelper";
3+
import { ICL4RLock, ICL4RParams, ICL4RRedeem } from "./CL4RHelper";
44
import { formatTokens, numberWithCommas, WEI } from "lib/util";
55
import { CL4RScheme } from "@daostack/arc.js";
66
import Decimal from "decimal.js";
@@ -26,6 +26,7 @@ const PeriodRow = (props: IProps) => {
2626
const [isRedeeming, setIsRedeeming] = React.useState(false);
2727
const lockingIds: Array<number> = [];
2828
const lockingIdsForRedeem: Array<number> = [];
29+
const redeemData: Array<ICL4RRedeem> = [];
2930

3031
let youLocked = new BN(0);
3132
for (const lock of lockData) {
@@ -39,7 +40,22 @@ const PeriodRow = (props: IProps) => {
3940
lockingIdsForRedeem.push(Number(lock.lockingId));
4041
}
4142
}
43+
redeemData.push(...lock.redeemed);
4244
}
45+
46+
const isPeriodRedeemed = youLocked.gt(new BN(0)) && Number(repuationRewardForLockings) === 0;
47+
let redeemedAt;
48+
let amountRedeemed = new BN(0);
49+
if (isPeriodRedeemed) {
50+
// Traverse the redeemed data to extract the redeem time and amount.
51+
for (const redeem of redeemData) {
52+
if (redeem.redeemedAt && period === Number(redeem.batchIndex)) {
53+
redeemedAt = moment.unix(Number(redeem.redeemedAt)).format("DD.MM.YYYY HH:mm");
54+
amountRedeemed = amountRedeemed.add(new BN(redeem.amount));
55+
}
56+
}
57+
}
58+
4359
React.useEffect(() => {
4460
const getRepuationData = async () => {
4561
const repuationRewardForBatch = (await props.cl4rScheme.getRepuationRewardForBatch(schemeParams.repRewardConstA, schemeParams.repRewardConstB, period));
@@ -52,7 +68,12 @@ const PeriodRow = (props: IProps) => {
5268

5369
const inProgress = currentLockingBatch === period && !isLockingEnded;
5470
const redeemable = moment().isSameOrAfter(moment.unix(Number(schemeParams.redeemEnableTime)));
55-
const reputationToReceive = youLocked.gt(new BN(0)) && Number(repuationRewardForLockings) === 0 ? <span className={css.inProgressLabel}>Redeemed</span> : `${numberWithCommas(repuationRewardForLockings)} REP`;
71+
const reputationToReceive = isPeriodRedeemed ?
72+
<div className={css.redeemedLabel}>
73+
<span className={css.inProgressLabel}>{`${numberWithCommas(formatTokens(amountRedeemed))} REP Redeemed`}</span>
74+
<span>{redeemedAt}</span>
75+
</div> :
76+
`${numberWithCommas(repuationRewardForLockings)} REP`;
5677

5778
const actionButtonClass = classNames({
5879
[css.actionButton]: true,
@@ -62,7 +83,7 @@ const PeriodRow = (props: IProps) => {
6283
return (
6384
<tr className={css.row}>
6485
<td>{period + 1}</td>
65-
<td>{`${numberWithCommas(formatTokens(new BN(youLocked)))} ${schemeParams.tokenSymbol}`}</td>
86+
<td>{`${numberWithCommas(formatTokens(youLocked))} ${schemeParams.tokenSymbol}`}</td>
6687
<td>{`${numberWithCommas(repuationRewardForBatch)} REP`}</td>
6788
<td>{inProgress ? <span className={css.inProgressLabel}>In Progress</span> : reputationToReceive}</td>
6889
<td>{!inProgress && redeemable && Number(repuationRewardForLockings) > 0 && <button className={actionButtonClass} onClick={() => handleRedeem(lockingIdsForRedeem, setIsRedeeming)} disabled={isRedeeming}>Redeem</button>}</td>

0 commit comments

Comments
 (0)