Skip to content

Commit 50d5de1

Browse files
Withdraw Options (#597)
* Enhanced withdraw and transfer functionality * Componentize and organize. * Fetch distributions from subchain. * Factor out Asset and Account form input components.
1 parent 397b4a0 commit 50d5de1

27 files changed

+916
-303
lines changed

packages/webapp/.env

+3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ NEXT_PUBLIC_TABLE_ROWS_MAX_FETCH_PER_SEC = "10"
1515
NEXT_PUBLIC_EDEN_CONTRACT_ACCOUNT = "test.edev"
1616
NEXT_PUBLIC_AA_FETCH_AFTER="1633883520000"
1717
NEXT_PUBLIC_TOKEN_CONTRACT = "eosio.token"
18+
NEXT_PUBLIC_TOKEN_SYMBOL = "WAX"
19+
NEXT_PUBLIC_TOKEN_PRECISION = "8"
1820

1921
# ATOMICHUB
2022
NEXT_PUBLIC_AA_BASE_URL = "https://test.wax.api.atomicassets.io/atomicassets/v1"
@@ -27,6 +29,7 @@ NEXT_PUBLIC_AA_SCHEMA_NAME = "members"
2729

2830
# OTHER
2931
NEXT_PUBLIC_BLOCKEXPLORER_ACCOUNT_BASE_URL = "https://wax-test.bloks.io/account"
32+
NEXT_PUBLIC_BLOCKEXPLORER_TRANSACTION_BASE_URL = "https://wax-test.bloks.io/transaction"
3033
NEXT_PUBLIC_APP_MINIMUM_DONATION_AMOUNT = "10.00000000 WAX"
3134
NEXT_PUBLIC_ENABLED_WALLETS = "ANCHOR,LEDGER,SOFTKEY"
3235
NEXT_PUBLIC_IPFS_BASE_URL = "https://infura-ipfs.io/ipfs"

packages/webapp/src/_app/hooks/queries.ts

-12
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import {
1919
} from "inductions/api";
2020
import {
2121
getChiefDelegates,
22-
getDistributionsForAccount,
2322
getDistributionState,
2423
getMasterPool,
2524
getHeadDelegate,
@@ -200,11 +199,6 @@ export const queryMasterPool = () => ({
200199
queryFn: getMasterPool,
201200
});
202201

203-
export const queryDistributionsForAccount = (account: string) => ({
204-
queryKey: ["query_distributions_for_account", account],
205-
queryFn: () => getDistributionsForAccount(account),
206-
});
207-
208202
export const queryTokenBalanceForAccount = (account: string) => ({
209203
queryKey: ["query_token_balance_for_account", account],
210204
queryFn: () => getTokenBalanceForAccount(account),
@@ -248,12 +242,6 @@ export const useMemberByAccountName = (accountName?: string) =>
248242
enabled: Boolean(accountName),
249243
});
250244

251-
export const useDistributionsForAccount = (account: string) =>
252-
useQuery({
253-
...queryDistributionsForAccount(account),
254-
enabled: Boolean(account),
255-
});
256-
257245
export const useDistributionState = () =>
258246
useQuery({
259247
...queryDistributionState(),
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/* Chrome, Safari, Edge, Opera */
2+
input::-webkit-outer-spin-button,
3+
input::-webkit-inner-spin-button {
4+
-webkit-appearance: none;
5+
margin: 0;
6+
}
7+
8+
/* Firefox */
9+
input[type="number"] {
10+
-moz-appearance: textfield;
11+
}

packages/webapp/src/_app/ui/form.tsx

+130-4
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,27 @@
11
import React, { HTMLProps } from "react";
22

3+
import { tokenConfig } from "config";
4+
import { Button } from "_app";
5+
36
export const Label: React.FC<{
47
htmlFor: string;
58
}> = (props) => (
6-
<label className="block text-sm font-medium text-gray-700" {...props}>
9+
<label className="block text-sm font-normal text-gray-600" {...props}>
710
{props.children}
811
</label>
912
);
1013

11-
export const Input: React.FC<HTMLProps<HTMLInputElement>> = (props) => (
14+
export const Input: React.FC<
15+
HTMLProps<HTMLInputElement> & {
16+
inputRef?: React.Ref<HTMLInputElement> | null;
17+
}
18+
> = ({ inputRef, ...props }) => (
1219
<input
1320
name={props.id}
14-
className={`w-full bg-white rounded border border-gray-300 focus:border-yellow-500 focus:ring-2 focus:ring-yellow-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out ${
21+
className={`w-full bg-white border border-gray-300 focus:border-yellow-500 focus:ring-2 focus:ring-yellow-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out ${
1522
props.disabled ? "bg-gray-50" : ""
1623
}`}
24+
ref={inputRef}
1725
{...props}
1826
/>
1927
);
@@ -47,7 +55,7 @@ export const TextArea: React.FC<HTMLProps<HTMLTextAreaElement>> = (props) => (
4755
<textarea
4856
rows={3}
4957
name={props.id}
50-
className={`w-full bg-white rounded border border-gray-300 focus:border-yellow-500 focus:ring-2 focus:ring-yellow-200 h-32 text-base outline-none text-gray-700 py-1 px-3 resize-none leading-6 transition-colors duration-200 ease-in-out ${
58+
className={`w-full bg-white border border-gray-300 focus:border-yellow-500 focus:ring-2 focus:ring-yellow-200 h-32 text-base outline-none text-gray-700 py-1 px-3 resize-none leading-6 transition-colors duration-200 ease-in-out ${
5159
props.disabled ? "bg-gray-50" : ""
5260
}`}
5361
{...props}
@@ -99,6 +107,122 @@ export const LabeledSet: React.FC<{
99107
);
100108
};
101109

110+
export const ChainAccountInput = (
111+
props: HTMLProps<HTMLInputElement> & { id: string }
112+
) => {
113+
const validateAccountField = (e: React.FormEvent<HTMLInputElement>) => {
114+
const target = e.target as HTMLInputElement;
115+
if (target.validity.valueMissing) {
116+
target.setCustomValidity("Enter an account name");
117+
} else {
118+
target.setCustomValidity("Invalid account name");
119+
}
120+
};
121+
122+
const clearErrorMessages = (e: React.FormEvent<HTMLInputElement>) => {
123+
(e.target as HTMLInputElement).setCustomValidity("");
124+
};
125+
126+
const onInput = (e: React.FormEvent<HTMLInputElement>) => {
127+
clearErrorMessages(e);
128+
props.onInput?.(e);
129+
};
130+
131+
const onInvalid = (e: React.FormEvent<HTMLInputElement>) => {
132+
validateAccountField(e);
133+
props.onInvalid?.(e);
134+
};
135+
136+
return (
137+
<Form.LabeledSet
138+
label={`${tokenConfig.symbol} account name (12 characters)`}
139+
htmlFor={props.id}
140+
>
141+
<Form.Input
142+
type="text"
143+
maxLength={12}
144+
pattern="^[a-z,1-5.]{1,12}$"
145+
{...props}
146+
onInvalid={onInvalid}
147+
onInput={onInput}
148+
/>
149+
</Form.LabeledSet>
150+
);
151+
};
152+
153+
export const AssetInput = (
154+
props: HTMLProps<HTMLInputElement> & {
155+
label: string; // required
156+
id: string; // required
157+
inputRef?: React.RefObject<HTMLInputElement>;
158+
}
159+
) => {
160+
const { label, inputRef, ...inputProps } = props;
161+
162+
const amountRef = inputRef ?? React.useRef<HTMLInputElement>(null);
163+
164+
const amountInputPreventChangeOnScroll = (
165+
e: React.WheelEvent<HTMLInputElement>
166+
) => (e.target as HTMLInputElement).blur();
167+
168+
const validateAmountField = (e: React.FormEvent<HTMLInputElement>) => {
169+
const target = e.target as HTMLInputElement;
170+
if (target.validity.rangeOverflow) {
171+
target.setCustomValidity("Insufficient funds available");
172+
} else {
173+
target.setCustomValidity("Enter a valid amount");
174+
}
175+
};
176+
177+
const clearErrorMessages = (e: React.FormEvent<HTMLInputElement>) => {
178+
(e.target as HTMLInputElement).setCustomValidity("");
179+
};
180+
181+
const setMaxAmount = () => {
182+
amountRef.current?.setCustomValidity("");
183+
184+
// ensures this works with uncontrolled instances of this input too
185+
Object.getOwnPropertyDescriptor(
186+
window.HTMLInputElement.prototype,
187+
"value"
188+
)?.set?.call?.(amountRef.current, inputProps.max);
189+
amountRef.current?.dispatchEvent(
190+
new Event("change", { bubbles: true })
191+
);
192+
// (adapted from: https://coryrylan.com/blog/trigger-input-updates-with-react-controlled-inputs)
193+
};
194+
195+
return (
196+
<Form.LabeledSet label={label} htmlFor={inputProps.id}>
197+
<div className="flex space-x-2">
198+
<div className="relative flex-1">
199+
<Form.Input
200+
type="number"
201+
inputMode="decimal"
202+
min={1 / Math.pow(10, tokenConfig.precision)}
203+
step="any"
204+
onWheel={amountInputPreventChangeOnScroll}
205+
onInvalid={validateAmountField}
206+
onInput={clearErrorMessages}
207+
inputRef={amountRef}
208+
{...inputProps}
209+
/>
210+
<div className="absolute top-3 right-2">
211+
<p className="text-sm text-gray-400">
212+
{tokenConfig.symbol}
213+
</p>
214+
</div>
215+
</div>
216+
{inputProps.max ? (
217+
<Button type="neutral" onClick={setMaxAmount}>
218+
Max
219+
</Button>
220+
) : null}
221+
</div>
222+
</Form.LabeledSet>
223+
);
224+
};
225+
102226
export const Form = {
103227
Label,
104228
Input,
@@ -107,6 +231,8 @@ export const Form = {
107231
LabeledSet,
108232
FileInput,
109233
Checkbox,
234+
ChainAccountInput,
235+
AssetInput,
110236
};
111237

112238
export default Form;

packages/webapp/src/_app/utils/asset.ts

+19
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
1+
import { tokenConfig } from "config";
2+
13
export interface Asset {
24
quantity: number; // integer without decimals
35
symbol: string;
46
precision: number;
57
}
68

9+
export const getDefaultTokenAsset = () => ({
10+
quantity: 0,
11+
symbol: tokenConfig.symbol,
12+
precision: tokenConfig.precision,
13+
});
14+
715
export const assetFromString = (asset: string): Asset => {
816
const re = /^[0-9]+\.[0-9]+ [A-Z,a-z]{1,5}$/;
917
if (asset.match(re) === null) {
@@ -20,6 +28,17 @@ export const assetFromString = (asset: string): Asset => {
2028
};
2129
};
2230

31+
/**
32+
* Convert a number to an Asset with the community-configured token symbol and precision
33+
* @param {number} value - a number that can contain a decimal
34+
* @returns {Asset} the Asset with the community's default token symbol and precision
35+
*/
36+
export const assetFromNumber = (value: number): Asset => ({
37+
symbol: tokenConfig.symbol,
38+
precision: tokenConfig.precision,
39+
quantity: value * Math.pow(10, tokenConfig.precision),
40+
});
41+
2342
export const assetToString = (price: Asset, decimals = 2) =>
2443
`${(price.quantity / Math.pow(10, price.precision)).toFixed(decimals)} ${
2544
price.symbol

packages/webapp/src/config.ts

+16
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ if (
99
!process.env.NEXT_PUBLIC_EOS_READ_RPC_URLS ||
1010
!process.env.NEXT_PUBLIC_EOS_CHAIN_ID ||
1111
!process.env.NEXT_PUBLIC_BLOCKEXPLORER_ACCOUNT_BASE_URL ||
12+
!process.env.NEXT_PUBLIC_BLOCKEXPLORER_TRANSACTION_BASE_URL ||
1213
!process.env.NEXT_PUBLIC_AA_BASE_URL ||
1314
!process.env.NEXT_PUBLIC_AA_MARKET_URL ||
1415
!process.env.NEXT_PUBLIC_AA_HUB_URL ||
@@ -28,6 +29,8 @@ if (
2829
!process.env.NEXT_PUBLIC_ELECTION_COMMUNITY_ROOM_URL ||
2930
!process.env.NEXT_PUBLIC_ELECTION_MEETING_DURATION_MS ||
3031
!process.env.NEXT_PUBLIC_TOKEN_CONTRACT ||
32+
!process.env.NEXT_PUBLIC_TOKEN_SYMBOL ||
33+
!process.env.NEXT_PUBLIC_TOKEN_PRECISION ||
3134
!process.env.NEXT_PUBLIC_SUBCHAIN_WASM_URL ||
3235
!process.env.NEXT_PUBLIC_SUBCHAIN_STATE_URL ||
3336
!process.env.NEXT_PUBLIC_SUBCHAIN_WS_URL ||
@@ -48,6 +51,9 @@ EOS_CHAIN_ID="${process.env.NEXT_PUBLIC_EOS_CHAIN_ID}"
4851
BLOCKEXPLORER_ACCOUNT_BASE_URL="${
4952
process.env.NEXT_PUBLIC_BLOCKEXPLORER_ACCOUNT_BASE_URL
5053
}"
54+
BLOCKEXPLORER_TRANSACTION_BASE_URL="${
55+
process.env.NEXT_PUBLIC_BLOCKEXPLORER_TRANSACTION_BASE_URL
56+
}"
5157
AA_BASE_URL="${process.env.NEXT_PUBLIC_AA_BASE_URL}"
5258
AA_MARKET_URL="${process.env.NEXT_PUBLIC_AA_MARKET_URL}"
5359
AA_HUB_URL="${process.env.NEXT_PUBLIC_AA_HUB_URL}"
@@ -74,6 +80,8 @@ ELECTION_COMMUNITY_ROOM_URL="${
7480
process.env.NEXT_PUBLIC_ELECTION_COMMUNITY_ROOM_URL
7581
}"
7682
TOKEN_CONTRACT="${process.env.NEXT_PUBLIC_TOKEN_CONTRACT}"
83+
TOKEN_SYMBOL="${process.env.NEXT_PUBLIC_TOKEN_SYMBOL}"
84+
TOKEN_PRECISION="${process.env.NEXT_PUBLIC_TOKEN_PRECISION}"
7785
SUBCHAIN_WASM_URL="${process.env.NEXT_PUBLIC_SUBCHAIN_WASM_URL}"
7886
SUBCHAIN_STATE_URL="${process.env.NEXT_PUBLIC_SUBCHAIN_STATE_URL}"
7987
SUBCHAIN_WS_URL="${process.env.NEXT_PUBLIC_SUBCHAIN_WS_URL}"
@@ -102,6 +110,8 @@ export const box = {
102110

103111
export const blockExplorerAccountBaseUrl =
104112
process.env.NEXT_PUBLIC_BLOCKEXPLORER_ACCOUNT_BASE_URL;
113+
export const blockExplorerTransactionBaseUrl =
114+
process.env.NEXT_PUBLIC_BLOCKEXPLORER_TRANSACTION_BASE_URL;
105115

106116
export const shortAppName = process.env.NEXT_PUBLIC_APP_SHORT_NAME;
107117
export const appName = process.env.NEXT_PUBLIC_APP_NAME;
@@ -137,6 +147,12 @@ export const chainConfig = {
137147
rpcEndpoints: [rpcEndpoint],
138148
};
139149

150+
export const tokenConfig = {
151+
contract: process.env.NEXT_PUBLIC_TOKEN_CONTRACT,
152+
symbol: process.env.NEXT_PUBLIC_TOKEN_SYMBOL,
153+
precision: Number(process.env.NEXT_PUBLIC_TOKEN_PRECISION),
154+
};
155+
140156
export const availableWallets = (
141157
process.env.NEXT_PUBLIC_ENABLED_WALLETS || ""
142158
).split(",");

0 commit comments

Comments
 (0)