Skip to content
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
2 changes: 2 additions & 0 deletions app2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
"@eslint/js": "^9.24.0",
"@keplr-wallet/types": "^0.12.220",
"@leapwallet/types": "^0.0.5",
"@mysten/sui": "^1.38.0",
"@mysten/wallet-standard": "^0.19.3",
"@number-flow/svelte": "^0.3.7",
"@safe-global/safe-apps-sdk": "^9.1.0",
"@safe-global/safe-gateway-typescript-sdk": "^3.23.1",
Expand Down
5 changes: 5 additions & 0 deletions app2/src/lib/components/ui/ConnectWalletButton.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ import Button from "./Button.svelte"
title="Cosmos"
>
</div>
<div
class="{Option.isSome(wallets.suiAddress) ? 'pulse-3 bg-green-500 shadow-[0_0_2px_1px_rgba(34,197,94,0.6)]' : 'bg-zinc-800'} w-2 h-2 rounded-full transition-colors duration-200"
title="Sui"
>
</div>
</div>
</Button>

Expand Down
2 changes: 2 additions & 0 deletions app2/src/lib/components/ui/Wallet/connect/connection.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import SharpPowerIcon from "$lib/components/icons/SharpPowerIcon.svelte"
import { type CosmosWalletId } from "$lib/wallet/cosmos"
import { type EvmWalletId } from "$lib/wallet/evm"
import { type SuiWalletId } from "$lib/wallet/sui"
import { RpcType } from "@unionlabs/sdk/schema"
import type { State } from "@wagmi/core"
import { Schema } from "effect"
Expand All @@ -27,6 +28,7 @@ interface Props {
connectedWalletId?:
| (T extends "cosmos" ? CosmosWalletId
: T extends "evm" ? EvmWalletId
: T extends "sui" ? SuiWalletId
: never)
| undefined
| null
Expand Down
2 changes: 2 additions & 0 deletions app2/src/lib/components/ui/Wallet/connect/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { CosmosWalletId } from "$lib/wallet/cosmos"
import type { EvmWalletId } from "$lib/wallet/evm"
import type { SuiWalletId } from "$lib/wallet/sui"
import type { RpcType } from "@unionlabs/sdk/schema"
import type { State } from "@wagmi/core"
import type { Schema } from "effect"
Expand All @@ -21,6 +22,7 @@ type Props<TChain extends Chain = Chain> = {
connectedWalletId?:
| (TChain extends "cosmos" ? CosmosWalletId
: TChain extends "evm" ? EvmWalletId
: TChain extends "sui" ? SuiWalletId
: never)
| undefined
onConnectClick: (walletIdentifier: string) => void | Promise<void>
Expand Down
90 changes: 67 additions & 23 deletions app2/src/lib/components/ui/Wallet/index.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@ import { dashboard } from "$lib/dashboard/stores/user.svelte"
import { uiStore } from "$lib/stores/ui.svelte"
import { cosmosStore, cosmosWalletsInformation } from "$lib/wallet/cosmos/index.js"
import { evmWalletsInformation, sepoliaStore } from "$lib/wallet/evm/index.js"
import { suiStore, suiWalletsInformation } from "$lib/wallet/sui"
import { Option } from "effect"
import Modal from "../Modal.svelte"

let currentWalletType = $state("all")

let evmConnected = $state(false)
let cosmosConnected = $state(false)
let suiConnected = $state(false)

$effect(() => {
evmConnected = sepoliaStore.connectionStatus === "connected"
cosmosConnected = cosmosStore.connectionStatus === "connected"
suiConnected = suiStore.connectionStatus === "connected"
})
</script>

Expand Down Expand Up @@ -43,10 +46,12 @@ $effect(() => {
style:left={currentWalletType === "all"
? "0"
: currentWalletType === "evm"
? "33.333%"
: "66.666%"}
style:width="33.333%"
style:height="100%"
? "25%"
: currentWalletType === "cosmos"
? "50%"
: "75%"}
style:width="25%"
style:height="75%"
>
</div>
<button
Expand Down Expand Up @@ -102,6 +107,23 @@ $effect(() => {
</span>
</div>
</button>
<button
onclick={() => currentWalletType = "sui"}
class="flex-1 px-4 py-2 text-sm font-medium rounded-md transition-colors relative cursor-pointer
{currentWalletType === 'sui'
? 'text-zinc-900 dark:text-white'
: 'text-zinc-500 hover:text-zinc-900 dark:hover:text-white'}"
>
<div class="flex items-center justify-center gap-2">
<span>Sui</span>
<span
class="w-1.5 h-1.5 rounded-full transition-all duration-300 ring-1 ring-opacity-20 {suiConnected
? 'bg-green-500 animate-pulse ring-green-500 shadow-[0_0_6px_0px_rgba(34,197,94,0.6)]'
: 'bg-white/10 dark:bg-white/5 backdrop-blur-sm ring-white/20'}"
>
</span>
</div>
</button>
</nav>
</section>

Expand All @@ -128,27 +150,49 @@ $effect(() => {
onDisconnectClick={cosmosStore.disconnect}
showDivider={false}
/>
{:else if currentWalletType === "all"}
{:else if currentWalletType === "sui"}
<Connection
chain="evm"
address={sepoliaStore.address}
chainWalletsInformation={evmWalletsInformation}
connectStatus={sepoliaStore.connectionStatus}
connectedWalletId={sepoliaStore.connectedWallet}
onConnectClick={sepoliaStore.connect}
onDisconnectClick={sepoliaStore.disconnect}
showDivider={true}
/>
<Connection
chain="cosmos"
address={cosmosStore.address}
chainWalletsInformation={cosmosWalletsInformation}
connectStatus={cosmosStore.connectionStatus}
connectedWalletId={cosmosStore.connectedWallet}
onConnectClick={cosmosStore.connect}
onDisconnectClick={cosmosStore.disconnect}
showDivider={true}
chain="sui"
address={suiStore.address}
chainWalletsInformation={suiWalletsInformation}
connectStatus={suiStore.connectionStatus}
connectedWalletId={suiStore.connectedWallet}
onConnectClick={(id: string) => suiStore.connect(id as any)}
onDisconnectClick={suiStore.disconnect}
showDivider={false}
/>

{:else if currentWalletType === "all"}
<Connection
chain="evm"
address={sepoliaStore.address}
chainWalletsInformation={evmWalletsInformation}
connectStatus={sepoliaStore.connectionStatus}
connectedWalletId={sepoliaStore.connectedWallet}
onConnectClick={sepoliaStore.connect}
onDisconnectClick={sepoliaStore.disconnect}
showDivider={true}
/>
<Connection
chain="cosmos"
address={cosmosStore.address}
chainWalletsInformation={cosmosWalletsInformation}
connectStatus={cosmosStore.connectionStatus}
connectedWalletId={cosmosStore.connectedWallet}
onConnectClick={cosmosStore.connect}
onDisconnectClick={cosmosStore.disconnect}
showDivider={true}
/>
<Connection
chain="sui"
address={suiStore.address}
chainWalletsInformation={suiWalletsInformation}
connectStatus={suiStore.connectionStatus}
connectedWalletId={suiStore.connectedWallet}
onConnectClick={(id: string) => suiStore.connect(id as any)}
onDisconnectClick={suiStore.disconnect}
showDivider={false}
/>
{/if}
</section>
</Modal>
48 changes: 48 additions & 0 deletions app2/src/lib/services/sui/balances.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Effect, Data, Schema } from "effect"
import { type FromHexError, fromHexString } from "$lib/utils/hex"
import type { Chain } from "@unionlabs/sdk/schema"
import { RawTokenBalance, TokenRawAmount, type TokenRawDenom } from "@unionlabs/sdk/schema"
import { Sui } from "@unionlabs/sdk-sui"
import { getSuiPublicClient, NoSuiRpcError } from "./clients"

export class ReadSuiCoinError extends Data.TaggedError("ReadSuiCoinError")<{ cause: unknown }> {}

export type FetchSuiBalanceError =
| NoSuiRpcError
| FromHexError
| ReadSuiCoinError
| Sui.CreatePublicClientError

export const BalanceSchema = Schema.Struct({
balance: Schema.String,
token: Schema.String,
address: Schema.String,
})

export const fetchSuiBalance = ({
chain,
tokenAddress,
walletAddress,
}: {
chain: Chain
tokenAddress: TokenRawDenom
walletAddress: string
}) =>
Effect.gen(function* () {
const coinType = yield* fromHexString(tokenAddress)

const publicClient = yield* getSuiPublicClient(chain)

const total = yield* Sui.readTotalCoinBalance(coinType, walletAddress).pipe(
Effect.provide(publicClient),
Effect.mapError((cause) => new ReadSuiCoinError({ cause })),
)

return RawTokenBalance.make(TokenRawAmount.make(total))
}).pipe(
Effect.annotateLogs({
universal_chain_id: chain.universal_chain_id,
walletAddress,
tokenAddress,
}),
)
32 changes: 32 additions & 0 deletions app2/src/lib/services/sui/clients.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Sui } from "@unionlabs/sdk-sui"
import type { Chain } from "@unionlabs/sdk/schema"
import { Data, Effect, Option } from "effect"
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519"

export class NoSuiRpcError extends Data.TaggedError("NoSuiRpcError")<{ chain: Chain }> {}

export const getSuiPublicClient = (chain: Chain) =>
Effect.gen(function* () {
const maybeRpc = chain.getRpcUrl("rpc")
if (Option.isNone(maybeRpc)) {
return yield* new NoSuiRpcError({ chain })
}
const url = maybeRpc.value.toString()

const layer = Sui.PublicClient.Live({ url })
const client = yield* Sui.PublicClient.pipe(Effect.provide(layer))
return client
})

export const getSuiWalletClient = (chain: Chain, signer: Ed25519Keypair) =>
Effect.gen(function* () {
const maybeRpc = chain.getRpcUrl("rpc")
if (Option.isNone(maybeRpc)) {
return yield* new NoSuiRpcError({ chain })
}
const url = maybeRpc.value.toString()

const layer = Sui.WalletClient.Live({ url, signer })
const wallet = yield* Sui.WalletClient.pipe(Effect.provide(layer))
return wallet
})
11 changes: 11 additions & 0 deletions app2/src/lib/services/transfer-ucs03-sui/account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { getAccountError } from "$lib/services/transfer-ucs03-evm/errors"
import { getWagmiConfig } from "$lib/wallet/evm/wagmi-config.svelte"
import { getAccount as getConnectedAccount } from "@wagmi/core"
import { Effect } from "effect"

export const getAccount = Effect.gen(function*() {
return yield* Effect.try({
try: () => getConnectedAccount(getWagmiConfig()),
catch: () => new getAccountError({ cause: "Could not get connected account" }),
})
})
9 changes: 9 additions & 0 deletions app2/src/lib/services/transfer-ucs03-sui/approval.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { ValidTransfer } from "@unionlabs/sdk/schema"
import { Effect } from "effect"

/**
* Sui coins don’t use approve/allowance.
* We short-circuit with a sentinel message so callers can skip receipt waits.
*/
export const approveTransfer = (_params: ValidTransfer["args"]) =>
Effect.succeed("sui-no-approval-needed" as const)
51 changes: 51 additions & 0 deletions app2/src/lib/services/transfer-ucs03-sui/chain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Effect, pipe } from "effect"
import type { Chain } from "@unionlabs/sdk/schema"
import { Sui } from "@unionlabs/sdk-sui"
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519"

export class SuiSwitchChainError extends Error {
constructor(readonly cause: unknown, readonly chainId?: string) {
super(`Sui switch network failed: ${String(cause)}`)
}
}
export class SuiWalletNotProvidedError extends Error {
constructor() { super("Sui signer (Ed25519Keypair) not provided") }
}

type SwitchChainSuccess = {
success: true
rpcUrl: string
publicClient: Sui.Sui.PublicClient
walletClient: Sui.Sui.WalletClient
}

export const SwitchChain = (chain: Chain, signer: Ed25519Keypair) =>
Effect.gen(function* () {
if (!signer) return yield* Effect.fail(new SuiWalletNotProvidedError())

const rpcUrl = yield* chain.getRpcUrl("rpc").pipe(
Effect.mapError((cause) => new SuiSwitchChainError(cause, chain.universal_chain_id)),
)

const publicLayer = Sui.PublicClient.Live({ url: rpcUrl })
const walletLayer = Sui.WalletClient.Live({ url: rpcUrl, signer: signer })

const { pub, wal } = yield* Effect.all({
pub: Sui.PublicClient,
wal: Sui.WalletClient,
}).pipe(Effect.provide(publicLayer), Effect.provide(walletLayer))

yield* Effect.tryPromise({
try: async () => pub.client.getReferenceGasPrice(),
catch: (cause) => new SuiSwitchChainError(cause, chain.universal_chain_id),
})

yield* Effect.sleep("1.5 seconds")

return {
success: true,
rpcUrl,
publicClient: pub,
walletClient: wal,
} satisfies SwitchChainSuccess
})
51 changes: 51 additions & 0 deletions app2/src/lib/services/transfer-ucs03-sui/channel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { ChannelValidationError } from "$lib/services/transfer-ucs03-evm/errors"
import type { UniversalChainId } from "@unionlabs/sdk/schema"
import { Channel, type Channels } from "@unionlabs/sdk/schema"
import { Effect } from "effect"

// TODO(ehegnes): replace this with a schema transform
export const getChannelInfo = (
source_universal_chain_id: UniversalChainId,
destination_universal_chain_id: UniversalChainId,
channels: typeof Channels.Type,
): Effect.Effect<typeof Channel.Type, ChannelValidationError> =>
Effect.gen(function*() {
const channel = channels.find(
chan =>
chan.destination_universal_chain_id === destination_universal_chain_id
&& chan.source_universal_chain_id === source_universal_chain_id,
)

if (
!channel
|| channel.source_connection_id === null
|| channel.source_channel_id === null
|| !channel.source_port_id
|| channel.destination_connection_id === null
|| channel.destination_channel_id === null
|| !channel.destination_port_id
) {
return yield* Effect.fail(
new ChannelValidationError({
source_universal_chain_id,
destination_universal_chain_id,
cause: "Missing required channel information",
}),
)
}

return new Channel({
source_universal_chain_id,
source_connection_id: channel.source_connection_id,
source_channel_id: channel.source_channel_id,
source_client_id: channel.source_client_id,
source_port_id: channel.source_port_id,
fees: channel.fees,
destination_universal_chain_id,
destination_connection_id: channel.destination_connection_id,
destination_channel_id: channel.destination_channel_id,
destination_client_id: channel.destination_client_id,
destination_port_id: channel.destination_port_id,
tags: channel.tags,
})
})
Loading
Loading