diff --git a/blockchain/Dockerfile b/blockchain/Dockerfile index 5f0159d8..5522ab7f 100644 --- a/blockchain/Dockerfile +++ b/blockchain/Dockerfile @@ -1,2 +1,2 @@ FROM ghcr.io/foundry-rs/foundry -ENTRYPOINT [ "anvil", "--block-time", "5", "--host", "0.0.0.0" ] +ENTRYPOINT [ "anvil", "--block-time", "1", "--host", "0.0.0.0" ] diff --git a/web/apps/processor/src/components/Asset/DepositCore.tsx b/web/apps/processor/src/components/Asset/DepositCore.tsx new file mode 100644 index 00000000..db27d82e --- /dev/null +++ b/web/apps/processor/src/components/Asset/DepositCore.tsx @@ -0,0 +1,103 @@ +import { Show, createEffect, createSignal } from "solid-js"; +import { useSession } from "@packages/components/providers/SessionProvider"; +import { Asset } from "@packages/types/asset"; +import { + Fraction, + ev, + fFromBigint, + fmul, +} from "@packages/types/primitives/fraction"; +import { DepositTextInput } from "../Inputs/DepositTextInput"; +import { useWallet } from "@packages/components/providers/WalletProvider"; +import { + ADDRESS, + ABI as TREASURY_ABI, + ADDRESS as TREASURY_ADDRESS, +} from "../../../../../packages/contracts/treasury"; +import { ABI as ERC20_ABI } from "../../../../../packages/contracts/erc20"; +import { Address } from "viem"; +import { DepositNumberInput } from "../Inputs/DepositNumberInput"; +import { handleDeposit } from "~/helpers/handleDeposit"; + +export function CreateProccessorDeposit(asset?: Asset, precision?: number) { + return () => ( + + + + ); +} + +const splitSig = (sig: string) => { + const pureSig = sig.replace("0x", ""); + const r = "0x" + pureSig.substring(0, 64); + const s = "0x" + pureSig.substring(64, 128); + const v = parseInt(pureSig.substring(128, 130), 16); + return { + r, + s, + v, + }; +}; + +export function DepositCore(props: { asset: Asset; precision: number }) { + const [amount, setAmount] = createSignal(fFromBigint(0n)); + const session = useSession(); + const wallet = useWallet(); + const [address, setAddress] = createSignal
( + wallet.address + ); + + createEffect(() => { + setAddress(wallet.address); + }); + + createEffect(() => {}); + + return ( + <> +
+ { + setAmount(f); + }} + /> + { + setAddress(f.toLowerCase() as Address); + }} + /> +
{ + try { + await handleDeposit({ + asset: props.asset, + address_value: address(), + amount: amount(), + treasury_address: ADDRESS, + wallet: wallet, + }); + } catch (error) { + console.error("Error during deposit:", error); + } + }} + > + Deposit +
+
+ + ); +} diff --git a/web/apps/processor/src/components/Asset/WithdrawCore.tsx b/web/apps/processor/src/components/Asset/WithdrawCore.tsx new file mode 100644 index 00000000..0f13edf8 --- /dev/null +++ b/web/apps/processor/src/components/Asset/WithdrawCore.tsx @@ -0,0 +1,104 @@ +import { Show, createEffect, createSignal } from "solid-js"; +import { useSession } from "@packages/components/providers/SessionProvider"; +import { Asset } from "@packages/types/asset"; +import { + Fraction, + ev, + fFromBigint, + fmul, +} from "@packages/types/primitives/fraction"; +import { DepositTextInput } from "../Inputs/DepositTextInput"; +import { useWallet } from "@packages/components/providers/WalletProvider"; +import { + ABI as TREASURY_ABI, + ADDRESS as TREASURY_ADDRESS, +} from "../../../../../packages/contracts/treasury"; +import { ABI as ERC20_ABI } from "../../../../../packages/contracts/erc20"; +import { Address } from "viem"; +import { DepositNumberInput } from "../Inputs/DepositNumberInput"; +import { WithdrawRequest, WithdrawResponse } from "@packages/types/mod"; +import { api } from "~/root"; +import { handleWithdraw } from "~/helpers/handleWithdraw"; + +export function CreateProccessorWithdraw(asset?: Asset, precision?: number) { + return () => ( + + + + ); +} + +const splitSig = (sig: string) => { + const pureSig = sig.replace("0x", ""); + const r = "0x" + pureSig.substring(0, 64); + const s = "0x" + pureSig.substring(64, 128); + const v = parseInt(pureSig.substring(128, 130), 16); + return { + r, + s, + v, + }; +}; + +export function WithdrawCore(props: { asset: Asset; precision: number }) { + const [amount, setAmount] = createSignal(fFromBigint(0n)); + const session = useSession(); + const wallet = useWallet(); + const [address, setAddress] = createSignal
( + wallet.address + ); + + createEffect(() => { + setAddress(wallet.address); + }); + + createEffect(() => {}); + + return ( + <> +
+ { + setAmount(f); + }} + /> + { + setAddress(f.toLowerCase() as Address); + }} + /> +
{ + try { + await handleWithdraw({ + asset: props.asset, + address_value: address(), + amount: amount(), + treasury_address: TREASURY_ADDRESS, + wallet: wallet, + }); + } catch (error) { + console.error("Error during deposit:", error); + } + }} + > + Withdraw +
+
+ + ); +} diff --git a/web/apps/processor/src/components/Deposit.tsx b/web/apps/processor/src/components/Deposit.tsx new file mode 100644 index 00000000..baa17be0 --- /dev/null +++ b/web/apps/processor/src/components/Deposit.tsx @@ -0,0 +1,178 @@ +import { + Component, + Show, + createEffect, + createMemo, + createSignal, + onCleanup, +} from "solid-js"; +import { useAssets } from "./providers/AssetsProvider"; +import { Asset } from "@packages/types/asset"; +import { joinPaths } from "solid-start/islands/server-router"; +import { api, base } from "~/root"; +import { AVAILABLE_CHAINS } from "@packages/components/providers/WalletProvider/chains"; +import { unwrap } from "solid-js/store"; +import { useWallet } from "@packages/components/providers/WalletProvider"; +import { CreateProccessorDeposit } from "./Asset/DepositCore"; +import { usePrecision } from "@packages/components/providers/PrecisionProvider"; +import { Dynamic } from "solid-js/web"; +import { Fraction, ev } from "@packages/types/primitives/fraction"; +import { useSession } from "@packages/components/providers/SessionProvider"; +import subscribeEvents from "@packages/utils/subscribeEvents"; +import params from "@packages/utils/params"; +import { Valut } from "@packages/types/valut"; +import { format } from "numerable"; +import { formatTemplate } from "@packages/utils/precision"; + +export const Deposit: Component<{}> = ({}) => { + const assets = useAssets(); + const wallet = useWallet(); + const session = useSession(); + const precision = usePrecision(); + const assetsList = createMemo(() => [...assets().values()]); + + const [selectedAsset, setSelectedAsset] = createSignal(); + + const assetMap = createMemo(() => { + const map = new Map(); + assetsList().forEach((asset) => map.set(asset.id, asset)); + return map; + }); + + const handleChangeCoin = (event: Event) => { + const assetId = (event.target as HTMLSelectElement).value; + setSelectedAsset(assetMap().get(assetId)); + }; + + createEffect(() => { + if (assetsList().length > 0) { + setSelectedAsset(assetsList()[0]); + } + }); + + const [balance, setBalance] = createSignal(undefined); + + let eventsource: EventSource | undefined; + + createEffect(async () => { + if (session() && selectedAsset() && precision()) { + const asset = selectedAsset(); + + eventsource = await subscribeEvents( + `${api}/private/balance`, + params({ asset_id: asset!.id }), + params({ asset_id: asset!.id }), + (data) => { + setBalance(Valut.parse(data).balance.Finite); + } + ); + } + }); + + onCleanup(() => { + eventsource?.close(); + }); + + return ( +
+

Deposit funds

+
+
+ +
+ + + +
+
+ + + +
+ {/* */} +
+
+ +
+ + + +
+
+ + + +
+ +

+ {balance() != undefined + ? `Your balance: ${format( + ev(balance()!), + formatTemplate(precision() ?? 3) + )} ${selectedAsset()?.symbol}` + : "Your balance: "} +

+
+ +
+ ); +}; diff --git a/web/apps/processor/src/components/Inputs/DepositNumberInput.tsx b/web/apps/processor/src/components/Inputs/DepositNumberInput.tsx new file mode 100644 index 00000000..330f60a3 --- /dev/null +++ b/web/apps/processor/src/components/Inputs/DepositNumberInput.tsx @@ -0,0 +1,124 @@ +import { format } from "numerable" +import { createMemo, createUniqueId, JSX } from "solid-js" +import { Fraction } from "@packages/types/primitives/fraction" +import { formatTemplate } from "@packages/utils/precision" + +export interface NumberInputComponent { + value?: Fraction + left?: JSX.Element + right?: JSX.Element + class?: JSX.HTMLAttributes["class"] + disabled?: boolean + disabledClass?: JSX.HTMLAttributes["class"] + precision?: number + onInput?: (f: Fraction) => void + onChange?: (f: Fraction) => void +} + +function formatHumanReadable(input: string, precision: number) { + // Remove all non-digit and non-dot characters + let formatted = + precision > 0 ? input.replace(/[^0-9.]/g, "") : input.replace(/[^0-9]/g, "") + formatted = formatted.replace(/(\..*?)\..*/g, "$1").replace(/^0[^.]/, "0") + + // Truncate decimal part to maxDigits digits + formatted = formatted.replace( + new RegExp("(\\.\\d{0," + precision + "})\\d*"), + "$1" + ) + + // Add commas every third digit before the dot + const dotIndex = formatted.indexOf(".") + if (dotIndex !== -1) { + const integerPart = formatted + .substring(0, dotIndex) + .replace(/\B(?=(\d{3})+(?!\d))/g, ",") + const decimalPart = formatted.substring(dotIndex) + formatted = + integerPart + + decimalPart.replace(/(?<=\.)[^.]+/g, (match) => match.replace(/\./g, "")) + } else { + formatted = formatted.replace(/\B(?=(\d{3})+(?!\d))/g, ",") + } + + // Ensure there are no two commas next to each other + formatted = formatted.replace(/(,)\1+/g, "$1") + + return formatted +} + +function humanReadableToBigint(input: string) { + const decimalNumber = input.replace(/,/g, "") + const index = decimalNumber.indexOf(".") + const decimalPlaces = + index >= 0 ? BigInt(decimalNumber.length - index - 1) : 0n + return Fraction.parse({ + numer: BigInt(decimalNumber.replace(".", "")), + denom: 10n ** decimalPlaces, + }) +} + +export function DepositNumberInput(props: NumberInputComponent) { + let inputDOM!: HTMLInputElement + const valueDOM = createMemo(() => { + return props.value == undefined ? { numer: 0n, denom: 1n } : props.value + }) + + const id = createUniqueId() + + return ( +
{ + inputDOM.focus() + }} + > + +
+ { + ;(e.target as HTMLInputElement).value = formatHumanReadable( + (e.target as HTMLInputElement).value, + props.precision ?? 3 + ) + if (props.onInput != undefined) { + props.onInput( + humanReadableToBigint((e.target as HTMLInputElement).value) + ) + } + }} + onChange={(e) => { + if (props.onChange != undefined) { + props.onChange( + humanReadableToBigint((e.target as HTMLInputElement).value) + ) + } + }} + onFocus={(e) => { + ;(e.target as HTMLInputElement).select() + }} + /> +
+ {props.right} +
+
+
+ ) +} diff --git a/web/apps/processor/src/components/Inputs/DepositTextInput.tsx b/web/apps/processor/src/components/Inputs/DepositTextInput.tsx new file mode 100644 index 00000000..554cb9f6 --- /dev/null +++ b/web/apps/processor/src/components/Inputs/DepositTextInput.tsx @@ -0,0 +1,64 @@ +import { createUniqueId, JSX } from "solid-js" + +export interface TextInputComponent { + value?: string + left?: JSX.Element + right?: JSX.Element + class?: JSX.HTMLAttributes["class"] + disabled?: boolean + disabledClass?: JSX.HTMLAttributes["class"] + precision?: number + onInput?: (f: string) => void + onChange?: (f: string) => void +} + +export function DepositTextInput(props: TextInputComponent) { + let inputDOM!: HTMLInputElement + + const id = createUniqueId() + + return ( +
{ + inputDOM.focus() + }} + > + +
+ { + if (props.onInput != undefined) { + props.onInput(e.target.value) + } + }} + onChange={(e) => { + if (props.onChange != undefined) { + props.onChange(e.target.value) + } + }} + onFocus={(e) => { + ;(e.target as HTMLInputElement).select() + }} + /> +
+ {/*
+ {props.right} +
*/} +
+ ) +} diff --git a/web/apps/processor/src/components/Navigation.tsx b/web/apps/processor/src/components/Navigation.tsx index 4fa2da03..55132662 100644 --- a/web/apps/processor/src/components/Navigation.tsx +++ b/web/apps/processor/src/components/Navigation.tsx @@ -5,7 +5,7 @@ import { useNav, Nav } from "~/components/providers/NavProvider"; export default function Navigation() { const nav = useNav(); return ( -
+ ); } diff --git a/web/apps/processor/src/components/Withdraw.tsx b/web/apps/processor/src/components/Withdraw.tsx new file mode 100644 index 00000000..2eb5aa37 --- /dev/null +++ b/web/apps/processor/src/components/Withdraw.tsx @@ -0,0 +1,179 @@ +import { + Component, + Show, + createEffect, + createMemo, + createSignal, + onCleanup, +} from "solid-js"; +import { useAssets } from "./providers/AssetsProvider"; +import { Asset } from "@packages/types/asset"; +import { joinPaths } from "solid-start/islands/server-router"; +import { api, base } from "~/root"; +import { AVAILABLE_CHAINS } from "@packages/components/providers/WalletProvider/chains"; +import { unwrap } from "solid-js/store"; +import { useWallet } from "@packages/components/providers/WalletProvider"; +import { CreateProccessorDeposit } from "./Asset/DepositCore"; +import { usePrecision } from "@packages/components/providers/PrecisionProvider"; +import { Dynamic } from "solid-js/web"; +import { Fraction, ev } from "@packages/types/primitives/fraction"; +import { useSession } from "@packages/components/providers/SessionProvider"; +import subscribeEvents from "@packages/utils/subscribeEvents"; +import params from "@packages/utils/params"; +import { Valut } from "@packages/types/valut"; +import { format } from "numerable"; +import { formatTemplate } from "@packages/utils/precision"; +import { CreateProccessorWithdraw } from "./Asset/WithdrawCore"; + +export const Withdraw: Component<{}> = ({}) => { + const assets = useAssets(); + const wallet = useWallet(); + const session = useSession(); + const precision = usePrecision(); + const assetsList = createMemo(() => [...assets().values()]); + + const [selectedAsset, setSelectedAsset] = createSignal(); + + const assetMap = createMemo(() => { + const map = new Map(); + assetsList().forEach((asset) => map.set(asset.id, asset)); + return map; + }); + + const handleChangeCoin = (event: Event) => { + const assetId = (event.target as HTMLSelectElement).value; + setSelectedAsset(assetMap().get(assetId)); + }; + + createEffect(() => { + if (assetsList().length > 0) { + setSelectedAsset(assetsList()[0]); + } + }); + + const [balance, setBalance] = createSignal(undefined); + + let eventsource: EventSource | undefined; + + createEffect(async () => { + if (session() && selectedAsset() && precision()) { + const asset = selectedAsset(); + + eventsource = await subscribeEvents( + `${api}/private/balance`, + params({ asset_id: asset!.id }), + params({ asset_id: asset!.id }), + (data) => { + setBalance(Valut.parse(data).balance.Finite); + } + ); + } + }); + + onCleanup(() => { + eventsource?.close(); + }); + + return ( +
+

Withdraw funds

+
+
+ +
+ + + +
+
+ + + +
+ {/* */} +
+
+ +
+ + + +
+
+ + + +
+ +

+ {balance() != undefined + ? `Your balance: ${format( + ev(balance()!), + formatTemplate(precision() ?? 3) + )} ${selectedAsset()?.symbol}` + : "Your balance: "} +

+
+ +
+ ); +}; diff --git a/web/apps/processor/src/components/providers/NavProvider.tsx b/web/apps/processor/src/components/providers/NavProvider.tsx index 862d2641..36019558 100644 --- a/web/apps/processor/src/components/providers/NavProvider.tsx +++ b/web/apps/processor/src/components/providers/NavProvider.tsx @@ -4,21 +4,21 @@ import { createSignal, JSX, useContext, -} from "solid-js"; +} from 'solid-js' export enum Nav { App, Account, Asset, + Deposit, + Withdraw, } -export const [nav, setNav] = createSignal