diff --git a/.env.example b/.env.example index 91aae0c01f..7a70cb1800 100644 --- a/.env.example +++ b/.env.example @@ -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 @@ -149,4 +152,7 @@ NEXT_PUBLIC_MODAL_FEATURE_LINK= # ImgProxy IMGPROXY_KEY= -IMGPROXY_SALT= \ No newline at end of file +IMGPROXY_SALT= + +# Enable/Disable access logs: true | false +ACCESS_LOGS_ENABLED=false \ No newline at end of file diff --git a/.gitignore b/.gitignore index e866c60bc9..584c479389 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ yarn-debug.log* yarn-error.log* .vscode/ .editorconfig +logs diff --git a/__tests__/components/bounty/create-bounty/token-amount/token-amount.controller.test.tsx b/__tests__/components/bounty/create-bounty/token-amount/token-amount.controller.test.tsx new file mode 100644 index 0000000000..0befff60e7 --- /dev/null +++ b/__tests__/components/bounty/create-bounty/token-amount/token-amount.controller.test.tsx @@ -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(); + + const totalAmountInput = result.getAllByTestId("total-amount-input")[0]; + + const valueString = randomValue.toString(); + + fireEvent.change(totalAmountInput, { target: { value: valueString } }); + + result.rerender(); + + const newValueContract = toSmartContractDecimals(issueAmount.value, decimals); + + expect(Number(BigInt(newValueContract) % BigInt(100))).toBe(0); + + jest.clearAllMocks(); + result.unmount(); + + executions += 1; + } + }); +}); diff --git a/components/bounty/comments/input-comment/controller.tsx b/components/bounty/comments/input-comment/controller.tsx index d1d1f51a76..bb04a291db 100644 --- a/components/bounty/comments/input-comment/controller.tsx +++ b/components/bounty/comments/input-comment/controller.tsx @@ -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"; @@ -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, @@ -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(); } @@ -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} /> diff --git a/components/bounty/comments/input-comment/view.tsx b/components/bounty/comments/input-comment/view.tsx index fa8b13f9f6..7a5ee54198 100644 --- a/components/bounty/comments/input-comment/view.tsx +++ b/components/bounty/comments/input-comment/view.tsx @@ -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"; @@ -12,6 +13,9 @@ export default function InputCommentView({ userAddress, avatarHash, comment, + commentLength, + maxLength, + error = null, onCommentSubmit, onCommentChange , }: { @@ -19,48 +23,66 @@ export default function InputCommentView({ userAddress: string; avatarHash: string; comment: string; + commentLength: number; + maxLength: number; + error: "max-length" | null; onCommentSubmit: (...props) => void; onCommentChange : (e: ChangeEvent) => void }) { const { t } = useTranslation("common"); + const borderColor = error ? "danger" : "gray-700"; + const errorMessage = { + "max-length": t("errors.comment.max-length", { max: maxLength }), + }[error]; + return ( -
-
-
- - - {handle ? `@${handle}` : truncateAddress(userAddress)}{" "} - + <> +
+
+
+ + + {handle ? `@${handle}` : truncateAddress(userAddress)}{" "} + +
+ +
+ {commentLength}/{maxLength} +
-
-