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 args #3920

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
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: 1 addition & 1 deletion app2/app2.nix
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ _: {
{
packages = {
app2 = jsPkgs.buildNpmPackage {
npmDepsHash = "sha256-4iHm9HkGsQzVmonjtTLbRIHHQRC9ser23gfMzYL6z2A=";
npmDepsHash = "sha256-qo3INRx9IMbe5lfJrm/7/S++u9TVun7NqtXuvqpzCII=";
src = ./.;
sourceRoot = "app2";
npmFlags = [ "--legacy-peer-deps" ];
Expand Down
1 change: 1 addition & 0 deletions app2/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions app2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@eslint/js": "^9.20.0",
"@keplr-wallet/types": "^0.12.190",
"@leapwallet/types": "^0.0.5",
"@scure/base": "^1.2.4",
"@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/kit": "^2.17.2",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
Expand Down
79 changes: 79 additions & 0 deletions app2/src/lib/examples/transfer-arguments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import type { RpcType } from "$lib/schema/chain"

type EVMTransferInput = {
sourceRpcType: "evm"
destinationRpcType: typeof RpcType.Type
baseToken: string
baseAmount: string
quoteToken: string
quoteAmount: string
sourceChannelId: number
wethToken: string
receiver: string
ucs03address: string
}

type CosmosTransferInput = {
sourceRpcType: "cosmos"
destinationRpcType: typeof RpcType.Type
baseToken: string
baseAmount: string
quoteToken: string
quoteAmount: string
sourceChannelId: number
receiver: string
ucs03address: string
}

type AptosTransferInput = {
sourceRpcType: "aptos"
destinationRpcType: typeof RpcType.Type
baseToken: string
baseAmount: string
quoteToken: string
quoteAmount: string
sourceChannelId: number
receiver: string
ucs03address: string
}

export const examples: {
evm: EVMTransferInput
cosmos: CosmosTransferInput
aptos: AptosTransferInput
} = {
evm: {
sourceRpcType: "evm",
destinationRpcType: "cosmos",
baseToken: "0x1c7d4b196cb0c7b01d743fbc6116a902379c7238",
baseAmount: "1000",
quoteToken: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
quoteAmount: "1000",
receiver: "union10z7xxj2m8q3f7j58uxmff38ws9u8m0vmne2key",
sourceChannelId: 1,
ucs03address: "0x742d35cc6634c0532925a3b844bc454e4438f44e",
wethToken: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
},
cosmos: {
sourceRpcType: "cosmos",
destinationRpcType: "evm",
baseToken: "0x1c7d4b196cb0c7b01d743fbc6116a902379c7238",
baseAmount: "1000",
quoteToken: "0xabcdef1234567890abcdef1234567890abcdef12",
quoteAmount: "1000",
receiver: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
sourceChannelId: 2,
ucs03address: "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
},
aptos: {
sourceRpcType: "aptos",
destinationRpcType: "evm",
baseToken: "0x1c7d4b196cb0c7b01d743fbc6116a902379c7238",
baseAmount: "1000",
quoteToken: "0x2abcdef1234567890abcdef1234567890abcdef12",
quoteAmount: "1000",
receiver: "0x1f9090aae28b8a3dceadf281b0f12828e676c326",
sourceChannelId: 3,
ucs03address: "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
}
}
2 changes: 1 addition & 1 deletion app2/src/lib/schema/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const RpcType = Schema.Union(
Schema.Literal("evm"),
Schema.Literal("cosmos"),
Schema.Literal("aptos")
)
).annotations({ message: () => "type must be 'evm', 'cosmos', or 'aptos'" })

export class ChainFeatures extends Schema.Class<ChainFeatures>("ChainFeatures")({
channel_list: Schema.Boolean,
Expand Down
5 changes: 4 additions & 1 deletion app2/src/lib/schema/channel.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { Schema } from "effect"

export const ChannelId = Schema.Int.pipe(Schema.brand("ChannelId"))
export const ChannelId = Schema.Int.pipe(
Schema.nonNegative({ message: () => "ChannelId must be non-negative" }),
Schema.brand("ChannelId")
)
7 changes: 7 additions & 0 deletions app2/src/lib/schema/token.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { Schema } from "effect"
import { Hex } from "$lib/schema/hex"
import { AddressEvmCanonical } from "$lib/schema/address"

export const TokenRawDenom = Hex.pipe(Schema.brand("TokenRawDenom"))
export const TokenRawAmount = Schema.BigInt.pipe(Schema.brand("TokenRawAmount"))
export const EVMWethToken = AddressEvmCanonical.pipe(
Schema.annotations({
message: () =>
"WETH token must be a valid EVM canonical address (e.g., 0x followed by 40 hex chars)"
})
)
96 changes: 96 additions & 0 deletions app2/src/lib/schema/transfer-arguments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Schema } from "effect"
import { RpcType } from "$lib/schema/chain"
import { EVMWethToken, TokenRawAmount, TokenRawDenom } from "$lib/schema/token"
import { ChannelId } from "$lib/schema/channel"
import { isValidCanonicalForChain } from "$lib/utils/convert-display"

const BaseTransferFields = {
baseToken: TokenRawDenom.annotations({
message: () => "baseToken must be a non-empty string (e.g., token address or symbol)"
}),
baseAmount: TokenRawAmount.annotations({
message: () => "baseAmount must be a valid bigint string (e.g., '1000000')"
}),
quoteToken: TokenRawDenom.annotations({
message: () => "quoteToken must be a non-empty string (e.g., token address or symbol)"
}),
quoteAmount: TokenRawAmount.annotations({
message: () => "quoteAmount must be a valid bigint string (e.g., '1000000')"
}),
sourceChannelId: ChannelId.annotations({
message: () => "sourceChannelId must be a non-negative integer"
}),
destinationRpcType: RpcType.annotations({
message: () => "destinationType must be a valid RPC type ('evm', 'cosmos', or 'aptos')"
})
}

const EVMTransferSchema = Schema.Struct({
...BaseTransferFields,
sourceRpcType: RpcType.pipe(
Schema.filter(v => v === "evm", { message: () => "type must be 'evm'" })
),
wethToken: EVMWethToken,
receiver: Schema.String.pipe(
Schema.nonEmptyString({ message: () => "receiver must be a non-empty string" })
)
}).pipe(
Schema.filter(data =>
isValidCanonicalForChain(data.receiver, data.destinationRpcType)
? true
: `receiver must be a valid display address for ${data.destinationRpcType}`
)
)

export class EVMTransfer extends Schema.Class<EVMTransfer>("EVMTransfer")(EVMTransferSchema) {}

const CosmosTransferSchema = Schema.Struct({
...BaseTransferFields,
sourceRpcType: RpcType.pipe(
Schema.filter(v => v === "cosmos", { message: () => "type must be 'cosmos'" })
),
receiver: Schema.String.pipe(
Schema.nonEmptyString({ message: () => "receiver must be a non-empty string" })
)
}).pipe(
Schema.filter(data =>
isValidCanonicalForChain(data.receiver, data.destinationRpcType)
? true
: `receiver must be a valid display address for ${data.destinationRpcType}`
)
)

export class CosmosTransfer extends Schema.Class<CosmosTransfer>("CosmosTransfer")(
CosmosTransferSchema
) {}

const AptosTransferSchema = Schema.Struct({
...BaseTransferFields,
sourceRpcType: RpcType.pipe(
Schema.filter(v => v === "aptos", { message: () => "type must be 'aptos'" })
),
receiver: Schema.String.pipe(
Schema.nonEmptyString({ message: () => "receiver must be a non-empty string" })
)
}).pipe(
Schema.filter(data =>
isValidCanonicalForChain(data.receiver, data.destinationRpcType)
? true
: `receiver must be a valid display address for ${data.destinationRpcType}`
)
)

export class AptosTransfer extends Schema.Class<AptosTransfer>("AptosTransfer")(
AptosTransferSchema
) {}

export const TransferSchema = Schema.Union(EVMTransfer, CosmosTransfer, AptosTransfer).annotations({
identifier: "Transfer",
title: "Transfer",
description: "transfer arguments"
})

export type Transfer = Schema.Schema.Type<typeof TransferSchema>
export type EVMTransferType = Schema.Schema.Type<typeof EVMTransfer>
export type CosmosTransferType = Schema.Schema.Type<typeof CosmosTransfer>
export type AptosTransferType = Schema.Schema.Type<typeof AptosTransfer>
137 changes: 137 additions & 0 deletions app2/src/lib/utils/convert-display.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { bech32 } from "@scure/base"
import { Schema } from "effect"
import { AddressAptosDisplay, AddressCosmosDisplay, AddressEvmDisplay } from "$lib/schema/address"

/**
* Convert a bech32 display address to canonical bytes
*/
export function cosmosDisplayToCanonical(displayAddress: string): Uint8Array {
try {
const decoded = bech32.decode(displayAddress as `${string}1${string}`)
const canonicalAddress = bech32.fromWords(decoded.words)
return new Uint8Array(canonicalAddress)
} catch (error: any) {
throw new Error(`Invalid Cosmos bech32 address: ${error.message}`)
}
}

/**
* Convert an EVM display address (hex) to canonical bytes
*/
export function evmDisplayToCanonical(displayAddress: string): Uint8Array {
// Validate EVM address format (0x + 40 hex characters)
if (!/^0x[0-9a-fA-F]{40}$/.test(displayAddress)) {
throw new Error("EVM address must be 0x followed by 40 hex characters")
}

// Remove 0x prefix and convert to bytes
const hexWithoutPrefix = displayAddress.slice(2)
const bytes = new Uint8Array(20)

for (let i = 0; i < 40; i += 2) {
bytes[i / 2] = Number.parseInt(hexWithoutPrefix.substring(i, i + 2), 16)
}

return bytes
}

/**
* Convert an Aptos display address (hex) to canonical bytes
*/
export function aptosDisplayToCanonical(displayAddress: string): Uint8Array {
// Validate Aptos address format (0x + 64 hex characters)
if (!/^0x[0-9a-fA-F]{64}$/.test(displayAddress)) {
throw new Error("Aptos address must be 0x followed by 64 hex characters")
}

// Remove 0x prefix and convert to bytes
const hexWithoutPrefix = displayAddress.slice(2)
const bytes = new Uint8Array(32)

for (let i = 0; i < 64; i += 2) {
bytes[i / 2] = Number.parseInt(hexWithoutPrefix.substring(i, i + 2), 16)
}

return bytes
}

/**
* Converts a Uint8Array to a hex string
*/
export function bytesToHex(bytes: Uint8Array): string {
let hexString = ""
for (const byte of bytes) {
hexString += byte.toString(16).padStart(2, "0")
}
return `0x${hexString}`
}

export const isValidCanonicalForChain = (
displayAddress: string,
destinationRpcType: string
): boolean => {
if (!displayAddress || displayAddress.length === 0) {
return false
}

// Function to validate display format using schema
const isValidDisplay = (schema: Schema.Schema<any, any>): boolean => {
try {
Schema.decodeSync(schema)(displayAddress, { errors: "all" })
return true
} catch (e) {
return false
}
}

// First validate the display format using appropriate schema
let isValidDisplayFormat = false
switch (destinationRpcType) {
case "evm":
isValidDisplayFormat = isValidDisplay(AddressEvmDisplay)
break
case "cosmos":
isValidDisplayFormat = isValidDisplay(AddressCosmosDisplay)
break
case "aptos":
isValidDisplayFormat = isValidDisplay(AddressAptosDisplay)
break
default:
return false
}

// If display format is invalid, canonical format cannot be valid
if (!isValidDisplayFormat) {
return false
}

// Then convert from display to canonical and validate
try {
let canonicalBytes: Uint8Array

switch (destinationRpcType) {
case "evm": {
// Convert EVM display address (checksum hex) to canonical bytes (20 bytes)
canonicalBytes = evmDisplayToCanonical(displayAddress)
return canonicalBytes.length === 20
}

case "cosmos": {
// Convert Cosmos display address (bech32) to canonical bytes
canonicalBytes = cosmosDisplayToCanonical(displayAddress)
return canonicalBytes.length === 20 || canonicalBytes.length === 32
}

case "aptos": {
// Convert Aptos display address (hex) to canonical bytes
canonicalBytes = aptosDisplayToCanonical(displayAddress)
return canonicalBytes.length === 32
}

default:
return false
}
} catch (error) {
return false
}
}
Loading