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

feat(app2): improve balance polling and supervision #4164

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
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
122 changes: 84 additions & 38 deletions app2/src/lib/components/Transfer/ChainAsset/AssetSelector.svelte
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
<script lang="ts">
import { Option } from "effect"
import {
Chunk,
Effect,
Fiber,
Record as R,
FiberStatus,
Option,
pipe,
Schedule,
Stream
} from "effect"
import { transfer } from "$lib/components/Transfer/transfer.svelte.ts"
import { wallets } from "$lib/stores/wallets.svelte.ts"
import Input from "$lib/components/ui/Input.svelte"
import Skeleton from "$lib/components/ui/Skeleton.svelte"
import TransferAsset from "$lib/components/Transfer/ChainAsset/TransferAsset.svelte"
import { Token } from "@unionlabs/sdk/schema"
import { balancesStore, denomFromChainKey, type BalanceKey } from "$lib/stores/balances.svelte"
import type { Tags } from "effect/Types"
import { SvelteMap } from "svelte/reactivity"

type Props = {
onSelect: () => void
Expand All @@ -21,6 +34,31 @@ const isWalletConnected = $derived.by(() => {
return Option.isSome(addressOption)
})

let statuses = $state(new SvelteMap<BalanceKey, Tags<FiberStatus.FiberStatus>>())

const statusMap = $derived(pipe(statuses, R.fromEntries, R.mapKeys(denomFromChainKey)))

$effect(() => {
const watcher = balancesStore.fiberMapStatuses$.pipe(
Stream.runCollect,
Effect.flatMap(chunk =>
Effect.sync(() => {
if (Chunk.isNonEmpty(chunk)) {
statuses = new SvelteMap(chunk)
}
})
),
Effect.repeat(Schedule.spaced("1 second")),
Effect.runFork
)

return () => {
console.log("[AssetSelector] cleaning up...")
Effect.runPromise(Fiber.interrupt(watcher))
// balancesStore.stopFetching();
}
})

const filteredTokens = $derived.by(() => {
// If we don't have base tokens yet, return empty array
if (Option.isNone(transfer.baseTokens)) return [] as Array<Token>
Expand Down Expand Up @@ -69,48 +107,56 @@ function selectAsset(token: Token) {
<div class="p-4 border-y border-zinc-700">
<!-- Search Bar -->
<Input
type="text"
placeholder="Search assets..."
disabled={!Option.isSome(transfer.sourceChain)}
value={searchQuery}
oninput={(e) => (searchQuery = (e.currentTarget as HTMLInputElement).value)}
type="text"
placeholder="Search assets..."
disabled={!Option.isSome(transfer.sourceChain)}
value={searchQuery}
oninput={(e) => (searchQuery = (e.currentTarget as HTMLInputElement).value)}
/>
</div>

<div class="overflow-y-scroll mb-12">
<div class="w-full h-full">
{#if Option.isNone(transfer.sourceChain)}
<div class="flex items-center justify-center text-zinc-500 p-8">
Please select a source chain first
</div>
{:else if Option.isNone(transfer.baseTokens)}
<div>
{#each Array(5) as _, i}
<div class="flex items-center w-full px-4 py-2 border-b border-zinc-700">
<div class="flex-1 min-w-0">
<div class="mb-1">
<Skeleton class="h-4 w-24" randomWidth={true}/>
</div>
<Skeleton class="h-3 w-32" randomWidth={true}/>
</div>
<div class="ml-2">
<Skeleton class="h-4 w-4"/>
{#if Option.isNone(transfer.sourceChain)}
<div class="flex items-center justify-center text-zinc-500 p-8">
Please select a source chain first
</div>
{:else if Option.isNone(transfer.baseTokens)}
<div>
{#each Array(5) as _, i}
<div
class="flex items-center w-full px-4 py-2 border-b border-zinc-700"
>
<div class="flex-1 min-w-0">
<div class="mb-1">
<Skeleton class="h-4 w-24" randomWidth={true} />
</div>
<Skeleton class="h-3 w-32" randomWidth={true} />
</div>
{/each}
</div>
{:else if filteredTokens.length === 0}
<div class="flex items-center justify-center text-zinc-500 p-8">
{searchQuery ? `No assets found matching "${searchQuery}"` : "No tokens found for this chain"}
</div>
{:else}
<div>
{#each filteredTokens as token}
{#key token.denom}
<TransferAsset {token} {selectAsset} />
{/key}
{/each}
</div>
{/if}
<div class="ml-2">
<Skeleton class="h-4 w-4" />
</div>
</div>
{/each}
</div>
{:else if filteredTokens.length === 0}
<div class="flex items-center justify-center text-zinc-500 p-8">
{searchQuery
? `No assets found matching "${searchQuery}"`
: "No tokens found for this chain"}
</div>
{:else}
<div>
{#each filteredTokens as token}
{#key token.denom}
<TransferAsset
{token}
{selectAsset}
status={statusMap[token.denom] ?? "NA"}
/>
{/key}
{/each}
</div>
{/if}
</div>
</div>
</div>
89 changes: 53 additions & 36 deletions app2/src/lib/components/Transfer/ChainAsset/TransferAsset.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,20 @@
import { cn } from "$lib/utils/index.js"
import { transfer } from "$lib/components/Transfer/transfer.svelte.js"
import type { Chain, Token } from "@unionlabs/sdk/schema"
import { Option } from "effect"
import { type FiberStatus, Option } from "effect"
import Skeleton from "$lib/components/ui/Skeleton.svelte"
import { formatUnits } from "viem"
import { chains } from "$lib/stores/chains.svelte.ts"
import SharpArrowLeft from "$lib/components/icons/SharpArrowLeft.svelte"
import type { Tags } from "effect/Types"

type Props = {
token: Token
selectAsset: (token: Token) => void
status: Tags<FiberStatus.FiberStatus> | "NA"
}

let { token, selectAsset }: Props = $props()
let { token, selectAsset, status }: Props = $props()

let isSelected = $derived(transfer.raw.asset === token.denom)

Expand Down Expand Up @@ -45,40 +47,55 @@ export const toDisplayName = (
</script>

<button
class={cn(
"flex flex-col items-start w-full overflow-x-scroll px-4 py-1 text-left hover:bg-zinc-700 transition-colors border-b border-zinc-700 cursor-pointer",
isSelected ? "bg-zinc-700 text-white" : "text-zinc-300"
)}
onclick={() => {
console.log(token)
selectAsset(token)
}}
class={cn(
"flex flex-col items-start w-full overflow-x-scroll px-4 py-1 text-left hover:bg-zinc-700 transition-colors border-b border-zinc-700 cursor-pointer",
isSelected ? "bg-zinc-700 text-white" : "text-zinc-300",
)}
onclick={() => {
console.log(token);
selectAsset(token);
}}
>
<div class="flex items-center flex gap-1 items-center overflow-x-scroll text-xs text-zinc-200">
<div class="mr-1">
{#if isLoading}
<Skeleton class="h-3 w-16"/>
{:else if Option.isSome(tokenBalance) && Option.isSome(tokenBalance.value.error)}
<span class="text-red-400">Error</span>
{:else}
{displayAmount}
{/if}
</div>
<div class="font-medium">
{token.representations[0]?.symbol ?? token.denom}
</div>
</div>
<div class="text-zinc-400 text-nowrap text-xs flex items-center gap-1">
{#if Option.isSome(chains.data)}
{#each token.wrapping as wrapping, i}
{#if i !== 0}
<SharpArrowLeft class="text-sky-300"/>
{/if}
{toDisplayName(
wrapping.unwrapped_chain.universal_chain_id,
chains.data.value,
)}
{/each}
<div class="flex gap-1 items-center overflow-x-scroll text-xs text-zinc-200">
<div class="mr-1">
{#if isLoading}
<Skeleton class="h-3 w-16" />
{:else if Option.isSome(tokenBalance) && Option.isSome(tokenBalance.value.error)}
<span class="text-red-400">Error</span>
{:else}
{displayAmount}
{/if}
</div>
</button>
<div class="font-medium">
{token.representations[0]?.symbol ?? token.denom}
</div>
</div>
<div class="text-zinc-400 text-nowrap text-xs flex items-center gap-1">
{#if Option.isSome(chains.data)}
{#each token.wrapping as wrapping, i}
{#if i !== 0}
<SharpArrowLeft class="text-sky-300" />
{/if}
{toDisplayName(
wrapping.unwrapped_chain.universal_chain_id,
chains.data.value,
)}
{/each}
{/if}
</div>
<div class="text-zinc-400 text-nowrap text-xs flex items-center gap-1">
<!-- FIXME: Styling here only for demonstration; consult team -->
{#if status === "Done"}
<span class="text-green-400">{status}</span>
{/if}
{#if status === "Running"}
<span class="text-blue-400">{status}</span>
{/if}
{#if status === "Suspended"}
<span class="text-orange-400">{status}</span>
{/if}
{#if status === "NA"}
<span class="text-red-400">DNE</span>
{/if}
</div>
</button>
8 changes: 4 additions & 4 deletions app2/src/lib/services/cosmos/balances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,9 @@ export const fetchCosmosBalance = ({
const displayAddress = yield* chain.toCosmosDisplay(walletAddress)
const decodedDenom = yield* fromHexString(tokenAddress)

yield* Effect.log(
`fetching balance for ${chain.universal_chain_id}:${displayAddress}:${decodedDenom}`
)
// yield* Effect.log(
// `fetching balance for ${chain.universal_chain_id}:${displayAddress}:${decodedDenom}`
// )

const fetchBalance = decodedDenom.startsWith(`${chain.addr_prefix}1`)
? fetchCosmosCw20Balance({
Expand All @@ -109,7 +109,7 @@ export const fetchCosmosBalance = ({
denom: decodedDenom
})

let balance = yield* Effect.retry(fetchBalance, cosmosBalanceRetrySchedule)
const balance = yield* Effect.retry(fetchBalance, cosmosBalanceRetrySchedule)

return RawTokenBalance.make(Option.some(TokenRawAmount.make(balance)))
}).pipe(
Expand Down
6 changes: 3 additions & 3 deletions app2/src/lib/services/evm/balances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@ export const fetchEvmBalance = ({
const client = yield* getPublicClient(chain)
const decodedDenom = yield* fromHexString(tokenAddress)

yield* Effect.log(
`fetching balance for ${chain.universal_chain_id}:${walletAddress}:${tokenAddress}`
)
// yield* Effect.log(
// `fetching balance for ${chain.universal_chain_id}:${walletAddress}:${tokenAddress}`
// )

const fetchBalance =
decodedDenom === "native"
Expand Down
Loading