Skip to content

Commit

Permalink
Restructure Transaction Details (#233)
Browse files Browse the repository at this point in the history
* init templates context

* rework transaction details

* add function to extract erc20 meta from receipt

* rework template meta

* rename favorites to templates

* change validation for padded address

* fix formatting issues

* remove leading zeros from address if any

* fixed the condition for "from"

* fix parsing address with leading zero

* updated log parsing

* Update app/ts/schema.ts

Co-authored-by: Micah Zoltu <[email protected]>

* fix ethers transfer saving

* fix double loading

---------

Co-authored-by: Micah Zoltu <[email protected]>
  • Loading branch information
jubalm and MicahZoltu authored Feb 20, 2024
1 parent 5d35de2 commit a073352
Show file tree
Hide file tree
Showing 17 changed files with 494 additions and 342 deletions.
22 changes: 14 additions & 8 deletions app/ts/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,27 @@ import { TransferPage } from './TransferPage/index.js'
import { EthereumProvider } from '../context/Ethereum.js'
import { WalletProvider } from '../context/Wallet.js'
import { NotificationProvider } from '../context/Notification.js'
import { TemplatesProvider } from '../context/TransferTemplates.js'

export function App() {
return (
<SplashScreen>
<NotificationProvider>
<EthereumProvider>
<WalletProvider>
<Router>
<Route path=''>
<TransferPage />
</Route>
<Route path='#tx/:transaction_hash'>
<TransactionPage />
</Route>
</Router>
<TemplatesProvider>
<Router>
<Route path=''>
<TransferPage />
</Route>
<Route path='#saved/:template_id'>
<TransferPage />
</Route>
<Route path='#tx/:transaction_hash'>
<TransactionPage />
</Route>
</Router>
</TemplatesProvider>
</WalletProvider>
</EthereumProvider>
</NotificationProvider>
Expand Down
2 changes: 1 addition & 1 deletion app/ts/components/HashRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const Router = ({ children }: { children: unknown | unknown[] }) => {
return <>{router.value.activeRoute}</>
}

export function useRouter<T extends { [key: string]: string }>() {
export function useRouter<T extends Partial<{ [key: string]: string }>>() {
return useComputed(() => ({ activeRoute: router.value.activeRoute, params: router.value.params as T }))
}

Expand Down
3 changes: 2 additions & 1 deletion app/ts/components/SetupTransfer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { useNotice } from '../store/notice.js'
import { TokenPicker } from './TokenPicker.js'
import { TokenAdd } from './TokenAdd.js'
import { TransferResult } from './TransferResult.js'
import { TemplateFeeder } from './TemplateFeeder.js'

export function SetupTransfer() {
return (
Expand All @@ -29,6 +30,7 @@ export function SetupTransfer() {
<TransferRecorder />
<TokenPicker />
<TokenAdd />
<TemplateFeeder />
</div>
</TransferForm>
)
Expand Down Expand Up @@ -82,6 +84,5 @@ const TransferForm = ({ children }: { children: ComponentChildren }) => {

useSignalEffect(listenForWalletsChainChange)
useSignalEffect(listenForQueryChanges)

return <form onSubmit={sendTransferRequest}>{children}</form>
}
38 changes: 38 additions & 0 deletions app/ts/components/TemplateFeeder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useComputed, useSignalEffect } from "@preact/signals"
import { formatUnits } from "ethers"
import { useTokenManager } from "../context/TokenManager.js"
import { useTransfer } from "../context/Transfer.js"
import { useTemplates } from "../context/TransferTemplates.js"
import { useRouter } from "./HashRouter.js"

export const TemplateFeeder = () => {
const templates = useTemplates()
const tokens = useTokenManager()
const router = useRouter<{ template_id: string | undefined }>()
const transfer = useTransfer()

const activeTemplate = useComputed(() => {
const templateIdFromParams = router.value.params.template_id
if (templateIdFromParams === undefined) return
const templateId = parseInt(templateIdFromParams)
return templates.cache.peek().data.at(templateId)
})

const selectedToken = useComputed(() => {
const tokensCache = tokens.cache.value.data
if (activeTemplate.value === undefined) return
const templateContractAddress = activeTemplate.value.contractAddress
return tokensCache.find(token => token.address === templateContractAddress)
})

const feedTemplateToTransferInput = () => {
const tmpl = activeTemplate.value
if (tmpl === undefined) return
const amount = formatUnits(tmpl.quantity, selectedToken.value?.decimals)
transfer.input.value = { to: tmpl.to, amount, token: selectedToken.value }
}

useSignalEffect(feedTemplateToTransferInput)

return <></>
}
112 changes: 112 additions & 0 deletions app/ts/components/TemplateRecorder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { Signal, useComputed, useSignal, useSignalEffect } from '@preact/signals'
import { toQuantity } from 'ethers'
import { JSX } from 'preact/jsx-runtime'
import { useTemplates } from '../context/TransferTemplates.js'
import { extractERC20TransferRequest } from '../library/ethereum.js'
import { serialize, TransferRequest, TransferTemplate } from '../schema.js'
import { useTransaction } from './TransactionProvider.js'

export const TemplateRecorder = () => {
const { response, receipt } = useTransaction()
const { add } = useTemplates()
const isSaved = useSignal(false)
const templateDraft = useSignal<TransferTemplate | undefined>(undefined)

const erc20TransferTemplate = useComputed(() => {
if (receipt.value.state !== 'resolved' || receipt.value.value === null) return
const erc20TransferRequest = extractERC20TransferRequest(receipt.value.value)
if (erc20TransferRequest === undefined) return
const parsed = TransferRequest.safeParse(erc20TransferRequest)
const label = templateDraft.peek()?.label
return parsed.success ? { ...parsed.value, label } : undefined
})

const ethTransferTemplate = useComputed(() => {
if (response.value.state !== 'resolved' || response.value.value === null) return
const { to, from, value } = response.value.value
const parsed = TransferRequest.safeParse({ to, from, quantity: toQuantity(value) })
return parsed.success ? { label: templateDraft.peek()?.label, ...parsed.value } : undefined
})

useSignalEffect(() => {
// Update draft with values coming from transaction
templateDraft.value = erc20TransferTemplate.value || ethTransferTemplate.value
})

const saveTemplate = () => {
if (!templateDraft.value) return
const serialized = serialize(TransferTemplate, templateDraft.value)
const template = TransferTemplate.parse(serialized)
add(template)
isSaved.value = true
}

// Activate form only after the transaction receipt is resolved
if (receipt.value.state !== 'resolved') return <></>

if (isSaved.value === true) return <TemplateAddConfirmation />

return <AddTemplateForm formData={templateDraft} onSubmit={saveTemplate} />
}

type AddTemplateFormProps = {
onSubmit: () => void
formData: Signal<TransferTemplate | undefined>
}

const AddTemplateForm = ({ formData, onSubmit }: AddTemplateFormProps) => {
const submitForm = (e: Event) => {
e.preventDefault()
onSubmit()
}

const updateLabel = (event: JSX.TargetedEvent<HTMLInputElement>) => {
event.preventDefault()
const templateData = formData.peek()
if (!templateData) return
formData.value = { ...templateData, label: event.currentTarget.value }
}

return (
<div class='my-4'>
<div class='font-bold text-2xl mb-2'>Save Transfer</div>
<div class='border border-dashed border-white/30 p-4'>
<div class='flex flex-col md:flex-row-reverse items-center gap-4'>
<div class='shrink w-full'>
<p class='text-white/50 text-sm'>Prevent accidental inputs by saving this transfer so you can quickly do this again later. Add a label to this transfer and hit save to continue.</p>
</div>
<form class='w-full' onSubmit={submitForm}>
<div class='grid gap-2 items-center w-full'>
<input class='border border-white/30 px-4 py-2 bg-transparent outline-none min-w-auto' type='text' value={formData.value?.label} onInput={updateLabel} placeholder='Add a label (optional)' />
<button type='submit' class='border border-white/50 bg-white/10 px-4 py-3 outline-none focus:bg-white/20 hover:bg-white/20'>
Save
</button>
</div>
</form>
</div>
</div>
</div>
)
}

const TemplateAddConfirmation = () => {
return (
<div class='my-4'>
<div class='border border-dashed border-lime-400/40 bg-lime-400/5 p-4'>
<div class='flex items-center justify-left gap-2'>
<svg width='60' height='60' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'>
<g fill='none' fillRule='evenodd'>
<path d='M0 0h24v24H0z' />
<circle stroke='currentColor' strokeWidth='2' strokeLinecap='round' cx='12' cy='12' r='9' />
<path d='m8.5 12.5 1.651 2.064a.5.5 0 0 0 .744.041L15.5 10' stroke='currentColor' strokeWidth='2' strokeLinecap='round' />
</g>
</svg>
<div>
<div class='font-bold text-lg'>Transfer Saved!</div>
<div class='text-white/50 leading-tight'>This transfer was added to the sidebar so you can use it as a starting point for your next transfer.</div>
</div>
</div>
</div>
</div>
)
}
55 changes: 42 additions & 13 deletions app/ts/components/Favorites.tsx → app/ts/components/Templates.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import { useSignal } from '@preact/signals'
import { useComputed, useSignal } from '@preact/signals'
import { formatEther, formatUnits } from 'ethers'
import { useTokenManager } from '../context/TokenManager.js'
import { useTemplates } from '../context/TransferTemplates.js'
import { removeNonStringsAndTrim } from '../library/utilities.js'
import { FavoriteModel, useFavorites } from '../store/favorites.js'
import { TransferTemplate } from '../schema.js'
import * as Icon from './Icon/index.js'

export const Favorites = () => {
export const Templates = () => {
const manage = useSignal(false)
const { favorites } = useFavorites()
const { cache: templatesCache } = useTemplates()
const { cache: tokensCache } = useTokenManager()

if (favorites.value.length < 1)
const templates = useComputed(() => templatesCache.value.data)
const getCachedToken = (contractAddress: string) => tokensCache.value.data.find(token => token.address === contractAddress)

if (templates.value.length < 1)
return (
<div class='pl-4 mb-4'>
<div class='flex justify-between'>
Expand All @@ -28,16 +35,19 @@ export const Favorites = () => {
</button>
</div>
<div class='grid gap-2'>
{favorites.value.map((favorite, index) => {
{templates.value.map((template, index) => {
const token = template.contractAddress && getCachedToken(template.contractAddress)
const amount = token ? formatUnits(template.quantity, token.decimals) : formatEther(template.quantity)

return (
<a class={removeNonStringsAndTrim('grid gap-2 items-center bg-white/10 px-4 py-3', manage.value ? 'grid-cols-[min-content,minmax(0,1fr),min-content]' : 'grid-cols-1 hover:bg-white/30')} href={`#saved/${index}`}>
<MoveUpButton show={manage.value === true} favorite={favorite} index={index} />
<MoveUpButton show={manage.value === true} template={template} index={index} />
<div class='grid gap-2 grid-cols-[auto,minmax(0,1fr)] items-center'>
{favorite.token ? <img class='w-8 h-8' src={`./img/${favorite.token.address.toLowerCase()}.svg`} /> : <img class='w-8 h-8' src={`./img/ethereum.svg`} />}
{token ? <img class='w-8 h-8' src={`/img/${token.address.toLowerCase()}.svg`} /> : <img class='w-8 h-8' src={`/img/ethereum.svg`} />}
<div class='text-left'>
<div>{favorite.label}</div>
<div>{template.label}</div>
<div class='overflow-hidden text-ellipsis whitespace-nowrap text-sm text-white/50'>
{favorite.amount} {favorite.token ? favorite.token.symbol : 'ETH'} to {favorite.recipientAddress}
{amount} {token ? token.symbol : 'ETH'} to {template.to}
</div>
</div>
</div>
Expand All @@ -52,12 +62,26 @@ export const Favorites = () => {

type PromoteButtonProps = {
show: boolean
favorite: FavoriteModel
template: TransferTemplate
index: number
}

const MoveUpButton = (props: PromoteButtonProps) => {
const { swapIndex } = useFavorites()
const { cache } = useTemplates()
const templates = useComputed(() => cache.value.data)

const swapIndex = (indexA: number, indexB: number) => {
// ignore same indices swap
if (indexA === indexB) return

const orderedTemplates = [...templates.value]

const tempA = orderedTemplates[indexA]
orderedTemplates[indexA] = orderedTemplates[indexB]
orderedTemplates[indexB] = tempA

cache.value = { ...cache.peek(), data: orderedTemplates }
}

if (!props.show) return <></>
if (props.index === 0) return <div></div>
Expand All @@ -75,7 +99,12 @@ type RemoveButtonProps = {
}

const RemoveButton = (props: RemoveButtonProps) => {
const { remove } = useFavorites()
const { cache } = useTemplates()

const remove = (index: number) => {
const newData = [...cache.value.data.slice(0, index), ...cache.value.data.slice(index + 1)]
cache.value = { ...cache.peek(), data: newData }
}

if (!props.show) return <></>

Expand Down
Loading

0 comments on commit a073352

Please sign in to comment.