Skip to content

Commit

Permalink
Fetch EVM balances, make everything generic over EVM chain, v2 schema…
Browse files Browse the repository at this point in the history
… fixes (#3962)

- **feat(app2): add balances evm**
- **refactor(app2): better schema, better balances**
- **feat(app2): fetch a balance**
- **feat(app2): fetch all sepolia balances**
- **feat(app2): fetch all evm balances**
- **feat(app2): show token errors**
- **fix(app2): evm balance fetching**
- **feat(app2): use v2_chains**
- **feat(app2): add chain.toViemChain()**
- **feat(app2): fetch balances for all evm chains, also significant
cleanup**
- **feat(app2): generic over evm chain**
- **chore(app2): fmt**
  • Loading branch information
cor authored Mar 6, 2025
2 parents 4da42b3 + d1b20be commit 46dd304
Show file tree
Hide file tree
Showing 25 changed files with 1,006 additions and 1,396 deletions.
121 changes: 54 additions & 67 deletions app2/src/generated/graphql-env.d.ts

Large diffs are not rendered by default.

1,741 changes: 590 additions & 1,151 deletions app2/src/generated/schema.graphql

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions app2/src/lib/constants/viem-chains.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {
arbitrumSepolia,
berachainTestnetbArtio,
holesky,
scrollSepolia,
sepolia
} from "viem/chains"

export const VIEM_CHAINS = [
sepolia,
holesky,
berachainTestnetbArtio,
arbitrumSepolia,
scrollSepolia
]
6 changes: 3 additions & 3 deletions app2/src/lib/queries/chains.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import type { Environment } from "$lib/constants"

export let chainsQuery = (environment: Environment) =>
createQueryGraphql({
schema: Schema.Struct({ v1_ibc_union_chains: Chains }),
schema: Schema.Struct({ v2_chains: Chains }),
document: graphql(`
query Chains($environment: String!) @cached(ttl: 60) {
v1_ibc_union_chains(where: {enabled: {_eq: true}}) {
v2_chains {
chain_id,
universal_chain_id,
display_name,
Expand All @@ -31,7 +31,7 @@ export let chainsQuery = (environment: Environment) =>
variables: { environment },
refetchInterval: "60 seconds",
writeData: data => {
chains.data = data.pipe(Option.map(d => d.v1_ibc_union_chains))
chains.data = data.pipe(Option.map(d => d.v2_chains))
},
writeError: error => {
chains.error = error
Expand Down
9 changes: 3 additions & 6 deletions app2/src/lib/queries/tokens.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@ export const tokensQuery = (universalChainId: UniversalChainId) =>
cw20 {
cw20_token_address
}
chain {
chain_id
}
representations {
name
symbol
Expand All @@ -34,18 +31,18 @@ export const tokensQuery = (universalChainId: UniversalChainId) =>
wrapping {
destination_channel_id
unwrapped_chain {
chain_id
universal_chain_id
}
unwrapped_denom
}
}
}
wrapping {
unwrapped_chain {
chain_id
universal_chain_id
}
wrapped_chain {
chain_id
universal_chain_id
}
destination_channel_id
unwrapped_denom
Expand Down
6 changes: 3 additions & 3 deletions app2/src/lib/queries/transfer-list-address.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const transferListLatestAddressQuery = (
query TransferListLatestAddress($addresses: jsonb, $limit: Int!) @cached(ttl: 1) {
v2_transfers(args: {
p_limit: $limit,
p_canonical_addresses: $addresses
p_addresses_canonical: $addresses
}) {
...TransferListItem
}
Expand Down Expand Up @@ -50,7 +50,7 @@ export const transferListPageLtAddressQuery = (
query TransferListPageLtAddress($page: String!, $addresses: jsonb, $limit: Int!) @cached(ttl: 30) {
v2_transfers(args: {
p_limit: $limit,
p_canonical_addresses: $addresses,
p_addresses_canonical: $addresses,
p_sort_order: $page
}) {
...TransferListItem
Expand Down Expand Up @@ -116,7 +116,7 @@ export const transferListPageGtAddressQuery = (
query TransferListPageGtAddress($page: String!, $addresses: jsonb, $limit: Int!) @cached(ttl: 30) {
v2_transfers(args: {
p_limit: $limit,
p_canonical_addresses: $addresses,
p_addresses_canonical: $addresses,
p_sort_order: $page,
p_comparison: "gt"
}) {
Expand Down
1 change: 1 addition & 0 deletions app2/src/lib/schema/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const AddressCosmosZkgm = Hex.pipe(Schema.brand("AddressCosmosZkgm")) //

// EVM Address Types
export const AddressEvmCanonical = AddressCanonicalBytes.pipe(Schema.brand("AddressEvmCanonical"))
export type AddressEvmCanonical = typeof AddressEvmCanonical.Type
export const AddressEvmDisplay = HexChecksum.pipe(Schema.brand("AddressEvmDisplay"))
export const AddressEvmZkgm = AddressEvmCanonical

Expand Down
17 changes: 11 additions & 6 deletions app2/src/lib/schema/chain.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { VIEM_CHAINS } from "$lib/constants/viem-chains"
import { Option, Schema } from "effect"
import type { Chain as ViemChain } from "viem"

export const ChainId = Schema.String.pipe(Schema.brand("ChainId"))
// e.g. union.union-testnet-9
Expand All @@ -9,11 +11,7 @@ export type UniversalChainId = typeof UniversalChainId.Type

export const ChainDisplayName = Schema.String.pipe(Schema.brand("ChainDisplayName"))

export const RpcType = Schema.Union(
Schema.Literal("evm"),
Schema.Literal("cosmos"),
Schema.Literal("aptos")
)
export const RpcType = Schema.Literal("evm", "cosmos", "aptos")

export class ChainFeatures extends Schema.Class<ChainFeatures>("ChainFeatures")({
channel_list: Schema.Boolean,
Expand All @@ -37,7 +35,14 @@ export class Chain extends Schema.Class<Chain>("Chain")({
addr_prefix: Schema.String,
testnet: Schema.Boolean,
features: Schema.Array(ChainFeatures)
}) {}
}) {
toViemChain(): Option.Option<ViemChain> {
if (this.rpc_type !== "evm") {
return Option.none()
}
return Option.fromNullable(VIEM_CHAINS.find(vc => `${vc.id}` === this.chain_id))
}
}

export const Chains = Schema.Array(Chain)

Expand Down
6 changes: 4 additions & 2 deletions app2/src/lib/schema/hex.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Schema } from "effect"

export const Hex = Schema.String.pipe(Schema.pattern(/^0x[0-9a-f]+$/))
export const Hex = Schema.TemplateLiteral("0x", Schema.String).pipe(Schema.pattern(/^0x[0-9a-f]+$/))

// TODO: validate ERC55 checksum
export const HexChecksum = Schema.String.pipe(Schema.pattern(/^0x[0-9a-fA-F]+$/))
export const HexChecksum = Schema.TemplateLiteral("0x", Schema.String).pipe(
Schema.pattern(/^0x[0-9a-fA-F]+$/)
)
11 changes: 4 additions & 7 deletions app2/src/lib/schema/token.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Schema } from "effect"
import { Hex } from "$lib/schema/hex"
import { ChainId } from "./chain.ts"
import { UniversalChainId } from "./chain.ts"
import { ChannelId } from "./channel.ts"

export const TokenRawDenom = Hex.pipe(Schema.brand("TokenRawDenom"))
Expand All @@ -21,7 +21,7 @@ export class TokenSource extends Schema.Class<TokenSource>("TokenSource")({
export class TokenSourceWrapping extends Schema.Class<TokenSourceWrapping>("TokenSourceWrapping")({
destination_channel_id: ChannelId,
unwrapped_chain: Schema.Struct({
chain_id: ChainId
universal_chain_id: UniversalChainId
}),
unwrapped_denom: TokenRawDenom
}) {}
Expand All @@ -41,10 +41,10 @@ export class TokenRepresentation extends Schema.Class<TokenRepresentation>("Toke

export class TokenWrapping extends Schema.Class<TokenWrapping>("TokenWrapping")({
unwrapped_chain: Schema.Struct({
chain_id: ChainId
universal_chain_id: UniversalChainId
}),
wrapped_chain: Schema.Struct({
chain_id: ChainId
universal_chain_id: UniversalChainId
}),
destination_channel_id: ChannelId,
unwrapped_denom: TokenRawDenom
Expand All @@ -53,9 +53,6 @@ export class TokenWrapping extends Schema.Class<TokenWrapping>("TokenWrapping")(
export class Token extends Schema.Class<Token>("Token")({
denom: TokenRawDenom,
cw20: Schema.OptionFromNullOr(TokenCw20),
chain: Schema.Struct({
chain_id: ChainId
}),
representations: Schema.Array(TokenRepresentation),
wrapping: Schema.Array(TokenWrapping)
}) {}
Expand Down
90 changes: 90 additions & 0 deletions app2/src/lib/services/evm/balances.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Data, Effect, Option, Schema, Schedule } from "effect"
import { erc20Abi, type PublicClient } from "viem"
import type { TimeoutException } from "effect/Cause"
import type { DurationInput } from "effect/Duration"
import type { ReadContractErrorType } from "viem"
import { getPublicClient } from "$lib/services/evm/clients"
import { RawTokenBalance, TokenRawAmount, type TokenRawDenom } from "$lib/schema/token"
import type { NoViemChainError } from "$lib/services/evm/clients"
import type { AddressEvmCanonical } from "$lib/schema/address"
import type { Chain } from "$lib/schema/chain"
import type { CreatePublicClientError } from "$lib/services/transfer"

export type FetchBalanceError =
| NoViemChainError
| TimeoutException
| ReadContractError
| CreatePublicClientError
export class ReadContractError extends Data.TaggedError("ReadContractError")<{
cause: ReadContractErrorType
}> {}

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

const fetchTokenBalance = ({
client,
tokenAddress,
walletAddress
}: {
client: PublicClient
tokenAddress: TokenRawDenom
walletAddress: AddressEvmCanonical
}) =>
Effect.tryPromise({
try: () =>
client.readContract({
address: tokenAddress,
abi: erc20Abi,
functionName: "balanceOf",
args: [walletAddress]
}),
catch: err => new ReadContractError({ cause: err as ReadContractErrorType })
})

export const createBalanceQuery = ({
chain,
tokenAddress,
walletAddress,
refetchInterval,
writeData,
writeError
}: {
chain: Chain
tokenAddress: TokenRawDenom
walletAddress: AddressEvmCanonical
refetchInterval: DurationInput
writeData: (data: RawTokenBalance) => void
writeError: (error: Option.Option<FetchBalanceError>) => void
}) => {
const fetcherPipeline = Effect.gen(function* (_) {
yield* Effect.log(`starting balances fetcher for ${walletAddress}:${tokenAddress}`)
const client = yield* getPublicClient(chain)

const balance = yield* Effect.retry(
fetchTokenBalance({ client, tokenAddress, walletAddress }).pipe(Effect.timeout("10 seconds")),
{ times: 4 }
)

yield* Effect.sync(() => {
writeData(RawTokenBalance.make(Option.some(TokenRawAmount.make(balance))))
writeError(Option.none())
})
}).pipe(
Effect.tapError(error =>
Effect.sync(() => {
writeError(Option.some(error))
})
),
Effect.catchAll(_ => Effect.succeed(null))
)

return Effect.repeat(
fetcherPipeline,
Schedule.addDelay(Schedule.repeatForever, () => refetchInterval)
)
}
63 changes: 63 additions & 0 deletions app2/src/lib/services/evm/clients.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Data, Effect, Option } from "effect"
import {
createPublicClient,
createWalletClient,
http,
custom,
type CreatePublicClientErrorType,
type CreateWalletClientErrorType
} from "viem"
import { getConnectorClient, type GetConnectorClientErrorType } from "@wagmi/core"
import { wagmiConfig } from "$lib/wallet/evm/wagmi-config"
import {
CreatePublicClientError,
CreateWalletClientError,
ConnectorClientError
} from "../transfer/errors.ts"
import type { Chain } from "$lib/schema/chain.ts"

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

export const getPublicClient = (chain: Chain) =>
Effect.gen(function* () {
const viemChain = chain.toViemChain()

if (Option.isNone(viemChain)) {
return yield* new NoViemChainError({ chain })
}

const client = yield* Effect.try({
try: () =>
createPublicClient({
chain: viemChain.value,
transport: http()
}),
catch: err => new CreatePublicClientError({ cause: err as CreatePublicClientErrorType })
})
return client
})

export const getWalletClient = (chain: Chain) =>
Effect.gen(function* () {
const viemChain = chain.toViemChain()

if (Option.isNone(viemChain)) {
return yield* new NoViemChainError({ chain })
}

const connectorClient = yield* Effect.tryPromise({
try: () => getConnectorClient(wagmiConfig),
catch: err => new ConnectorClientError({ cause: err as GetConnectorClientErrorType })
})

return yield* Effect.try({
try: () =>
createWalletClient({
chain: viemChain.value,
transport: custom(connectorClient.transport)
}),
catch: err => new CreateWalletClientError({ cause: err as CreateWalletClientErrorType })
})
})
14 changes: 7 additions & 7 deletions app2/src/lib/services/transfer-ucs03-evm/approval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import {
type WriteContractErrorType
} from "viem"
import { WaitForTransactionReceiptError, WriteContractError } from "./errors.ts"
import { getPublicClient, getWalletClient } from "./clients.ts"
import type { TransactionEvmParams } from "$lib/services/transfer-ucs03-evm/machine"
import { getPublicClient, getWalletClient } from "../evm/clients.ts"
import type { Ucs03TransferEvm } from "$lib/services/transfer-ucs03-evm/machine"
import { getAccount } from "$lib/services/transfer-ucs03-evm/account.ts"
import type { Chain } from "$lib/schema/chain.ts"

export const approveTransfer = (transactionArgs: TransactionEvmParams) =>
export const approveTransfer = (chain: Chain, transactionArgs: Ucs03TransferEvm) =>
Effect.gen(function* () {
const walletClient = yield* getWalletClient
const walletClient = yield* getWalletClient(chain)

const account = yield* Effect.flatMap(getAccount, account =>
account ? Effect.succeed(account) : Effect.fail(new Error("No account connected"))
Expand All @@ -23,7 +24,6 @@ export const approveTransfer = (transactionArgs: TransactionEvmParams) =>
walletClient.writeContract({
account: account.address as `0x${string}`,
abi: erc20Abi,
chain: transactionArgs.chain,
functionName: "approve",
address: transactionArgs.baseToken,
args: [transactionArgs.ucs03address, transactionArgs.baseAmount]
Expand All @@ -34,9 +34,9 @@ export const approveTransfer = (transactionArgs: TransactionEvmParams) =>
return hash
})

export const waitForApprovalReceipt = (hash: Hash) =>
export const waitForApprovalReceipt = (chain: Chain, hash: Hash) =>
Effect.gen(function* () {
const publicClient = yield* getPublicClient
const publicClient = yield* getPublicClient(chain)

const receipt = yield* Effect.tryPromise({
try: () => publicClient.waitForTransactionReceipt({ hash }),
Expand Down
Loading

0 comments on commit 46dd304

Please sign in to comment.