Skip to content
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
372 changes: 356 additions & 16 deletions bun.lock

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,18 @@
"devDependencies": {
"@eslint/compat": "^1.2.5",
"@eslint/js": "^9.18.0",
"@playwright/test": "^1.55.0",
"@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^6.1.1",
"@tailwindcss/vite": "^4.0.0",
"@types/bun": "^1.3.8",
"@playwright/test": "^1.55.0",
"drizzle-kit": "^0.31.9",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-svelte": "^3.0.0",
"globals": "^16.0.0",
"husky": "^9.1.7",
"lint-staged": "^16.1.0",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.11",
Expand All @@ -42,14 +43,17 @@
"tailwindcss": "^4.0.0",
"typescript": "^5.0.0",
"typescript-eslint": "^8.20.0",
"lint-staged": "^16.1.0",
"vite": "^7.1.1"
},
"lint-staged": {
"*.{js,ts,svelte,css,md,json,html}": "prettier --write"
},
"dependencies": {
"@electric-sql/pglite": "^0.3.15",
"@solana/wallet-adapter-base": "^0.9.27",
"@solana/wallet-adapter-phantom": "^0.9.28",
"@solana/wallet-adapter-solflare": "^0.6.32",
"@solana/web3.js": "^1.98.4",
"@sveltejs/adapter-cloudflare": "^7.0.3",
"@web3-onboard/coinbase": "^2.4.2",
"@web3-onboard/core": "^2.24.1",
Expand All @@ -58,6 +62,7 @@
"@web3-onboard/zeal": "^2.1.1",
"axios": "^1.9.0",
"base64-js": "^1.5.1",
"bs58": "^6.0.0",
"drizzle-orm": "^0.45.1",
"rxjs": "^7.8.2",
"viem": "~2.45.1"
Expand Down
5 changes: 4 additions & 1 deletion src/lib/components/GetQuote.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@
inputTokens,
outputTokens = $bindable(),
account,
recipient,
mainnet
}: {
exclusiveFor: string;
useExclusiveForQuoteRequest?: boolean;
inputTokens: TokenContext[];
outputTokens: TokenContext[];
account: () => `0x${string}`;
recipient: () => `0x${string}` | undefined;
mainnet: boolean;
} = $props();

Expand All @@ -33,6 +35,7 @@
)
: undefined;

const receiver = recipient() ?? account();
const response = await orderServer.getQuotes({
user: account(),
userChain: inputTokens[0].token.chain,
Expand All @@ -47,7 +50,7 @@
}),
outputs: outputTokens.map(({ token }) => {
return {
receiver: account(),
receiver: receiver,
asset: token.address,
chain: token.chain,
amount: 0n
Expand Down
94 changes: 69 additions & 25 deletions src/lib/components/InputTokenModal.svelte
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
<script lang="ts">
import { chainMap, coinList, type Token } from "$lib/config";
import { chainMap, clients, coinList, type Token } from "$lib/config";
import FieldRow from "$lib/components/ui/FieldRow.svelte";
import FormControl from "$lib/components/ui/FormControl.svelte";
import InlineMetaField from "$lib/components/ui/InlineMetaField.svelte";
import { AssetSelection } from "$lib/libraries/assetSelection";
import store, { type TokenContext } from "$lib/state.svelte";
import { toBigIntWithDecimals } from "$lib/utils/convert";
import { type InteropableAddress, getInteropableAddress } from "$lib/utils/interopableAddresses";
import solanaWallet from "$lib/utils/solana-wallet.svelte";

const v = (num: number | null) => (num ? num : 0);
const formatBalance = (value: bigint, decimals: number) =>
Expand Down Expand Up @@ -77,6 +78,9 @@
active = false;
}

const hasClient = (chain: string) => chain in clients;
const isChainAvailable = (chain: string) => chain !== "solanaDevnet" || solanaWallet.connected;

const uniqueInputTokens = $derived([
...new Set(
coinList(store.mainnet)
Expand Down Expand Up @@ -114,12 +118,16 @@
}
return 0;
}
const balancePromises = selectedIndices.map(
(i) =>
(store.intentType === "compact" ? store.compactBalances : store.balances)[tokens[i].chain][
tokens[i].address
]
);
const balancePromises = selectedIndices.map((i) => {
const token = tokens[i];
if (token.chain === "solanaDevnet") {
return store.solanaBalances.solanaDevnet?.[token.address] ?? Promise.resolve(0n);
}
if (!hasClient(token.chain)) return Promise.resolve(0n);
return (store.intentType === "compact" ? store.compactBalances : store.balances)[token.chain][
token.address
];
});
const balances = await Promise.all(balancePromises);

const goal = toBigIntWithDecimals(total, tokens[0].decimals);
Expand Down Expand Up @@ -211,29 +219,65 @@
<div>
{#each tokenSet as tkn, rowIndex}
{@const iaddr = iaddrFor(tkn)}
{@const evmChain = hasClient(tkn.chain)}
{@const chainAvailable = isChainAvailable(tkn.chain)}
<FieldRow columns={rowColumns} striped index={rowIndex}>
<div class="truncate text-xs font-medium text-gray-700">{tkn.chain}</div>
{#await (store.intentType === "compact" ? store.compactBalances : store.balances)[tkn.chain][tkn.address]}
<InlineMetaField
bind:value={inputs[iaddr]}
metaText="..."
disabled={!isEnabled(iaddr)}
/>
{:then balance}
<InlineMetaField
bind:value={inputs[iaddr]}
metaText={formatBalance(balance, tkn.decimals)}
disabled={!isEnabled(iaddr)}
/>
{:catch _}
<div class="truncate text-xs font-medium text-gray-700">
{tkn.chain}
</div>
{#if evmChain}
{#await (store.intentType === "compact" ? store.compactBalances : store.balances)[tkn.chain][tkn.address]}
<InlineMetaField
bind:value={inputs[iaddr]}
metaText="..."
disabled={!isEnabled(iaddr) || !chainAvailable}
/>
{:then balance}
<InlineMetaField
bind:value={inputs[iaddr]}
metaText={formatBalance(balance, tkn.decimals)}
disabled={!isEnabled(iaddr) || !chainAvailable}
/>
{:catch _}
<InlineMetaField
bind:value={inputs[iaddr]}
metaText="err"
disabled={!isEnabled(iaddr) || !chainAvailable}
/>
{/await}
{:else if store.solanaBalances.solanaDevnet?.[tkn.address]}
{#await store.solanaBalances.solanaDevnet[tkn.address]}
<InlineMetaField
bind:value={inputs[iaddr]}
metaText="..."
disabled={!isEnabled(iaddr) || !chainAvailable}
/>
{:then balance}
<InlineMetaField
bind:value={inputs[iaddr]}
metaText={formatBalance(balance, tkn.decimals)}
disabled={!isEnabled(iaddr) || !chainAvailable}
/>
{:catch _}
<InlineMetaField
bind:value={inputs[iaddr]}
metaText="err"
disabled={!isEnabled(iaddr) || !chainAvailable}
/>
{/await}
{:else}
<InlineMetaField
bind:value={inputs[iaddr]}
metaText="err"
disabled={!isEnabled(iaddr)}
metaText=""
disabled={!isEnabled(iaddr) || !chainAvailable}
/>
{/await}
{/if}
<div class="flex justify-center">
<input type="checkbox" bind:checked={enabledByToken[iaddr]} />
<input
type="checkbox"
bind:checked={enabledByToken[iaddr]}
disabled={!chainAvailable}
/>
</div>
</FieldRow>
{/each}
Expand Down
59 changes: 59 additions & 0 deletions src/lib/components/SolanaWalletButton.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<script lang="ts">
import { AVAILABLE_WALLETS } from "$lib/utils/solana-wallet.svelte";
import solanaWallet from "$lib/utils/solana-wallet.svelte";

let selectedWalletIndex = $state(0);
let connecting = $state(false);
let error = $state<string | null>(null);

const truncate = (key: string) => `${key.slice(0, 4)}…${key.slice(-4)}`;

async function connect() {
error = null;
connecting = true;
try {
await solanaWallet.connect(AVAILABLE_WALLETS[selectedWalletIndex].adapter);
} catch (e: unknown) {
error = e instanceof Error ? e.message : "Connection failed";
} finally {
connecting = false;
}
}

async function disconnect() {
await solanaWallet.disconnect();
error = null;
}
</script>

{#if solanaWallet.connected && solanaWallet.publicKey}
<div class="flex items-center gap-1">
<span class="rounded bg-green-50 px-2 py-0.5 text-xs font-medium text-green-700">
◎ {truncate(solanaWallet.publicKey)}
</span>
<button class="cursor-pointer text-xs text-gray-400 hover:text-red-500" onclick={disconnect}>
</button>
</div>
{:else}
<div class="flex items-center gap-1">
<select
class="rounded border border-gray-200 bg-white px-1 py-0.5 text-xs text-gray-700"
bind:value={selectedWalletIndex}
>
{#each AVAILABLE_WALLETS as wallet, i (wallet.name)}
<option value={i}>{wallet.name}</option>
{/each}
</select>
<button
class="cursor-pointer rounded border border-gray-200 bg-white px-2 py-0.5 text-xs font-medium text-gray-700 hover:border-sky-300 hover:text-sky-700 disabled:cursor-not-allowed disabled:text-gray-400"
disabled={connecting}
onclick={connect}
>
{connecting ? "Connecting…" : "Connect Solana"}
</button>
</div>
{#if error}
<p class="mt-0.5 text-xs text-red-500">{error}</p>
{/if}
{/if}
67 changes: 60 additions & 7 deletions src/lib/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createPublicClient, createWalletClient, custom, fallback, http } from "viem";
import { createPublicClient, createWalletClient, custom, defineChain, fallback, http } from "viem";
import { Connection } from "@solana/web3.js";
import {
arbitrum,
arbitrumSepolia,
Expand All @@ -16,6 +17,8 @@ import {
export const ADDRESS_ZERO = "0x0000000000000000000000000000000000000000" as const;
export const BYTES32_ZERO =
"0x0000000000000000000000000000000000000000000000000000000000000000" as const;

// --- EVM addresses --- //
export const COMPACT = "0x00000000000000171ede64904551eeDF3C6C9788" as const;
export const INPUT_SETTLER_COMPACT_LIFI = "0x0000000000cd5f7fDEc90a03a31F79E5Fbc6A9Cf" as const;
export const INPUT_SETTLER_ESCROW_LIFI = "0x000025c3226C00B2Cdc200005a1600509f4e00C0" as const;
Expand All @@ -26,6 +29,30 @@ export const MULTICHAIN_INPUT_SETTLER_COMPACT =
export const ALWAYS_OK_ALLOCATOR = "281773970620737143753120258" as const;
export const POLYMER_ALLOCATOR = "116450367070547927622991121" as const; // 0x02ecC89C25A5DCB1206053530c58E002a737BD11 signing by 0x934244C8cd6BeBDBd0696A659D77C9BDfE86Efe6
export const COIN_FILLER = "0x0000000000eC36B683C2E6AC89e9A75989C22a2e" as const;

// --- Solana addresses --- //
const solanaDevnet = defineChain({
id: 11,
name: "Solana Devnet",
nativeCurrency: { name: "SOL", symbol: "SOL", decimals: 9 },
rpcUrls: {
default: { http: ["https://api.devnet.solana.com"] }
},
testnet: true
});
export const solanaDevnetConnection = new Connection("https://api.devnet.solana.com", "confirmed");

// catalyst-intent-svm program IDs (devnet) — from Anchor.toml
export const SOLANA_INTENTS_PROTOCOL = "4SQaweUpT1LrRg1gh9sVEDL3Q4jZH3wxRi3qpz23SRpj" as const;
export const SOLANA_OUTPUT_SETTLER_SIMPLE = "8yt6Q3Gj8QCAqRVHQULckMf4rWSKQgN6SKVy9QTY5uWe" as const;
export const SOLANA_INPUT_SETTLER_ESCROW = "HyfCubUzStNcbAhW94PHPwRMqz2FTo9ox5HXxu2o6Ygq" as const;
export const SOLANA_POLYMER_ORACLE = "GjXkLKfMpz1MGDTFhKf31gXdcPAxPEFaEu85GmqQgLyL" as const;

// PDA(seed: "output_settler_simple") of the SOLANA_OUTPUT_SETTLER_SIMPLE program
export const SOLANA_OUTPUT_SETTLER_PDA =
"0xfef7041ed572ebef0bcb798166b921a7691e435b9e035e2236cc225e655bc237" as const;

// --- Oracles --- ///
export const WORMHOLE_ORACLE = {
ethereum: "0x0000000000000000000000000000000000000000",
arbitrum: "0x0000000000000000000000000000000000000000",
Expand All @@ -43,7 +70,9 @@ export const POLYMER_ORACLE = {
sepolia: "0x00d5b500ECa100F7cdeDC800eC631Aca00BaAC00",
baseSepolia: "0x00d5b500ECa100F7cdeDC800eC631Aca00BaAC00",
arbitrumSepolia: "0x00d5b500ECa100F7cdeDC800eC631Aca00BaAC00",
optimismSepolia: "0x00d5b500ECa100F7cdeDC800eC631Aca00BaAC00"
optimismSepolia: "0x00d5b500ECa100F7cdeDC800eC631Aca00BaAC00",
// PDA(seed: "polymer") of the SOLANA_POLYMER_ORACLE program
solanaDevnet: "0xfd8c1179dcc56c06fe0e9363feadd964dd3fa13b75ce88f61dee3bfdade95af6"
} as const;

export type availableAllocators = typeof ALWAYS_OK_ALLOCATOR | typeof POLYMER_ALLOCATOR;
Expand All @@ -62,14 +91,16 @@ export const chainMap = {
katana,
megaeth,
bsc,
polygon
polygon,
solanaDevnet
} as const;

export const chains = Object.keys(chainMap) as (keyof typeof chainMap)[];
export type chain = (typeof chains)[number];
export const chainList = (mainnet: boolean) => {
if (mainnet == true) {
return ["ethereum", "base", "arbitrum", "megaeth", "katana", "polygon", "bsc"];
} else return ["sepolia", "optimismSepolia", "baseSepolia", "arbitrumSepolia"];
return ["ethereum", "base", "arbitrum", "megaeth", "katana", "polygon", "bsc", "solana"];
} else return ["sepolia", "optimismSepolia", "baseSepolia", "arbitrumSepolia", "solanaDevnet"];
};

export type balanceQuery = Record<chain, Record<`0x${string}`, Promise<bigint>>>;
Expand Down Expand Up @@ -260,6 +291,25 @@ export const coinList = (mainnet: boolean) => {
name: "weth",
chain: "arbitrumSepolia",
decimals: 18
},
{
address: ADDRESS_ZERO,
name: "sol",
chain: "solanaDevnet",
decimals: 9
},
{
// So11111111111111111111111111111111111111112
address: `0x069b8857feab8184fb687f634618c035dac439dc1aeb3b5598a0f00000000001`,
name: "wsol",
chain: "solanaDevnet",
decimals: 9
},
{
address: `0x3b442cb3912157f13a933d0134282d032b5ffecd01a2dbf1b7790608df002ea7`,
name: "usdc",
chain: "solanaDevnet",
decimals: 6
}
] as const;
};
Expand Down Expand Up @@ -300,7 +350,8 @@ export const polymerChainIds = {
megaeth: megaeth.id,
katana: katana.id,
bsc: bsc.id,
polygon: polygon.id
polygon: polygon.id,
solanaDevnet: solanaDevnet.id
} as const;

export type Verifier = "wormhole" | "polymer";
Expand Down Expand Up @@ -365,7 +416,9 @@ export function getOracle(verifier: Verifier, chain: chain) {
export function getClient(chainId: number | bigint | string) {
const chainName = getChainName(Number(chainId));
if (!chainName) new Error("Could not find chain");
return clients[chainName];
const client = clients[chainName as keyof typeof clients];
if (!client) throw new Error(`No RPC client for chain: ${chainName}`);
return client;
}

export const clients = {
Expand Down
Loading
Loading