Skip to content

Commit

Permalink
refactor(app): transfer (#3366)
Browse files Browse the repository at this point in the history
  • Loading branch information
cor authored Jan 7, 2025
2 parents 1d1814f + 423c3e6 commit de0f54d
Show file tree
Hide file tree
Showing 42 changed files with 2,247 additions and 38 deletions.
83 changes: 83 additions & 0 deletions app/src/lib/components/TransferFrom/components/ChainDialog.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<script lang="ts">
import type { Readable } from "svelte/store"
import type { ContextStore } from "$lib/components/TransferFrom/transfer/context.ts"
interface Props {
context: Readable<ContextStore>
kind: "source" | "destination"
dialogOpen: boolean
onChainSelect: (type: "source" | "destination", chain: string) => void
onClose: () => void
}
export let context: Props["context"]
export let kind: Props["kind"]
export let dialogOpen: Props["dialogOpen"]
export let onChainSelect: Props["onChainSelect"]
export let onClose: Props["onClose"]
</script>

{#if dialogOpen && $context?.chains}
<dialog
open
aria-label={`Select ${kind} chain`}
class="absolute z-50 inset-0 overflow-y-scroll p-0 bg-transparent m-0 w-full h-full animate-fade-in backdrop-blur-md"
>
<button
type="button"
class="fixed inset-0 w-full h-full bg-gradient-to-t from-black to-black/10 animate-fade-in"
on:click|self={onClose}
aria-label="Close dialog"
/>

<div class="relative z-10">
<div class="flex justify-center">
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 md:gap-8 max-w-4xl py-8 px-4">
{#each $context.chains as chain, i}
<button
style="animation-delay: {i * 100}ms"
on:click={() => onChainSelect(kind, chain.chain_id)}
type="button"
class="h-72 flex items-end border p-4 bg-secondary group hover:bg-accent transition-colors animate-slide-up"
>
<span class="font-supermolot uppercase font-bold text-xl text-start text-secondary-foreground group-hover:text-secondary">
{chain.display_name}
</span>
</button>
{/each}
</div>
</div>
</div>
</dialog>
{/if}

<style>
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slide-up {
from {
transform: translateY(30px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
:global(.animate-fade-in) {
animation: fade-in 0.3s ease-out forwards;
}
:global(.animate-slide-up) {
animation: slide-up 0.4s ease-out forwards;
opacity: 0;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script>
export let visible = true
export let width = 0 // Pixel value
export let height = 0 // Pixel value
export let translateZ = 0
export let rotateY = "0deg"
</script>

<div
class="absolute bg-muted flex flex-col items-center border-2"
class:opacity-100={visible}
class:opacity-0={!visible}
class:pointer-events-none={!visible}
style={`width: ${width}px; height: ${height}px; transform: rotateY(${rotateY}) translateZ(${translateZ}px);`}
>
<slot />
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<script lang="ts">
import type { Readable } from "svelte/store"
import type { ContextStore } from "$lib/components/TransferFrom/transfer/context.ts"
import { truncate } from "$lib/utilities/format.ts"
import { formatUnits } from "viem"
import { Button } from "$lib/components/ui/button"
import type { CubeFaces } from "$lib/components/TransferFrom/components/Cube/types.ts"
import type { RawIntentsStore } from "$lib/components/TransferFrom/transfer/raw-intents.ts"
import type { IntentsStore } from "$lib/components/TransferFrom/transfer/intents.ts"
interface Props {
stores: {
rawIntents: RawIntentsStore
intents: Readable<IntentsStore>
context: Readable<ContextStore>
}
rotateTo: (face: CubeFaces) => void
}
export let stores: Props["stores"]
export let rotateTo: Props["rotateTo"]
let { rawIntents, context, intents } = stores
$: sortedAssets = [...($intents.sourceAssets ?? [])].sort((a, b) => {
if (a.isSupported !== b.isSupported) {
return a.isSupported ? -1 : 1
}
return Number(b.balance.balance - a.balance.balance)
})
function setAsset(address: string) {
rawIntents.updateField("asset", address)
rotateTo("intentFace")
}
</script>

<div class="flex flex-col h-full w-full">
<div class="text-primary p-2 px-4 flex items-center justify-between border-b-2">
<span class="font-bold uppercase">Assets</span>
<button
class="border-2 h-6 w-6 flex items-center justify-center"
on:click={() => rotateTo("intentFace")}
>✕
</button>
</div>

{#if sortedAssets.length}
<div class="flex-1 overflow-y-auto">
{#each sortedAssets as asset}
<div class="pb-2 flex flex-col justify-start">
<Button
variant="ghost"
class="px-4 py-2 w-full rounded-none flex justify-between items-center"
on:click={() => setAsset(asset.balance.address)}
>
<div class:opacity-30={!asset.isSupported}>
{truncate(asset.symbol, 6)}
</div>
<p class:opacity-30={!asset.isSupported}>
{formatUnits(asset.balance.balance, asset.supportedAsset?.decimals ?? 0)}
</p>
</Button>
</div>
{/each}
</div>
{:else}
<div class="px-4 p-2">
<p>No spendable balances</p>
</div>
{/if}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<!-- ChainSelector.svelte -->
<script lang="ts">
import type { Readable } from "svelte/store"
import type { ContextStore } from "$lib/components/TransferFrom/transfer/context.ts"
import type { RawIntentsStore } from "$lib/components/TransferFrom/transfer/raw-intents.ts"
import type { CubeFaces } from "$lib/components/TransferFrom/components/Cube/types.ts"
import { Button } from "$lib/components/ui/button"
import { TRANSFER_DEBUG } from "$lib/components/TransferFrom/transfer/config.ts"
interface Props {
stores: {
rawIntents: RawIntentsStore
context: Readable<ContextStore>
}
rotateTo: (face: CubeFaces) => void
selected: "source" | "destination"
}
export let stores: Props["stores"]
export let rotateTo: Props["rotateTo"]
export let selected: Props["selected"]
let expandedChainId: string | null = null
let { rawIntents, context } = stores
function setChain(selected: "source" | "destination", chainId: string) {
rawIntents.updateField(selected, chainId)
rotateTo("intentFace")
}
function toggleExpand(chainId: string) {
expandedChainId = expandedChainId === chainId ? null : chainId
}
</script>

<div class="flex flex-col h-full w-full">
<!-- Title Bar -->
<div class="text-primary p-2 flex items-center justify-between border-b-2">
<span class="font-bold uppercase">{selected} chain</span>
<button
class="border-2 h-6 w-6 flex items-center justify-center"
on:click={() => rotateTo("intentFace")}
>✕
</button>
</div>

<!-- Chain List -->
<div class="flex flex-col h-full overflow-y-scroll">
<div class="p-2 space-y-2 h-full">
{#each $context.chains as chain}
<div>
<Button
variant="ghost"
class="px-4 py-2 w-full rounded-none flex justify-between items-center"
on:click={() => setChain(selected, chain.chain_id)}
>
<div class="flex items-center gap-2">
<span>{chain.display_name}</span>
</div>
<button
class="border-2 border-black h-8 w-8 hover:bg-gray-200 active:border-gray-400"
on:click|stopPropagation={() => toggleExpand(chain.chain_id)}
>
<span>i</span>
</button>
</Button>

<!-- Expanded Info Panel -->
{#if expandedChainId === chain.chain_id}
<div class="">
<div class="grid grid-cols-2 gap-2 text-sm">
<div class="border-2 border-black p-2">
<h4 class="font-bold mb-1">Network Info</h4>
<p>Chain ID: {chain.chain_id}</p>
<p>Type: {chain.rpc_type}</p>
<p>Prefix: {chain.addr_prefix}</p>
</div>
{#if !TRANSFER_DEBUG}
<div class="border-2 border-black p-2">
<h4 class="font-bold mb-1">Status</h4>
<p>Enabled: {chain.enabled ? '' : ''}</p>
<p>Staging: {chain.enabled_staging ? '' : ''}</p>
</div>
{/if}
{#if chain.explorers?.length}
<div class="col-span-2 border-2 border-black p-2">
<h4 class="font-bold mb-1">Explorers</h4>
{#each chain.explorers as explorer}
<a href={explorer.tx_url} class="text-xs truncate">
{explorer.tx_url.split('/')[2]}
</a>
{/each}
</div>
{/if}
</div>
</div>
{/if}
</div>
{/each}
</div>
</div>

</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<script lang="ts">
import Direction from "$lib/components/TransferFrom/components/Direction.svelte"
import SelectedAsset from "$lib/components/TransferFrom/components/SelectedAsset.svelte"
import type { Readable } from "svelte/store"
import type { ValidationStoreAndMethods } from "$lib/components/TransferFrom/transfer/validation.ts"
import type { ContextStore } from "$lib/components/TransferFrom/transfer/context.ts"
import { Button } from "$lib/components/ui/button"
import type { IntentsStore } from "$lib/components/TransferFrom/transfer/intents.ts"
import type { CubeFaces } from "$lib/components/TransferFrom/components/Cube/types.ts"
import type { RawIntentsStore } from "$lib/components/TransferFrom/transfer/raw-intents.ts"
import { Input } from "$lib/components/ui/input"
interface Props {
stores: {
rawIntents: RawIntentsStore
intents: Readable<IntentsStore>
context: Readable<ContextStore>
validation: ValidationStoreAndMethods
}
rotateTo: (face: CubeFaces) => void
}
export let stores: Props["stores"]
export let rotateTo: Props["rotateTo"]
let { rawIntents, intents, validation, context } = stores
</script>

<div class="flex flex-col w-full h-full ">

<div class="text-primary p-2 flex items-center justify-between border-b-2">
<span class="font-bold uppercase">Transfer</span>
</div>
<div class="flex flex-col h-full w-full justify-between p-4">
<div class="flex flex-col gap-4">
<Direction {intents} {validation} getSourceChain={() => rotateTo("sourceFace")} getDestinationChain={() => rotateTo("destinationFace")}/>
<SelectedAsset {intents} onSelectAsset={() => rotateTo("assetsFace")}/>
<div class="flex flex-col gap-1">
<Input
id="amount"
type="number"
name="amount"
minlength={1}
maxlength={64}
required={true}
disabled={!$intents.selectedAsset.address}
autocorrect="off"
placeholder="0.00"
spellcheck="false"
autocomplete="off"
inputmode="decimal"
data-field="amount"
autocapitalize="none"
pattern="^[0-9]*[.,]?[0-9]*$"
class="p-1 {$validation.errors.amount ? 'border-red-500' : ''}"
value={$intents.amount}
on:input={event => rawIntents.updateField('amount', event)}
/>
{#if $validation.errors.amount}
<span class="text-red-500 text-sm">{$validation.errors.amount}</span>
{/if}
</div>

<div class="flex flex-col gap-1">
<Input
type="text"
id="receiver"
name="receiver"
required={true}
disabled={!$intents.destinationChain}
autocorrect="off"
spellcheck="false"
autocomplete="off"
data-field="receiver"
class="p-1 disabled:bg-black/30 {$validation.errors.receiver ? 'border-red-500' : ''}"
placeholder="Enter destination address"
value={$intents.receiver}
on:input={event => rawIntents.updateField('receiver', event)}
/>
{#if $validation.errors.receiver}
<span class="text-red-500 text-sm">{$validation.errors.receiver}</span>
{/if}
</div>
</div>
<Button
disabled={!$validation.isValid}
on:click={() => rotateTo("verifyFace")}>Transfer
</Button>
</div>
</div>
Loading

0 comments on commit de0f54d

Please sign in to comment.