From 438c3cc2b92d70817127d26e2c622f5a4af4fb6e Mon Sep 17 00:00:00 2001 From: Kacper Gumieniuk Date: Wed, 16 Aug 2023 18:58:08 +0200 Subject: [PATCH 1/4] Deposit in payment processor functionality --- blockchain/Dockerfile | 2 +- .../src/components/Asset/DepositCore.tsx | 164 +++++ web/apps/processor/src/components/Deposit.tsx | 180 +++++ .../components/Inputs/DepositNumberInput.tsx | 124 ++++ .../components/Inputs/DepositTextInput.tsx | 64 ++ .../src/components/providers/NavProvider.tsx | 1 + web/apps/processor/src/root.tsx | 34 +- web/packages/contracts/erc20.ts | 632 ++++++++++++++++++ web/packages/contracts/treasury.ts | 337 ++++++++++ 9 files changed, 1523 insertions(+), 15 deletions(-) create mode 100644 web/apps/processor/src/components/Asset/DepositCore.tsx create mode 100644 web/apps/processor/src/components/Deposit.tsx create mode 100644 web/apps/processor/src/components/Inputs/DepositNumberInput.tsx create mode 100644 web/apps/processor/src/components/Inputs/DepositTextInput.tsx create mode 100644 web/packages/contracts/erc20.ts create mode 100644 web/packages/contracts/treasury.ts 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..d0968d3a --- /dev/null +++ b/web/apps/processor/src/components/Asset/DepositCore.tsx @@ -0,0 +1,164 @@ +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"; + +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); + }} + /> +
{ + const address_value = address(); + const value = BigInt( + Math.floor(ev(fmul(props.asset.decimals, amount()))) + ); + if (wallet && address_value && wallet.address) { + const nonce = (await wallet.publicClient?.readContract({ + address: props.asset.address as Address, + abi: ERC20_ABI, + functionName: "nonces", + account: wallet.address as Address, + args: [wallet.address as Address], + })) as bigint; + + const deadline = + ((await wallet.publicClient?.getBlock())?.timestamp ?? 0n) + + 3600n; + + const domain = { + name: props.asset.name, + version: "1", + chainId: BigInt(wallet.selected_network.network.id), + verifyingContract: props.asset.address as Address, + }; + + const permit = { + owner: wallet.address as Address, + spender: TREASURY_ADDRESS as Address, + value, + nonce, + deadline, + }; + + console.log(domain, permit); + + const signature = await wallet.walletClient?.signTypedData({ + account: wallet.address as Address, + domain, + types: { + Permit: [ + { name: "owner", type: "address" }, + { name: "spender", type: "address" }, + { name: "value", type: "uint256" }, + { name: "nonce", type: "uint256" }, + { name: "deadline", type: "uint256" }, + ], + EIP712Domain: [ + { name: "name", type: "string" }, + { name: "version", type: "string" }, + { name: "chainId", type: "uint256" }, + { name: "verifyingContract", type: "address" }, + ], + }, + primaryType: "Permit", + message: permit, + }); + if (signature) { + const { r, s, v } = splitSig(signature); + await wallet.walletClient?.writeContract({ + chain: wallet.selected_network.network, + address: TREASURY_ADDRESS, + abi: TREASURY_ABI, + functionName: "depositPermit", + account: wallet.address as Address, + args: [ + props.asset.address as Address, + value, + deadline, + v, + r, + s, + ], + }); + } + } + }} + > + Deposit +
+
+ + ); +} diff --git a/web/apps/processor/src/components/Deposit.tsx b/web/apps/processor/src/components/Deposit.tsx new file mode 100644 index 00000000..0693645c --- /dev/null +++ b/web/apps/processor/src/components/Deposit.tsx @@ -0,0 +1,180 @@ +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: "} +

+
+ + {selectedAsset() && selectedAsset()!.name} + {/* */} +
+ ); +}; 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/providers/NavProvider.tsx b/web/apps/processor/src/components/providers/NavProvider.tsx index 862d2641..e75a6c21 100644 --- a/web/apps/processor/src/components/providers/NavProvider.tsx +++ b/web/apps/processor/src/components/providers/NavProvider.tsx @@ -10,6 +10,7 @@ export enum Nav { App, Account, Asset, + Deposit, } export const [nav, setNav] = createSignal