Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ NEXT_PUBLIC_PROPOSAL_REJECTED=
# Google Analytics Measurement ID
NEXT_PUBLIC_GA_MEASUREMENT_ID=

# Enable/Disable Elastic logs: true | false
NEXT_LOG_TO_ELASTIC=false

# API log level: 0 none, 1 error, 2 warn, 3 info, 4 trace, 5 log, 6 debug
LOG_LEVEL=6

Expand Down Expand Up @@ -149,4 +152,7 @@ NEXT_PUBLIC_MODAL_FEATURE_LINK=

# ImgProxy
IMGPROXY_KEY=
IMGPROXY_SALT=
IMGPROXY_SALT=

# Enable/Disable access logs: true | false
ACCESS_LOGS_ENABLED=false
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ yarn-debug.log*
yarn-error.log*
.vscode/
.editorconfig
logs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { NumberFormatValues } from "react-number-format";

import { toSmartContractDecimals } from "@taikai/dappkit/dist/src/utils/numbers";
import { fireEvent } from "@testing-library/dom";
import BigNumber from "bignumber.js";

import CreateBountyTokenAmount, {
ZeroNumberFormatValues,
} from "components/bounty/create-bounty/token-amount/controller";

import { Network } from "interfaces/network";
import { DistributionsProps } from "interfaces/proposal";
import { Token } from "interfaces/token";

import { render } from "__tests__/utils/custom-render";

jest.mock("x-hooks/use-bepro", () => () => ({}));

const mockCurrentToken: Token = {
address: "0x1234567890123456789012345678901234567890",
name: "Mock Token",
symbol: "MOCK",
};

const mockCurrentNetwork: Network = {
id: 1,
name: "Mock Network",
updatedAt: new Date(),
createdAt: new Date(),
description: "Mock Description",
networkAddress: "0x1234567890123456789012345678901234567890",
creatorAddress: "0x1234567890123456789012345678901234567890",
openBounties: 0,
totalBounties: 0,
allowCustomTokens: false,
councilMembers: [],
banned_domains: [],
closeTaskAllowList: [],
allow_list: [],
mergeCreatorFeeShare: 0.05,
proposerFeeShare: 0.5,
chain: {
chainId: 1,
chainRpc: "https://mock-rpc.com",
name: "Mock Chain",
chainName: "Mock Chain",
chainShortName: "MOCK",
chainCurrencySymbol: "MOCK",
chainCurrencyDecimals: "18",
chainCurrencyName: "Mock Token",
blockScanner: "https://mock-scanner.com",
registryAddress: "0x1234567890123456789012345678901234567890",
eventsApi: "https://mock-events.com",
isDefault: true,
closeFeePercentage: 10,
cancelFeePercentage: 1.0,
networkCreationFeePercentage: 0.5,
},
};

describe("TokenAmountController", () => {
beforeEach(() => {
jest.clearAllMocks();
});

it("fuzzes total input and ensures internal adjusted total is divisible by 100 in contract units", () => {
let executions = 0;

let issueAmount = ZeroNumberFormatValues;
let previewAmount = ZeroNumberFormatValues;

const setPreviewAmount = jest.fn((value: NumberFormatValues) => {
previewAmount = value;
});
const updateIssueAmount = jest.fn((value: NumberFormatValues) => {
issueAmount = value;
});

while (executions < 100) {
const decimals = Math.floor(Math.random() * 13) + 6;
const randomValue = parseFloat((Math.random() * 499999 + 1).toFixed(decimals));

const result = render(<CreateBountyTokenAmount
currentToken={mockCurrentToken}
updateCurrentToken={jest.fn()}
addToken={jest.fn()}
canAddCustomToken={true}
defaultToken={mockCurrentToken}
userAddress="0x1234567890123456789012345678901234567890"
customTokens={[]}
tokenBalance={new BigNumber(1)}
issueAmount={issueAmount}
updateIssueAmount={updateIssueAmount}
isFunders={true}
decimals={decimals}
isFunding={false}
needValueValidation={false}
previewAmount={previewAmount}
distributions={{} as DistributionsProps}
currentNetwork={mockCurrentNetwork}
setPreviewAmount={setPreviewAmount}
setDistributions={jest.fn()}
sethasAmountError={jest.fn()}
/>);

const totalAmountInput = result.getAllByTestId("total-amount-input")[0];

const valueString = randomValue.toString();

fireEvent.change(totalAmountInput, { target: { value: valueString } });

result.rerender(<CreateBountyTokenAmount
currentToken={mockCurrentToken}
updateCurrentToken={jest.fn()}
addToken={jest.fn()}
canAddCustomToken={true}
defaultToken={mockCurrentToken}
userAddress="0x1234567890123456789012345678901234567890"
customTokens={[]}
tokenBalance={new BigNumber(1)}
issueAmount={issueAmount}
updateIssueAmount={updateIssueAmount}
isFunders={true}
decimals={decimals}
isFunding={false}
needValueValidation={false}
previewAmount={previewAmount}
distributions={{} as DistributionsProps}
currentNetwork={mockCurrentNetwork}
setPreviewAmount={setPreviewAmount}
setDistributions={jest.fn()}
sethasAmountError={jest.fn()}
/>);

const newValueContract = toSmartContractDecimals(issueAmount.value, decimals);

expect(Number(BigInt(newValueContract) % BigInt(100))).toBe(0);

jest.clearAllMocks();
result.unmount();

executions += 1;
}
});
});
18 changes: 15 additions & 3 deletions components/bounty/comments/input-comment/controller.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import {ChangeEvent, useState} from "react";

import { AxiosError } from "axios";
import {useTranslation} from "next-i18next";

import InputCommentView from "components/bounty/comments/input-comment/view";

import { COMMENT_MAX_LENGTH } from "helpers/constants";
import {QueryKeys} from "helpers/query-keys";

import {IdsComment, TypeComment} from "interfaces/comments";

import {CreateComment} from "x-hooks/api/comments";
import { useToastStore } from "x-hooks/stores/toasts/toasts.store";
import useReactQueryMutation from "x-hooks/use-react-query-mutation";
import { useTaskSubscription } from "x-hooks/use-task-subscription";

Expand All @@ -34,7 +37,10 @@ export default function InputComment({
deliverable: QueryKeys.deliverable(ids?.deliverableId?.toString()),
proposal: QueryKeys.proposalComments(ids?.proposalId?.toString())
}[type];
const commentLength = comment?.length || 0;
const error = commentLength > COMMENT_MAX_LENGTH ? "max-length" : null;

const { addError, addSuccess } = useToastStore();
const { refresh: refreshSubscriptions } = useTaskSubscription();
const { mutate: addComment } = useReactQueryMutation({
queryKey: queryKey,
Expand All @@ -43,9 +49,12 @@ export default function InputComment({
...ids,
type
}),
toastSuccess: t("bounty:actions.comment.success"),
toastError: t("bounty:actions.comment.error"),
onSuccess: () => {
onSettled: (data, error: AxiosError<{ message: string }>) => {
if (error) {
addError(t("actions.failed"), `${error?.response?.data?.message}`);
return;
}
addSuccess(t("actions.success"), t("bounty:actions.comment.success"));
setComment("");
refreshSubscriptions();
}
Expand All @@ -61,6 +70,9 @@ export default function InputComment({
userAddress={userAddress}
avatarHash={avatar}
comment={comment}
commentLength={commentLength}
maxLength={COMMENT_MAX_LENGTH}
error={error}
onCommentChange={onCommentChange}
onCommentSubmit={addComment}
/>
Expand Down
90 changes: 56 additions & 34 deletions components/bounty/comments/input-comment/view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {useTranslation} from "next-i18next";

import AvatarOrIdenticon from "components/avatar-or-identicon";
import Button from "components/button";
import If from "components/If";

import {truncateAddress} from "helpers/truncate-address";

Expand All @@ -12,55 +13,76 @@ export default function InputCommentView({
userAddress,
avatarHash,
comment,
commentLength,
maxLength,
error = null,
onCommentSubmit,
onCommentChange ,
}: {
handle?: string;
userAddress: string;
avatarHash: string;
comment: string;
commentLength: number;
maxLength: number;
error: "max-length" | null;
onCommentSubmit: (...props) => void;
onCommentChange : (e: ChangeEvent<HTMLTextAreaElement>) => void
}) {
const { t } = useTranslation("common");

const borderColor = error ? "danger" : "gray-700";
const errorMessage = {
"max-length": t("errors.comment.max-length", { max: maxLength }),
}[error];

return (
<div className="border-radius-8 p-3 bg-gray-850 mb-3 border-gray-700 border">
<div className="d-flex align-items-center mb-2">
<div className="d-flex align-items-center text-truncate">
<AvatarOrIdenticon
user={{
avatar: avatarHash,
handle,
address: userAddress,
}}
size="xsm"
/>
<span className="xs-medium ms-2 text-truncate">
{handle ? `@${handle}` : truncateAddress(userAddress)}{" "}
</span>
<>
<div className={`border-radius-8 p-3 bg-gray-850 border border-${borderColor}`}>
<div className="d-flex align-items-center justify-content-between mb-2">
<div className="d-flex align-items-center text-truncate">
<AvatarOrIdenticon
user={{
avatar: avatarHash,
handle,
address: userAddress,
}}
size="xsm"
/>
<span className="xs-medium ms-2 text-truncate">
{handle ? `@${handle}` : truncateAddress(userAddress)}{" "}
</span>
</div>

<div className="xs-medium text-gray-300">
{commentLength}/{maxLength}
</div>
</div>
</div>
<textarea
tabIndex={0}
className="ps-0 form-control input-comment"
rows={2}
data-testid="comments-textarea"
placeholder={t("comments.input.placeholder")}
value={comment}
onChange={onCommentChange}
/>
<textarea
tabIndex={0}
className="ps-0 form-control input-comment"
rows={2}
data-testid="comments-textarea"
placeholder={t("comments.input.placeholder")}
value={comment}
onChange={onCommentChange}
/>

<div className="d-flex justify-content-end mt-2">
<Button
className="btn-comment"
data-testid="comments-btn"
onClick={onCommentSubmit}
disabled={!comment?.length}
>
{t("comments.button")}
</Button>
<div className="d-flex justify-content-end mt-2">
<Button
className="btn-comment"
data-testid="comments-btn"
onClick={onCommentSubmit}
disabled={!comment?.length || !!error}
>
{t("comments.button")}
</Button>
</div>
</div>
</div>

<If condition={!!error}>
<small className="xs-small text-danger">{errorMessage}</small>
</If>
</>
);
}
Loading