Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transfer receiver #4092

Merged
merged 9 commits into from
Mar 21, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 50 additions & 41 deletions app2/src/lib/components/Transfer/Amount.svelte
Original file line number Diff line number Diff line change
@@ -3,8 +3,15 @@ import Input from "$lib/components/ui/Input.svelte"
import { transfer } from "$lib/components/Transfer/transfer.svelte.ts"
import { Option } from "effect"
import { formatUnits } from "viem"
import Skeleton from "$lib/components/ui/Skeleton.svelte"
import { wallets } from "$lib/stores/wallets.svelte.ts"
import Skeleton from "$lib/components/ui/Skeleton.svelte"
import Label from "$lib/components/ui/Label.svelte"
type Props = {
type: "source" | "destination"
disabled?: boolean
}
let { type, disabled = false }: Props = $props()
let chainWallet = $derived.by(() => {
if (Option.isSome(transfer.sourceChain)) {
@@ -60,23 +67,46 @@ function setMaxAmount() {
}
</script>

<!-- AMOUNT INPUT -->
<Input
id="amount"
label="amount"
type="text"
required
disabled={!transfer.raw.asset}
autocorrect="off"
placeholder="0.00"
spellcheck="false"
autocomplete="off"
inputmode="decimal"
data-field="amount"
autocapitalize="none"
pattern="^[0-9]*[.]?[0-9]*$"
value={transfer.raw.amount}
oninput={(event) => {

<div class="w-full">
{#if Option.isSome(chainWallet)}
{#if type === "source"}
<div class="flex w-full justify-between items-center text-xs gap-1">
<div class="flex gap-1">
<Label>BALANCE:</Label>
{#if !transfer.raw.source || !transfer.raw.asset}
0
{:else if !allDataReadyForBalance()}
<Skeleton class="h-3 w-16 inline-block"/>
{:else}
{displayBalance}
{/if}
</div>
<button
class="cursor-pointer text-xs text-sky-400 hover:text-sky-200"
onclick={setMaxAmount}
>
MAX
</button>
</div>
{/if}
{/if}
<Input
id="amount"
type="text"
required
disabled={!transfer.raw.asset || disabled}
autocorrect="off"
placeholder="0.00"
spellcheck="false"
autocomplete="off"
inputmode="decimal"
data-field="amount"
autocapitalize="none"
pattern="^[0-9]*[.]?[0-9]*$"
value={transfer.raw.amount}
class="h-12 text-center"
oninput={(event) => {
const input = event.currentTarget
const value = input.value

@@ -96,26 +126,5 @@ function setMaxAmount() {
input.value = transfer.raw.amount
}
}}
class="text-center"
/>

<div class="flex w-full justify-between text-xs">
<p>
BALANCE:
{#if Option.isSome(chainWallet)}
{#if !transfer.raw.source || !transfer.raw.asset}
0
{:else if !allDataReadyForBalance()}
<Skeleton class="h-3 w-16 inline-block"/>
{:else}
{displayBalance}
{/if}
{/if}
</p>
<button
class="cursor-pointer hover:underline"
onclick={setMaxAmount}
>
USE MAX
</button>
</div>
/>
</div>
82 changes: 42 additions & 40 deletions app2/src/lib/components/Transfer/ChainAsset/ChainAssetButton.svelte
Original file line number Diff line number Diff line change
@@ -4,6 +4,8 @@ import { Option } from "effect"
import { transfer } from "$lib/components/Transfer/transfer.svelte.js"
import Label from "$lib/components/ui/Label.svelte"
import { chainLogoMap } from "$lib/constants/chain-logos.ts"
import SharpChevronDownIcon from "$lib/components/icons/SharpChevronDownIcon.svelte"
import LoadingSpinnerIcon from "$lib/components/icons/LoadingSpinnerIcon.svelte"
type Props = {
type: "source" | "destination"
@@ -27,76 +29,76 @@ const isChainLoading = $derived.by(() => {
})
</script>

<div>
<Label>{type}</Label>
<div class="w-full">
<Label>{type === "source" ? "From" : "To"}</Label>
<button
onclick={onClick}
class={cn(
"w-full rounded-lg border border-zinc-600 bg-zinc-700 text-zinc-200",
"w-full h-12 rounded-md bg-zinc-800 text-zinc-200",
"hover:bg-zinc-600 hover:border-zinc-500",
"focus:outline-none focus:ring-2 focus:ring-sky-500",
"disabled:opacity-50 disabled:cursor-not-allowed",
"transition-all duration-200 cursor-pointer",
"p-0 overflow-hidden"
)}
>
<div class="flex items-center">
{#if isChainLoading}
<!-- Loading State -->
<div class="flex items-center justify-center p-2 flex-1">
<span>Loading...</span>
<div class="flex gap-2 items-center justify-between p-2 flex-1">
<div class="w-8 h-8 flex items-center bg-zinc-500 text-white rounded-full justify-center">
<LoadingSpinnerIcon/>
</div>
<span class="text-zinc-400">Loading...</span>
<div class="text-transparent">
<SharpChevronDownIcon/>
</div>
</div>
{:else if Option.isNone(selectedChain)}
<!-- No Chain Selected -->
<div class="flex items-center justify-between p-2 flex-1">
<span class="text-zinc-400">Select {type}</span>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="text-zinc-400">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
<div class="flex gap-2 items-center justify-between p-2 flex-1">
<div class="w-8 h-8 flex items-center bg-zinc-500 rounded-full justify-center">
?
</div>
<span class="text-zinc-400 flex-1 text-start">Select</span>
<SharpChevronDownIcon/>
</div>
{:else}

<!-- Chain Selected -->
<div class="flex items-center bg-zinc-800 p-2 border-r border-zinc-600">
<div class="w-8 h-8 flex items-center justify-center overflow-hidden">
<img src={chainLogoMap.get(selectedChain.value.universal_chain_id).color} alt="">
<div class="flex gap-2 items-center justify-between p-2 flex-1 w-full">

<!--LOGO-->
<div class="flex items-center">
<div class="w-8 h-8 flex items-center justify-center overflow-hidden">
<img src={chainLogoMap.get(selectedChain.value.universal_chain_id).color} alt="">
</div>
</div>
</div>

<!-- Asset part (for both source and destination) -->
<div class="flex-1 p-2 flex items-center justify-between">
{#if type === "source" && transfer.raw.asset && Option.isNone(transfer.baseToken)}
<!-- Asset Loading (only for source) -->
<span class="flex items-center">
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span>Loading...</span>
</span>
{:else if Option.isSome(transfer.baseToken)}
<!-- Selected Asset (both source and destination) -->
<div class="flex items-center">
<!-- Show the asset, grayed out for destination type -->
<span class={type === "destination" ? "truncate text-zinc-400" : "truncate"}>
{transfer.baseToken.value.representations[0]?.name ?? transfer.baseToken.value.denom}
</span>
<!-- Show the asset, grayed out for destination type -->
<div class={cn(
type === "destination" ? "truncate" : "truncate",
"flex flex-col items-start w-full"
)}>
<p class="text-sm leading-4">
{transfer.baseToken.value.representations[0]?.symbol ?? transfer.baseToken.value.denom}
</p>
{#if Option.isSome(transfer.sourceChain)}
<p class="text-xs text-zinc-400">{transfer.sourceChain.value.display_name}</p>
{/if}

</div>
{:else if type === "source"}
<!-- No Asset Selected (only for source) -->
<span class="text-zinc-400">Select asset</span>
<span class="text-zinc-400 flex-1 text-start">Select</span>
{:else}
<!-- No Asset Selected (for destination) -->
<span class="text-zinc-400">No asset selected</span>
<span class="text-zinc-400 flex-1 text-start">No asset</span>
{/if}
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="text-zinc-400">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
<SharpChevronDownIcon class="size-6"/>
</div>
{/if}
</div>
Original file line number Diff line number Diff line change
@@ -29,12 +29,11 @@ function selectChain(chain: Chain) {
{#each chains.data.value as chain}
<button
class={cn(
"flex items-center gap-2 justify-start px-2 py-1 rounded-md transition-colors",
"border border-zinc-700 hover:bg-zinc-700",
"flex items-center gap-2 justify-start px-2 py-1 rounded-md transition-colors cursor-pointer",
(type === "source" && transfer.raw.source === chain.chain_id) ||
(type === "destination" && transfer.raw.destination === chain.chain_id)
? "bg-zinc-700 ring-2 ring-sky-500"
: "bg-zinc-800/50"
? "bg-zinc-700 hover:bg-zinc-600 ring-1 ring-sky-500"
: "bg-zinc-800 hover:bg-zinc-600"
)}
onclick={() => selectChain(chain)}
>
25 changes: 19 additions & 6 deletions app2/src/lib/components/Transfer/ChainAsset/index.svelte
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
<script lang="ts">
import { fade, fly } from "svelte/transition"
import { onMount, onDestroy } from "svelte"
import ChainAssetButton from "$lib/components/Transfer/ChainAsset/ChainAssetButton.svelte"
import ChainSelector from "$lib/components/Transfer/ChainAsset/ChainSelector.svelte"
import TransferDirectionInfo from "$lib/components/Transfer/ChainAsset/TransferDirectionInfo.svelte"
import AssetSelector from "$lib/components/Transfer/ChainAsset/AssetSelector.svelte"
import SharpCancelIcon from "$lib/components/icons/SharpCancelIcon.svelte"
type Props = {
type: "source" | "destination"
@@ -25,6 +27,20 @@ function onChainSelected() {
function onAssetSelected() {
closeModal()
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === "Escape" && open) {
closeModal()
}
}
onMount(() => {
document.addEventListener("keydown", handleKeydown)
})
onDestroy(() => {
document.removeEventListener("keydown", handleKeydown)
})
</script>

{#if open}
@@ -34,14 +50,11 @@ function onAssetSelected() {
<div class="p-4 border-b border-zinc-700 flex justify-between items-center h-12">
<h3 class="text-sm font-medium text-zinc-400">Select {type.charAt(0).toUpperCase() + type.slice(1)}</h3>
<button
class="text-zinc-400 hover:text-white"
aria-label="Close"
class="text-zinc-400 hover:text-white cursor-pointer"
onclick={closeModal}
>
<span class="sr-only">close</span>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
<SharpCancelIcon />
</button>
</div>

500 changes: 461 additions & 39 deletions app2/src/lib/components/Transfer/Receiver.svelte

Large diffs are not rendered by default.

66 changes: 42 additions & 24 deletions app2/src/lib/components/Transfer/index.svelte
Original file line number Diff line number Diff line change
@@ -21,10 +21,8 @@ import ChainAsset from "$lib/components/Transfer/ChainAsset/index.svelte"
import type { TransferStateUnion } from "$lib/components/Transfer/validation.ts"
import { Effect, Option } from "effect"
import { wallets } from "$lib/stores/wallets.svelte"
import TransferAsset from "./ChainAsset/TransferAsset.svelte"
import { WETH_DENOMS } from "$lib/constants/weth-denoms.ts"
import type { Instruction } from "@unionlabs/sdk/ucs03"
import { runPromiseExit } from "effect/Runtime"
import { createEvmToCosmosFungibleAssetOrder, Batch } from "@unionlabs/sdk/ucs03"
import {
createViemPublicClient,
@@ -41,7 +39,8 @@ import {
} from "@unionlabs/sdk/cosmos"
import { sepolia } from "viem/chains"
import { http } from "viem"
import { tapBoth } from "effect/STM"
import AngleArrowIcon from "$lib/components/icons/AngleArrowIcon.svelte"
import { truncate } from "$lib/utils/format.ts"
function getStatus(
state: TransferStateUnion
@@ -281,22 +280,42 @@ const checkAllowances = (ti: typeof transferIntents) =>
return Option.some(allowanceChecks)
})
let showDetails = $state(false)
</script>

<Card class="max-w-md relative flex flex-col gap-2">
<ChainAsset type="source"/>
<ChainAsset type="destination"/>
<Amount/>
<Receiver/>
<ShowData/>
<Button
class="mt-2"
variant="primary"
onclick={transfer.submit}
disabled={!isButtonEnabled || transfer.validation._tag !== "Success"}
>
{buttonText}
</Button>
<Card class="max-w-sm relative flex flex-col justify-between min-h-[400px]">
<div class=" flex flex-col gap-4">
<ChainAsset type="source"/>
<ChainAsset type="destination"/>
<Amount type="source"/>
</div>

<div class="flex flex-col items-end">
<div class="flex items-center mr-5 text-zinc-400">
{#if transfer.args.receiver}
<p class="text-xs mb-2">{truncate(transfer.raw.receiver, 5, "middle")}</p>
{:else}
<p class="text-xs mb-2"> No receiver</p>
{/if}
<AngleArrowIcon class="rotate-270"/>
</div>
<div class="w-full items-end flex gap-2">
<Button
class="flex-1"
variant="primary"
onclick={transfer.submit}
disabled={!isButtonEnabled || transfer.validation._tag !== "Success"}
>
{buttonText}
</Button>
<Receiver/>

</div>
</div>
{#if showDetails}
<ShowData/>
{/if}
</Card>


@@ -317,7 +336,7 @@ const checkAllowances = (ti: typeof transferIntents) =>
</div>
{:else if step._tag === "SubmitInstruction"}
<div>Submit transfer instruction</div>
<pre>{JSON.stringify(instruction,null,2)}</pre>
<pre>{JSON.stringify(instruction, null, 2)}</pre>
{/if}
</li>
{/each}
@@ -326,21 +345,20 @@ const checkAllowances = (ti: typeof transferIntents) =>
{/if}



<h2>transfer intents</h2>
<pre>{JSON.stringify(transferIntents,null,2)}</pre>
<pre>{JSON.stringify(transferIntents, null, 2)}</pre>

<h2>instruction</h2>
<pre>{JSON.stringify(instruction,null,2)}</pre>
<pre>{JSON.stringify(instruction, null, 2)}</pre>

<h2>allowances</h2>
<pre>{JSON.stringify(allowances,null,2)}</pre>
<pre>{JSON.stringify(allowances, null, 2)}</pre>

<h2>required approvals</h2>
<pre>{JSON.stringify(requiredApprovals,null,2)}</pre>
<pre>{JSON.stringify(requiredApprovals, null, 2)}</pre>

<h2>transfer steps</h2>
<pre>{JSON.stringify(transferSteps,null,2)}</pre>
<pre>{JSON.stringify(transferSteps, null, 2)}</pre>

{#if transfer.state._tag !== "Empty"}
{#if getStatus(transfer.state) === "filling"}
1 change: 1 addition & 0 deletions app2/src/lib/components/icons/AngleArrowIcon.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...$$props}><!-- Icon from Google Material Icons by Material Design Authors - https://github.com/material-icons/material-icons/blob/master/LICENSE --><path fill="currentColor" d="m11 9l1.42 1.42L8.83 14H18V4h2v12H8.83l3.59 3.58L11 21l-6-6z"/></svg>
1 change: 1 addition & 0 deletions app2/src/lib/components/icons/FilledBookmarkIcon.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...$$props}><!-- Icon from Google Material Icons by Material Design Authors - https://github.com/material-icons/material-icons/blob/master/LICENSE --><path fill="currentColor" d="M17 3H7c-1.1 0-1.99.9-1.99 2L5 21l7-3l7 3V5c0-1.1-.9-2-2-2"/></svg>
1 change: 1 addition & 0 deletions app2/src/lib/components/icons/LoadingSpinnerIcon.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...$$props}><!-- Icon from All by undefined - undefined --><path fill="none" stroke="currentColor" stroke-dasharray="16" stroke-dashoffset="16" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3c4.97 0 9 4.03 9 9"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.2s" values="16;0"/><animateTransform attributeName="transform" dur="1.5s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/></path></svg>
1 change: 1 addition & 0 deletions app2/src/lib/components/icons/OutlinedBookmarkIcon.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...$$props}><!-- Icon from Google Material Icons by Material Design Authors - https://github.com/material-icons/material-icons/blob/master/LICENSE --><path fill="currentColor" d="M17 3H7c-1.1 0-1.99.9-1.99 2L5 21l7-3l7 3V5c0-1.1-.9-2-2-2m0 15l-5-2.18L7 18V5h10z"/></svg>
1 change: 1 addition & 0 deletions app2/src/lib/components/icons/RestoreIcon.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...$$props}><!-- Icon from Google Material Icons by Material Design Authors - https://github.com/material-icons/material-icons/blob/master/LICENSE --><path fill="currentColor" d="M13 3a9 9 0 0 0-9 9H1l3.89 3.89l.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7s-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42A8.95 8.95 0 0 0 13 21a9 9 0 0 0 0-18m-1 5v5l4.28 2.54l.72-1.21l-3.5-2.08V8z"/></svg>
1 change: 1 addition & 0 deletions app2/src/lib/components/icons/SharpCancelIcon.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...$$props}><!-- Icon from Google Material Icons by Material Design Authors - https://github.com/material-icons/material-icons/blob/master/LICENSE --><path fill="currentColor" d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10s10-4.47 10-10S17.53 2 12 2m5 13.59L15.59 17L12 13.41L8.41 17L7 15.59L10.59 12L7 8.41L8.41 7L12 10.59L15.59 7L17 8.41L13.41 12z"/></svg>
1 change: 1 addition & 0 deletions app2/src/lib/components/icons/SharpChevronDownIcon.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...$$props}><!-- Icon from Google Material Icons by Material Design Authors - https://github.com/material-icons/material-icons/blob/master/LICENSE --><path fill="currentColor" d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6l-6-6z"/></svg>
1 change: 1 addition & 0 deletions app2/src/lib/components/icons/SharpChevronLeftIcon.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...$$props}><!-- Icon from Google Material Icons by Material Design Authors - https://github.com/material-icons/material-icons/blob/master/LICENSE --><path fill="currentColor" d="M15.41 7.41L14 6l-6 6l6 6l1.41-1.41L10.83 12z"/></svg>
1 change: 1 addition & 0 deletions app2/src/lib/components/icons/SharpListRemoveIcon.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...$$props}><!-- Icon from Google Material Icons by Material Design Authors - https://github.com/material-icons/material-icons/blob/master/LICENSE --><path fill="currentColor" d="M14 10H3v2h11zm0-4H3v2h11zM3 16h7v-2H3zm11.41 6L17 19.41L19.59 22L21 20.59L18.41 18L21 15.41L19.59 14L17 16.59L14.41 14L13 15.41L15.59 18L13 20.59z"/></svg>
2 changes: 1 addition & 1 deletion app2/src/lib/components/ui/Input.svelte
Original file line number Diff line number Diff line change
@@ -27,7 +27,7 @@ let {
}: Props = $props()
const classes = cn(
"w-full p-2 bg-zinc-700 border border-zinc-600 rounded text-zinc-200",
"w-full p-2 bg-zinc-800 rounded-md text-zinc-200",
"focus:outline-none focus:ring-2 focus:ring-sky-500",
"disabled:opacity-50 disabled:cursor-not-allowed",
"transition-colors duration-200",
31 changes: 31 additions & 0 deletions app2/src/lib/utils/format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Truncates a string based on the given parameters
* @param {string} str - The string to truncate
* @param {number} showChars - Number of characters to show on each end when truncated
* @param {string} position - Where to place the ellipsis: 'start', 'middle', or leave empty for end
* @returns {string} - The truncated string
*/
export function truncate(str: string, showChars: number, position: string): string {
// If string is shorter than or equal to the total characters to show, return it as is
if (str.length <= showChars * 2 || showChars <= 0) {
return str
}

const ellipsis = "..."

switch (position.toLowerCase()) {
case "start":
// Show ellipsis at the start
return ellipsis + str.slice(str.length - showChars)

case "middle": {
// Show ellipsis in the middle
const firstPart = str.slice(0, showChars)
const lastPart = str.slice(str.length - showChars)
return firstPart + ellipsis + lastPart
}
default:
// Show ellipsis at the end (default)
return str.slice(0, showChars) + ellipsis
}
}