diff --git a/app/app.nix b/app/app.nix index 8fb0a0b965..36a4747ed5 100644 --- a/app/app.nix +++ b/app/app.nix @@ -19,7 +19,7 @@ _: { { packages = { app = jsPkgs.buildNpmPackage { - npmDepsHash = "sha256-YAUgEe+4g4GbKprXJoeMKrkvhUqXZ6md5WJph2DGcEM="; + npmDepsHash = "sha256-KZE/nwYPBiqJEaVyV8vqu/nLmqtb6XrGRvKQwauQWxw="; src = ./.; sourceRoot = "app"; npmFlags = [ diff --git a/app/package-lock.json b/app/package-lock.json index 68c15aa116..a925f10f3f 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -32,7 +32,7 @@ "svelte-sonner": "^0.3.27", "temporal-polyfill": "^0.2.5", "three": "0.170.0", - "valibot": "0.42.1", + "valibot": "1.0.0-beta.9", "vaul-svelte": "^0.3.2", "viem": "2.21.52" }, @@ -42,7 +42,7 @@ "@iconify-json/lucide": "^1.2.3", "@iconify-json/mdi": "^1.2.0", "@iconify-json/tabler": "1.2.8", - "@keplr-wallet/types": "0.12.157", + "@keplr-wallet/types": "^0.12.158", "@leapwallet/types": "^0.0.5", "@melt-ui/pp": "^0.3.2", "@melt-ui/svelte": "^0.83.0", @@ -1223,9 +1223,9 @@ } }, "node_modules/@keplr-wallet/types": { - "version": "0.12.157", - "resolved": "https://registry.npmjs.org/@keplr-wallet/types/-/types-0.12.157.tgz", - "integrity": "sha512-CWRg6AK2x7sXZ0rhr9NSL8z9sIdnNHgrce3nyOBAJkXJ9XtrrJoOtopnkFaQmFOanysMHUrKMCbZFiaYrBmqew==", + "version": "0.12.158", + "resolved": "https://registry.npmjs.org/@keplr-wallet/types/-/types-0.12.158.tgz", + "integrity": "sha512-dhIYxeCIovK/YblwnetYAZNC8SPGfRLbM7TQ79vuaPCLm6jWLjcDkFnkIa/1jKBVbqH7mQAM64hcph+o6wrv2w==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -10149,9 +10149,9 @@ } }, "node_modules/valibot": { - "version": "0.42.1", - "resolved": "https://registry.npmjs.org/valibot/-/valibot-0.42.1.tgz", - "integrity": "sha512-3keXV29Ar5b//Hqi4MbSdV7lfVp6zuYLZuA9V1PvQUsXqogr+u5lvLPLk3A4f74VUXDnf/JfWMN6sB+koJ/FFw==", + "version": "1.0.0-beta.9", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.0.0-beta.9.tgz", + "integrity": "sha512-yEX8gMAZ2R1yI2uwOO4NCtVnJQx36zn3vD0omzzj9FhcoblvPukENIiRZXKZwCnqSeV80bMm8wNiGhQ0S8fiww==", "license": "MIT", "peerDependencies": { "typescript": ">=5" diff --git a/app/package.json b/app/package.json index e0b882d312..b48f6680fc 100644 --- a/app/package.json +++ b/app/package.json @@ -15,9 +15,6 @@ "postinstall": "patch-package" }, "dependencies": { - "temporal-polyfill": "^0.2.5", - "mode-watcher": "0.5.0", - "svelte-sonner": "^0.3.27", "@cosmjs/amino": "^0.32.4", "@cosmjs/cosmwasm-stargate": "0.32.4", "@cosmjs/encoding": "^0.32.4", @@ -35,10 +32,13 @@ "cmdk-sv": "^0.0.18", "gql.tada": "1.8.10", "graphql-request": "7.1.2", + "mode-watcher": "0.5.0", "svelte-persisted-store": "^0.11.0", "svelte-radix": "^1.1.1", + "svelte-sonner": "^0.3.27", + "temporal-polyfill": "^0.2.5", "three": "0.170.0", - "valibot": "0.42.1", + "valibot": "1.0.0-beta.9", "vaul-svelte": "^0.3.2", "viem": "2.21.52" }, @@ -48,7 +48,7 @@ "@iconify-json/lucide": "^1.2.3", "@iconify-json/mdi": "^1.2.0", "@iconify-json/tabler": "1.2.8", - "@keplr-wallet/types": "0.12.157", + "@keplr-wallet/types": "^0.12.158", "@leapwallet/types": "^0.0.5", "@melt-ui/pp": "^0.3.2", "@melt-ui/svelte": "^0.83.0", diff --git a/app/src/app.d.ts b/app/src/app.d.ts index 238503aac3..3c2ca70a08 100644 --- a/app/src/app.d.ts +++ b/app/src/app.d.ts @@ -21,6 +21,10 @@ interface AptosWindow { declare global { namespace App {} + interface EventTarget { + value?: string + } + interface Window extends AptosWindow, KeplrWindow, LeapWindow, Browser, GoogleRecaptcha { EventEmitter: typeof EventEmitter } diff --git a/app/src/lib/components/connect/connect.svelte b/app/src/lib/components/connect/connect.svelte index fe8923189a..268a3f155c 100644 --- a/app/src/lib/components/connect/connect.svelte +++ b/app/src/lib/components/connect/connect.svelte @@ -33,10 +33,6 @@ let connectedWallets = derived( } ) -onMount(() => { - console.info($aptosStore) -}) - $: if ($connectedWallets >= 1) { buttonText = $connectedWallets < 3 ? `Connected ${$connectedWallets}/3` : "Connected" } else { diff --git a/app/src/lib/polyfill.ts b/app/src/lib/polyfill.ts index ee87df69dc..a760ec40f9 100644 --- a/app/src/lib/polyfill.ts +++ b/app/src/lib/polyfill.ts @@ -1,5 +1,5 @@ import "temporal-polyfill/global" -import EventEmitter from "events" +import EventEmitter from "node:events" import { browser } from "$app/environment" if (browser) { diff --git a/app/src/lib/queries/balance/index.ts b/app/src/lib/queries/balance/index.ts index 3aea0d924d..987d5dbd46 100644 --- a/app/src/lib/queries/balance/index.ts +++ b/app/src/lib/queries/balance/index.ts @@ -19,6 +19,16 @@ export function userBalancesQuery({ connected?: boolean }) { return createQueries({ + combine: resultArray => ({ + data: resultArray.reduce( + (accumulator, current, index) => { + accumulator[chains[index].chain_id] = current.data + return accumulator + }, + {} as Record + ), + pending: resultArray.some(result => result.isLoading) + }), queries: chains.map(chain => ({ queryKey: [ "balances", @@ -37,14 +47,15 @@ export function userBalancesQuery({ .filter(rpc => rpc.type === "alchemy" || rpc.type === "routescan") .at(0) - if (rpc?.type === "alchemy") { - return await getBalancesFromAlchemy({ + if (rpc?.type === "routescan") { + return await getBalancesFromRoutescan({ url: rpc.url, walletAddress: userAddr.evm.canonical }) } - if (rpc?.type === "routescan") { - return await getBalancesFromRoutescan({ + + if (rpc?.type === "alchemy") { + return await getBalancesFromAlchemy({ url: rpc.url, walletAddress: userAddr.evm.canonical }) @@ -61,7 +72,7 @@ export function userBalancesQuery({ return multicallResults .map((result, index) => ({ - balance: result.balance, + balance: BigInt(result.balance), address: tokenList[index].denom, name: tokenList[index].display_name, symbol: tokenList[index].display_symbol, diff --git a/app/src/lib/queries/chains.ts b/app/src/lib/queries/chains.ts index 7cf5cd326c..77b0a3ef10 100644 --- a/app/src/lib/queries/chains.ts +++ b/app/src/lib/queries/chains.ts @@ -1,12 +1,16 @@ +import { URLS } from "$lib/constants" +import { request } from "graphql-request" import { createQuery } from "@tanstack/svelte-query" import { chainsQueryDocument } from "$lib/graphql/queries/chains" -import { request } from "graphql-request" -import { URLS } from "$lib/constants" +export const chainsQueryKeys = { + all: ["chains"] as const, + list: (filters: string) => [...chainsQueryKeys.all, { filters }] as const +} export const chainsQuery = () => createQuery({ - queryKey: ["chains"], + queryKey: chainsQueryKeys.all, placeholderData: (previousData, _) => previousData, queryFn: async () => (await request(URLS().GRAPHQL, chainsQueryDocument, {})).v1_chains, enabled: true, diff --git a/app/src/routes/transfer-new/(components)/assets-dialog.svelte b/app/src/routes/transfer-new/(components)/assets-dialog.svelte new file mode 100644 index 0000000000..954e374298 --- /dev/null +++ b/app/src/routes/transfer-new/(components)/assets-dialog.svelte @@ -0,0 +1,77 @@ + + + + + + Select Asset + +
+
    + {#each assets as asset, index} + {@const supportedAsset = getSupportedAsset(chain, asset.address)} + {#if $showUnsupported || supportedAsset} +
  • + +
  • + {/if} + {/each} +
+
+
+
diff --git a/app/src/routes/transfer-new/(components)/chain-button.svelte b/app/src/routes/transfer-new/(components)/chain-button.svelte new file mode 100644 index 0000000000..9c3a78073d --- /dev/null +++ b/app/src/routes/transfer-new/(components)/chain-button.svelte @@ -0,0 +1,23 @@ + + + diff --git a/app/src/routes/transfer-new/(components)/chain-dialog.svelte b/app/src/routes/transfer-new/(components)/chain-dialog.svelte new file mode 100644 index 0000000000..43f6420030 --- /dev/null +++ b/app/src/routes/transfer-new/(components)/chain-dialog.svelte @@ -0,0 +1,54 @@ + + + + + + + Select {kind} Network + + + +
    + {#each chains as chain, index} + {@const selected = selectedChain === chain.chain_id} +
  • + +
  • + {/each} +
+
+
+
diff --git a/app/src/routes/transfer-new/(components)/chevron.svelte b/app/src/routes/transfer-new/(components)/chevron.svelte new file mode 100644 index 0000000000..99b87fed01 --- /dev/null +++ b/app/src/routes/transfer-new/(components)/chevron.svelte @@ -0,0 +1,4 @@ + + diff --git a/app/src/routes/transfer-new/+layout.svelte b/app/src/routes/transfer-new/+layout.svelte new file mode 100644 index 0000000000..f5c3e41ad6 --- /dev/null +++ b/app/src/routes/transfer-new/+layout.svelte @@ -0,0 +1,13 @@ + + + + Union | Send + + + +
+ +
+
diff --git a/app/src/routes/transfer-new/+page.svelte b/app/src/routes/transfer-new/+page.svelte new file mode 100644 index 0000000000..7087c046bd --- /dev/null +++ b/app/src/routes/transfer-new/+page.svelte @@ -0,0 +1,304 @@ + + + updateParams({ source: value })} + selectedChain={$page.url.searchParams.get('source') || $transferState.data?.source} +/> + + { + // set receiver to self initially + const selectedDestinationChain = chains.find(c => c.chain_id === value) + if (!selectedDestinationChain) return + destinationChain.set(selectedDestinationChain) + let destinationAddress = $userAddress[$destinationChain?.rpc_type]?.canonical + if (!destinationAddress?.length) return + if ($destinationChain?.rpc_type === 'cosmos') { + destinationAddress = bech32ToBech32Address({ + address: destinationAddress, + toPrefix: $destinationChain.addr_prefix, + }) + } + if (!destinationAddress?.length) return updateParams({ destination: value }) + const receiverField = document.querySelector('input[name="receiver"]') + if (!receiverField) return + receiverField.value = destinationAddress + return updateParams({ destination: value, receiver: destinationAddress }) + }} +/> + + updateParams({ asset: address })} +/> + +
{ + event.preventDefault() + event.stopPropagation() + const final = v.safeParse(transferSchema, { + asset: $transferState.data?.asset, + amount: $transferState.data?.amount, + source: $transferState.data?.source, + receiver: $transferState.data?.receiver, + destination: $transferState.data?.destination, + }) + console.info(final) + if (!final.success) { + const issues = final.issues.map(issue => ({ path: issue.path?.at(0), ...issue })) + // @ts-ignore + errors.set(issues) + toast.error(JSON.stringify($errors, undefined, 2)) + } + }} +> + + header + +
+

From

+ + {$sourceChain?.display_name ?? 'Select chain'} + +
+ +
+

to

+ + + {$destinationChain?.display_name ?? 'Select chain'} + +
+ + receiver + + debounce(() => updateParams({ receiver: `${event.target?.value}` }), 1_000)()} + /> + amount + + debounce(() => updateParams({ amount: `${event.target?.value}` }), 1_000)()} + /> +
+ + + + +
+
+ + diff --git a/app/src/routes/transfer-new/validation.ts b/app/src/routes/transfer-new/validation.ts new file mode 100644 index 0000000000..3ea85bc7ec --- /dev/null +++ b/app/src/routes/transfer-new/validation.ts @@ -0,0 +1,74 @@ +import { + evmChainId, + aptosChainId, + cosmosChainId, + isValidEvmAddress, + isValidBech32Address +} from "@unionlabs/client" +import * as v from "valibot" +import { isHex } from "viem" + +const chainId = [...evmChainId, ...cosmosChainId, ...aptosChainId] + +export const transferSchema = v.pipe( + v.object({ + source: v.pipe( + v.string(), + v.trim(), + v.picklist(chainId, "Invalid source chain id"), + v.title("Source") + ), + destination: v.pipe( + v.string(), + v.trim(), + v.picklist(chainId, "Invalid destination chain id"), + v.title("Destination") + ), + receiver: v.pipe( + v.string(), + v.trim(), + v.title("Receiver"), + v.description("Receiver must be a valid address") + ), + asset: v.pipe( + v.string(), + v.trim(), + v.title("Asset"), + v.description("Asset must be a valid asset contract address") + ), + amount: v.pipe( + v.string(), + v.trim(), + v.title("Amount"), + v.description("Amount must be a valid number") + ) + }), + v.forward( + v.partialCheck( + [["destination"], ["receiver"]], + input => { + if (aptosChainId.includes(input.destination)) return isHex(input.receiver) + if (evmChainId.includes(input.destination)) return isValidEvmAddress(input.receiver) + if (cosmosChainId.includes(input.destination)) return isValidBech32Address(input.receiver) + return false + }, + "`receiver` must be a valid address for the selected destination chain" + ), + ["receiver"] + ) +) + +export type TransferSchema = v.InferOutput + +// const test: TransferSchema = { +// source: "11155111", +// destination: "stride-internal-1", +// asset: "0x0000000000000000000000000000000000000000", +// receiver: "0x0000000000000000000000000000000000000000", +// amount: 4 +// } + +// const parsed = await v.safeParseAsync(transferSchema, test) +// if (!parsed.success) { +// console.log(parsed.issues.map(x => x.path?.[0].key)) +// } diff --git a/app/src/routes/transfer/state-machine.ts b/app/src/routes/transfer/state-machine.ts deleted file mode 100644 index f668d51809..0000000000 --- a/app/src/routes/transfer/state-machine.ts +++ /dev/null @@ -1 +0,0 @@ -export type {} diff --git a/flake.lock b/flake.lock index 6acd73eb00..70e5ee2d1e 100644 --- a/flake.lock +++ b/flake.lock @@ -398,11 +398,11 @@ }, "nixpkgs-js": { "locked": { - "lastModified": 1732014248, - "narHash": "sha256-y/MEyuJ5oBWrWAic/14LaIr/u5E0wRVzyYsouYY3W6w=", + "lastModified": 1733212471, + "narHash": "sha256-M1+uCoV5igihRfcUKrr1riygbe73/dzNnzPsmaLCmpo=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "23e89b7da85c3640bbc2173fe04f4bd114342367", + "rev": "55d15ad12a74eb7d4646254e13638ad0c4128776", "type": "github" }, "original": { diff --git a/typescript-sdk/bun.lockb b/typescript-sdk/bun.lockb index 218330d8c4..40e3ec3d52 100755 Binary files a/typescript-sdk/bun.lockb and b/typescript-sdk/bun.lockb differ diff --git a/typescript-sdk/package.json b/typescript-sdk/package.json index 759fe6d390..2afe38bec6 100644 --- a/typescript-sdk/package.json +++ b/typescript-sdk/package.json @@ -29,32 +29,32 @@ "check-package": "deno run --allow-all npm:publint --strict && deno run --allow-all npm:@arethetypeswrong/cli@latest --pack --ignore-rules 'cjs-resolves-to-esm'" }, "dependencies": { - "@aptos-labs/ts-sdk": "^1.33.0", + "@aptos-labs/ts-sdk": "^1.33.1", "@cosmjs/amino": "^0.32.4", "@cosmjs/cosmwasm-stargate": "0.32.4", "@cosmjs/proto-signing": "^0.32.4", "@cosmjs/stargate": "0.32.4", "@cosmjs/tendermint-rpc": "^0.32.4", - "@scure/base": "^1.1.9", + "@scure/base": "^1.2.1", "neverthrow": "^8.1.1", "ofetch": "^1.4.1", - "ox": "^0.2.2", + "ox": "^0.4.0", "patch-package": "^8.0.0", - "viem": "^2.21.48" + "viem": "^2.21.53" }, "devDependencies": { "@total-typescript/ts-reset": "^0.6.1", - "@types/bun": "^1.1.13", - "@types/node": "^22.9.1", + "@types/bun": "^1.1.14", + "@types/node": "^22.10.1", "consola": "^3.2.3", "cosmjs-types": "^0.9.0", "jsr": "^0.13.2", - "knip": "^5.37.1", + "knip": "^5.39.1", "tsup": "^8.3.5", "tsx": "^4.19.2", - "typescript": "^5.6.3", + "typescript": "^5.7.2", "vite-tsconfig-paths": "^5.1.3", - "vitest": "^2.1.5" + "vitest": "^2.1.8" }, "repository": { "type": "git",