diff --git a/package.json b/package.json index 752da197..6f83c8ed 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@tanstack/react-query": "^5.32.1", "@tanstack/react-query-devtools": "^5.32.1", "@typescript-eslint/eslint-plugin": "^7.8.0", + "bignumber.js": "^9.1.2", "bindings-js": "file:./src/temp/stellar-xdr-web", "dompurify": "^3.1.2", "html-react-parser": "^5.1.10", diff --git a/src/app/(sidebar)/endpoints/[[...pages]]/page.tsx b/src/app/(sidebar)/endpoints/[[...pages]]/page.tsx index c75cce56..4f2608c1 100644 --- a/src/app/(sidebar)/endpoints/[[...pages]]/page.tsx +++ b/src/app/(sidebar)/endpoints/[[...pages]]/page.tsx @@ -18,7 +18,7 @@ import { useQueryClient } from "@tanstack/react-query"; import { InfoCards } from "@/components/InfoCards"; import { SdsLink } from "@/components/SdsLink"; import { NextLink } from "@/components/NextLink"; -import { formComponentTemplate } from "@/components/formComponentTemplate"; +import { formComponentTemplateEndpoints } from "@/components/formComponentTemplateEndpoints"; import { PrettyJson } from "@/components/PrettyJson"; import { InputSideElement } from "@/components/InputSideElement"; @@ -232,7 +232,7 @@ export default function Endpoints() { // Validate saved params when the page loads const paramErrors = () => { return Object.keys(params).reduce((res, param) => { - const error = formComponentTemplate(param)?.validate?.( + const error = formComponentTemplateEndpoints(param)?.validate?.( parseJsonString(params[param]), requiredFields.includes(param), ); @@ -498,7 +498,10 @@ export default function Endpoints() { {renderPostPayload()} {allFields.map((f) => { - const component = formComponentTemplate(f, pageData.custom?.[f]); + const component = formComponentTemplateEndpoints( + f, + pageData.custom?.[f], + ); if (component) { const isRequired = requiredFields.includes(f); diff --git a/src/app/(sidebar)/transaction/build/components/Operations.tsx b/src/app/(sidebar)/transaction/build/components/Operations.tsx index dc1a403a..63e59da9 100644 --- a/src/app/(sidebar)/transaction/build/components/Operations.tsx +++ b/src/app/(sidebar)/transaction/build/components/Operations.tsx @@ -3,16 +3,19 @@ import { ChangeEvent, useEffect, useState } from "react"; import { Badge, Button, Card, Icon, Select } from "@stellar/design-system"; -import { formComponentTemplate } from "@/components/formComponentTemplate"; +import { formComponentTemplateTxnOps } from "@/components/formComponentTemplateTxnOps"; import { Box } from "@/components/layout/Box"; import { TabbedButtons } from "@/components/TabbedButtons"; import { ValidationResponseCard } from "@/components/ValidationResponseCard"; +import { SdsLink } from "@/components/SdsLink"; import { arrayItem } from "@/helpers/arrayItem"; import { isEmptyObject } from "@/helpers/isEmptyObject"; +import { sanitizeObject } from "@/helpers/sanitizeObject"; + import { TRANSACTION_OPERATIONS } from "@/constants/transactionOperations"; import { useStore } from "@/store/useStore"; -import { TxnOperation } from "@/types/types"; +import { AssetObjectValue, TxnOperation } from "@/types/types"; export const Operations = () => { const { transaction } = useStore(); @@ -174,6 +177,38 @@ export const Operations = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const missingSelectedAssetFields = ( + param: string, + value: any, + ): { isAssetField: boolean; missingAssetFields: string[] } => { + const assetInputs = ["asset", "selling", "buying"]; + const isAssetField = assetInputs.includes(param); + + const initialValues = { + isAssetField, + missingAssetFields: [], + }; + + if (isAssetField) { + if (!value || value === "native") { + return initialValues; + } + + const assetInputs = (Object.values(value)[0] || {}) as { + asset_code: string; + issuer: string; + }; + + return { + isAssetField, + missingAssetFields: + assetInputs.asset_code && assetInputs.issuer ? [] : [param], + }; + } + + return initialValues; + }; + const validateOperationParam = ({ opIndex, opParam, @@ -187,13 +222,18 @@ export const Operations = () => { opParamError?: OperationError; opType: string; }): OperationError => { - const validateFn = formComponentTemplate(opParam)?.validate; + const validateFn = formComponentTemplateTxnOps({ + param: opParam, + opType, + index: opIndex, + })?.validate; const opError = opParamError || operationsError[opIndex] || EMPTY_OPERATION_ERROR; const opParamErrorFields = { ...opError.error }; let opParamMissingFields = [...opError.missingFields]; + //==== Handle input validation for entered value if (validateFn) { const error = validateFn(opValue); @@ -204,6 +244,7 @@ export const Operations = () => { } } + //==== Handle missing required fields // If param needs value and there is value entered, remove param from // missing fields. If there is no value, nothing to do. if (opParamMissingFields.includes(opParam)) { @@ -220,6 +261,20 @@ export const Operations = () => { } } + //==== Handle selected asset with missing fields + const missingAsset = missingSelectedAssetFields(opParam, opValue); + + if ( + missingAsset.isAssetField && + missingAsset.missingAssetFields.length > 0 + ) { + // If there is a missing asset value and the param is not in required + // fields, add it to the missing fields + if (!opParamMissingFields.includes(opParam)) { + opParamMissingFields = [...opParamMissingFields, opParam]; + } + } + return { operationType: opType, error: opParamErrorFields, @@ -242,10 +297,10 @@ export const Operations = () => { updateBuildSingleOperation(opIndex, { ...op, - params: { + params: sanitizeObject({ ...op?.params, [opParam]: opValue, - }, + }), }); const validatedOpParam = validateOperationParam({ @@ -334,7 +389,25 @@ export const Operations = () => { }; const formErrors = getOperationsError(); - const sourceAccountComponent = formComponentTemplate("source_account"); + + const renderSourceAccount = (opType: string, index: number) => { + const sourceAccountComponent = formComponentTemplateTxnOps({ + param: "source_account", + opType, + index, + }); + + return opType && sourceAccountComponent + ? sourceAccountComponent.render({ + value: txnOperations[index].source_account, + error: operationsError[index]?.error?.["source_account"], + isRequired: false, + onChange: (e: ChangeEvent) => { + handleOperationSourceAccountChange(index, e.target.value, opType); + }, + }) + : null; + }; const OperationTabbedButtons = ({ index, @@ -406,109 +479,117 @@ export const Operations = () => { }: { index: number; operationType: string; - }) => ( - { + updateBuildSingleOperation(index, { + operation_type: e.target.value, + params: [], + source_account: "", + }); + + let initParamError: OperationError = EMPTY_OPERATION_ERROR; + + // Get operation required fields if there is operation type + if (e.target.value) { + initParamError = { + ...initParamError, + missingFields: [ + ...(TRANSACTION_OPERATIONS[e.target.value]?.requiredParams || + []), + ], + operationType: e.target.value, + }; + } - // Get operation required fields if there is operation type - if (e.target.value) { - initParamError = { - ...initParamError, - missingFields: [ - ...(TRANSACTION_OPERATIONS[e.target.value]?.requiredParams || []), - ], - operationType: e.target.value, - }; + setOperationsError([ + ...arrayItem.update(operationsError, index, initParamError), + ]); + }} + note={ + opInfo ? ( + <> + {opInfo.description}{" "} + See documentation. + + ) : null } - - setOperationsError([ - ...arrayItem.update(operationsError, index, initParamError), - ]); - }} - > - - - {/* TODO: remove disabled attribute when operation is implemented */} - - - - - - - - - - - - - - - - - - - - - - - - ); + > + {/* TODO: remove disabled attribute when operation is implemented */} + + + + + + + + + + + + + + + + + + + + + + + + + + ); + }; return ( @@ -547,25 +628,72 @@ export const Operations = () => { <> {TRANSACTION_OPERATIONS[op.operation_type]?.params.map( (input) => { - const component = formComponentTemplate(input); + const component = formComponentTemplateTxnOps({ + param: input, + opType: op.operation_type, + index: idx, + custom: + TRANSACTION_OPERATIONS[op.operation_type].custom?.[ + input + ], + }); + const baseProps = { + value: txnOperations[idx]?.params[input], + error: operationsError[idx]?.error?.[input], + isRequired: + TRANSACTION_OPERATIONS[ + op.operation_type + ].requiredParams.includes(input), + }; if (component) { - return component.render({ - value: txnOperations[idx]?.params[input], - error: operationsError[idx]?.error?.[input], - isRequired: - TRANSACTION_OPERATIONS[ - op.operation_type - ].requiredParams.includes(input), - onChange: (e: ChangeEvent) => { - handleOperationParamChange({ - opIndex: idx, - opParam: input, - opValue: e.target.value, - opType: op.operation_type, + switch (input) { + case "asset": + case "buying": + case "selling": + return component.render({ + ...baseProps, + onChange: (assetValue: AssetObjectValue) => { + let asset; + + if (assetValue.type === "native") { + asset = "native"; + } else if ( + assetValue.type && + [ + "credit_alphanum4", + "credit_alphanum12", + ].includes(assetValue.type) + ) { + asset = { + [assetValue.type]: { + asset_code: assetValue.code, + issuer: assetValue.issuer, + }, + }; + } + + handleOperationParamChange({ + opIndex: idx, + opParam: input, + opValue: asset, + opType: op.operation_type, + }); + }, + }); + default: + return component.render({ + ...baseProps, + onChange: (e: ChangeEvent) => { + handleOperationParamChange({ + opIndex: idx, + opParam: input, + opValue: e.target.value, + opType: op.operation_type, + }); + }, }); - }, - }); + } } return null; @@ -574,22 +702,7 @@ export const Operations = () => { {/* Optional source account for all operations */} - <> - {op.operation_type && sourceAccountComponent - ? sourceAccountComponent.render({ - value: txnOperations[idx].source_account, - error: operationsError[idx]?.error?.["source_account"], - isRequired: false, - onChange: (e: ChangeEvent) => { - handleOperationSourceAccountChange( - idx, - e.target.value, - op.operation_type, - ); - }, - }) - : null} - + <>{renderSourceAccount(op.operation_type, idx)} ))} diff --git a/src/app/(sidebar)/transaction/build/components/TransactionXdr.tsx b/src/app/(sidebar)/transaction/build/components/TransactionXdr.tsx index 732b63ed..8794cc4f 100644 --- a/src/app/(sidebar)/transaction/build/components/TransactionXdr.tsx +++ b/src/app/(sidebar)/transaction/build/components/TransactionXdr.tsx @@ -11,6 +11,8 @@ import { ValidationResponseCard } from "@/components/ValidationResponseCard"; import { Box } from "@/components/layout/Box"; import { isEmptyObject } from "@/helpers/isEmptyObject"; +import { xdrUtils } from "@/helpers/xdr/utils"; + import { useStore } from "@/store/useStore"; import { Routes } from "@/constants/routes"; import { AnyObject, KeysOfUnion } from "@/types/types"; @@ -55,7 +57,7 @@ export const TransactionXdr = () => { val = BigInt(value); break; case "fee": - val = BigInt(value); + val = BigInt(value) * BigInt(txnOperations.length); break; case "cond": // eslint-disable-next-line no-case-declarations @@ -91,18 +93,27 @@ export const TransactionXdr = () => { return { ...res, [key]: val }; }, {}); - const parseOpParams = ({ - params, - amountParams, - }: { - params: AnyObject; - amountParams: string[]; - }) => { + const getXdrVal = (key: string, val: any) => { + switch (key) { + // Amount + case "amount": + case "buy_amount": + case "starting_balance": + return xdrUtils.toAmount(val); + // Number + case "offer_id": + return BigInt(val); + // Price + case "price": + return xdrUtils.toPrice(val); + default: + return val; + } + }; + + const parseOpParams = ({ params }: { params: AnyObject }) => { return Object.entries(params).reduce((res, [key, val]) => { - res[key] = amountParams.includes(key) - ? // Multiplying to create raw XDR amount - BigInt(val) * BigInt(10000000) - : val; + res[key] = getXdrVal(key, val); return res; }, {} as AnyObject); @@ -113,7 +124,6 @@ export const TransactionXdr = () => { body: { [op.operation_type]: parseOpParams({ params: op.params, - amountParams: ["starting_balance"], }), }, })); diff --git a/src/components/formComponentTemplate.tsx b/src/components/formComponentTemplateEndpoints.tsx similarity index 99% rename from src/components/formComponentTemplate.tsx rename to src/components/formComponentTemplateEndpoints.tsx index 3d31dc7c..26f684a1 100644 --- a/src/components/formComponentTemplate.tsx +++ b/src/components/formComponentTemplateEndpoints.tsx @@ -48,15 +48,15 @@ type TemplateRenderIncludeFailedProps = { isRequired?: boolean; }; -type FormComponentTemplate = { +type FormComponentTemplateEndpointsProps = { render: (...args: any[]) => JSX.Element; validate: ((...args: any[]) => any) | null; }; -export const formComponentTemplate = ( +export const formComponentTemplateEndpoints = ( id: string, custom?: AnyObject, -): FormComponentTemplate | null => { +): FormComponentTemplateEndpointsProps | null => { switch (id) { case "account_id": return { diff --git a/src/components/formComponentTemplateTxnOps.tsx b/src/components/formComponentTemplateTxnOps.tsx new file mode 100644 index 00000000..9cb9b8e5 --- /dev/null +++ b/src/components/formComponentTemplateTxnOps.tsx @@ -0,0 +1,216 @@ +import { JSX } from "react"; + +import { PubKeyPicker } from "@/components/FormElements/PubKeyPicker"; +import { TextPicker } from "@/components/FormElements/TextPicker"; +import { AssetPicker } from "@/components/FormElements/AssetPicker"; +import { PositiveIntPicker } from "@/components/FormElements/PositiveIntPicker"; + +import { validate } from "@/validate"; +import { AnyObject, AssetObjectValue, JsonAsset } from "@/types/types"; + +// Types +type TemplateRenderProps = { + value: string | undefined; + error: string | undefined; + onChange: (val: any) => void; + isRequired?: boolean; +}; + +type TemplateRenderAssetProps = { + value: JsonAsset | undefined; + error: { code: string | undefined; issuer: string | undefined } | undefined; + onChange: (asset: AssetObjectValue | undefined) => void; + isRequired?: boolean; +}; + +type FormComponentTemplateTxnOpsProps = { + render: (...args: any[]) => JSX.Element; + validate: ((...args: any[]) => any) | null; +}; + +const assetPickerValue = ( + value: JsonAsset | undefined, +): AssetObjectValue | undefined => { + if (!value) { + return undefined; + } + + if (value === "native") { + return { type: "native", code: "", issuer: "" }; + } + + const type = Object.keys(value)[0] as keyof typeof value; + const val = value[type] as { + asset_code: string; + issuer: string; + }; + + return { + type, + code: val.asset_code, + issuer: val.issuer, + }; +}; + +export const formComponentTemplateTxnOps = ({ + param, + opType, + index, + custom, +}: { + param: string; + opType: string; + index: number; + custom?: AnyObject; +}): FormComponentTemplateTxnOpsProps | null => { + const id = `${index}-${opType}-${param}`; + + switch (param) { + case "amount": + case "buy_amount": + return { + render: (templ: TemplateRenderProps) => ( + + ), + validate: validate.amount, + }; + case "asset": + return { + render: (templ: TemplateRenderAssetProps) => ( + + ), + validate: validate.assetJson, + }; + case "buying": + return { + render: (templ: TemplateRenderAssetProps) => ( + + ), + validate: validate.assetJson, + }; + case "destination": + return { + render: (templ: TemplateRenderProps) => ( + + ), + validate: validate.publicKey, + }; + case "offer_id": + return { + render: (templ: TemplateRenderProps) => ( + + ), + validate: validate.positiveInt, + }; + case "price": + return { + render: (templ: TemplateRenderProps) => ( + + ), + validate: validate.positiveNumber, + }; + case "selling": + return { + render: (templ: TemplateRenderAssetProps) => ( + + ), + validate: validate.assetJson, + }; + case "source_account": + return { + render: (templ: TemplateRenderProps) => ( + + ), + validate: validate.publicKey, + }; + case "starting_balance": + return { + render: (templ: TemplateRenderProps) => ( + + ), + validate: validate.amount, + }; + default: + return null; + } +}; diff --git a/src/constants/transactionOperations.ts b/src/constants/transactionOperations.ts index 15e24d3d..405273a8 100644 --- a/src/constants/transactionOperations.ts +++ b/src/constants/transactionOperations.ts @@ -1,9 +1,12 @@ +import { AnyObject } from "@/types/types"; + type TransactionOperation = { label: string; description: string; docsUrl: string; params: string[]; requiredParams: string[]; + custom?: AnyObject; }; export const TRANSACTION_OPERATIONS: { [key: string]: TransactionOperation } = { @@ -16,4 +19,71 @@ export const TRANSACTION_OPERATIONS: { [key: string]: TransactionOperation } = { params: ["destination", "starting_balance"], requiredParams: ["destination", "starting_balance"], }, + payment: { + label: "Payment", + description: + "Sends an amount in a specific asset to a destination account.", + docsUrl: + "https://developers.stellar.org/docs/learn/fundamentals/list-of-operations#payment", + params: ["destination", "asset", "amount"], + requiredParams: ["destination", "asset", "amount"], + }, + manage_sell_offer: { + label: "Manage Sell Offer", + description: "Creates, updates, or deletes an offer.", + docsUrl: + "https://developers.stellar.org/docs/learn/fundamentals/list-of-operations#manage-sell-offer", + params: ["selling", "buying", "amount", "price", "offer_id"], + requiredParams: ["selling", "buying", "amount", "price", "offer_id"], + custom: { + amount: { + label: "Amount you are selling", + note: "An amount of zero will delete the offer.", + }, + price: { + label: "Price of 1 unit of selling in terms of buying", + }, + offer_id: { + note: "If 0, will create a new offer. Existing offer id numbers can be found using the Offers for Account endpoint.", + }, + }, + }, + manage_buy_offer: { + label: "Manage Buy Offer", + description: "Creates, updates, or deletes an offer.", + docsUrl: + "https://developers.stellar.org/docs/learn/fundamentals/list-of-operations#manage-buy-offer", + params: ["selling", "buying", "buy_amount", "price", "offer_id"], + requiredParams: ["selling", "buying", "buy_amount", "price", "offer_id"], + custom: { + buy_amount: { + label: "Amount you are buying", + note: "An amount of zero will delete the offer.", + }, + price: { + label: "Price of 1 unit of buying in terms of selling", + }, + offer_id: { + note: "If 0, will create a new offer. Existing offer id numbers can be found using the Offers for Account endpoint.", + }, + }, + }, + create_passive_sell_offer: { + label: "Create Passive Sell Offer", + description: + "Creates an offer that does not take another offer of equal price when created.", + docsUrl: + "https://developers.stellar.org/docs/learn/fundamentals/list-of-operations#create-passive-sell-offer", + params: ["selling", "buying", "amount", "price"], + requiredParams: ["selling", "buying", "amount", "price"], + custom: { + amount: { + label: "Amount you are selling", + note: "An amount of zero will delete the offer.", + }, + price: { + label: "Price of 1 unit of selling in terms of buying", + }, + }, + }, }; diff --git a/src/helpers/xdr/fraction.ts b/src/helpers/xdr/fraction.ts new file mode 100644 index 00000000..7a12f215 --- /dev/null +++ b/src/helpers/xdr/fraction.ts @@ -0,0 +1,42 @@ +// This content is copied from js-stellar-base (src/util/continued_fraction.js) +import BigNumber from "bignumber.js"; + +const MAX_INT = ((1 << 31) >>> 0) - 1; + +export function best_r(rawNumber: string | number | BigNumber) { + let number = new BigNumber(rawNumber); + let a; + let f; + const fractions = [ + [new BigNumber(0), new BigNumber(1)], + [new BigNumber(1), new BigNumber(0)], + ]; + let i = 2; + + // eslint-disable-next-line no-constant-condition + while (true) { + if (number.gt(MAX_INT)) { + break; + } + a = number.integerValue(BigNumber.ROUND_FLOOR); + f = number.minus(a); + const h = a.times(fractions[i - 1][0]).plus(fractions[i - 2][0]); + const k = a.times(fractions[i - 1][1]).plus(fractions[i - 2][1]); + if (h.gt(MAX_INT) || k.gt(MAX_INT)) { + break; + } + fractions.push([h, k]); + if (f.eq(0)) { + break; + } + number = new BigNumber(1).div(f); + i += 1; + } + const [n, d] = fractions[fractions.length - 1]; + + if (n.isZero() || d.isZero()) { + throw new Error("Couldn't find approximation"); + } + + return [n.toNumber(), d.toNumber()]; +} diff --git a/src/helpers/xdr/utils.ts b/src/helpers/xdr/utils.ts new file mode 100644 index 00000000..572e982f --- /dev/null +++ b/src/helpers/xdr/utils.ts @@ -0,0 +1,36 @@ +// XDR helpers from js-stellar-base +import { xdr } from "@stellar/stellar-sdk"; +import BigNumber from "bignumber.js"; +import { best_r } from "./fraction"; + +const ONE = 10000000; + +function toXDRAmount(value: string) { + // Using BigNumber to handle decimal point values + return BigInt(new BigNumber(value).times(ONE).toString()); +} + +function fromXDRAmount(value: string) { + return new BigNumber(value).div(ONE).toFixed(7); +} + +function toXDRPrice(price: string) { + const approx = best_r(price); + + return { + n: parseInt(approx[0].toString(), 10), + d: parseInt(approx[1].toString(), 10), + }; +} + +function fromXDRPrice(price: xdr.Price) { + const n = new BigNumber(price.n()); + return n.div(new BigNumber(price.d())).toString(); +} + +export const xdrUtils = { + toAmount: toXDRAmount, + fromAmount: fromXDRAmount, + toPrice: toXDRPrice, + fromPrice: fromXDRPrice, +}; diff --git a/src/types/types.ts b/src/types/types.ts index 923c5ce3..48577652 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -88,6 +88,21 @@ export type AssetObject = { value: AssetObjectValue; }; +export type JsonAsset = + | "native" + | { + credit_alphanum4: { + asset_code: string; + issuer: string; + }; + } + | { + credit_alphanum12: { + asset_code: string; + issuer: string; + }; + }; + // ============================================================================= // Transaction // ============================================================================= diff --git a/src/validate/index.ts b/src/validate/index.ts index 04f32f3c..4ee3d928 100644 --- a/src/validate/index.ts +++ b/src/validate/index.ts @@ -1,9 +1,11 @@ import { amount } from "./methods/amount"; import { asset } from "./methods/asset"; import { assetCode } from "./methods/assetCode"; +import { assetJson } from "./methods/assetJson"; import { assetMulti } from "./methods/assetMulti"; import { memo } from "./methods/memo"; import { positiveInt } from "./methods/positiveInt"; +import { positiveNumber } from "./methods/positiveNumber"; import { publicKey } from "./methods/publicKey"; import { timeBounds } from "./methods/timeBounds"; import { transactionHash } from "./methods/transactionHash"; @@ -13,9 +15,11 @@ export const validate = { amount, asset, assetCode, + assetJson, assetMulti, memo, positiveInt, + positiveNumber, publicKey, timeBounds, transactionHash, diff --git a/src/validate/methods/asset.ts b/src/validate/methods/asset.ts index 2969c4a7..c3eb012f 100644 --- a/src/validate/methods/asset.ts +++ b/src/validate/methods/asset.ts @@ -1,7 +1,7 @@ import { isEmptyObject } from "@/helpers/isEmptyObject"; +import { AssetObjectValue } from "@/types/types"; import { assetCode } from "./assetCode"; import { publicKey } from "./publicKey"; -import { AssetObjectValue } from "@/types/types"; export const asset = (asset: AssetObjectValue | undefined) => { if (asset?.type && asset.type === "native") { diff --git a/src/validate/methods/assetJson.ts b/src/validate/methods/assetJson.ts new file mode 100644 index 00000000..7ec5d35b --- /dev/null +++ b/src/validate/methods/assetJson.ts @@ -0,0 +1,32 @@ +import { isEmptyObject } from "@/helpers/isEmptyObject"; +import { JsonAsset } from "@/types/types"; +import { assetCode } from "./assetCode"; +import { publicKey } from "./publicKey"; + +// Validate asset in XDR or JSON format +export const assetJson = (asset: JsonAsset | undefined) => { + if (!asset || asset === "native") { + return false; + } + + const type = Object.keys(asset)[0] as keyof typeof asset; + const values = asset[type] as { + asset_code: string; + issuer: string; + }; + + const invalid = Object.entries({ + code: assetCode(values.asset_code || "", type), + issuer: publicKey(values.issuer || ""), + }).reduce((res, cur) => { + const [key, value] = cur; + + if (value) { + return { ...res, [key]: value }; + } + + return res; + }, {}); + + return isEmptyObject(invalid) ? false : invalid; +}; diff --git a/src/validate/methods/positiveNumber.ts b/src/validate/methods/positiveNumber.ts new file mode 100644 index 00000000..b4f6d88c --- /dev/null +++ b/src/validate/methods/positiveNumber.ts @@ -0,0 +1,9 @@ +export const positiveNumber = (value: string) => { + if (value.toString().charAt(0) === "-") { + return "Expected a positive number or zero."; + } else if (!value.toString().match(/^[0-9]*(\.[0-9]+){0,1}$/g)) { + return "Expected a positive number with a period for the decimal point."; + } + + return false; +};