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
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const ConnectWalletButton: FC<Props> = ({ provider, onConnect }) => {
<button typeof="button" onClick={handleConnect} type="button" className={`py-5 px-6 bg-secondary-700 hover:bg-secondary-600 transition-colors duration-200 rounded-xl ${isLoading && 'cursor-progress opacity-80'}`}>
<div className="flex flex-row justify-between gap-9 items-stretch">
<ResolveConnectorIcon
connector={provider.name}
connector={provider.id}
iconClassName="w-10 h-10 p-0.5 rounded-lg bg-secondary-800 border border-secondary-400"
className="grid grid-cols-2 gap-1 min-w-fit"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ const OptionSelect = ({ onPasskeyLogin, goToStep, onConnectFinish }: {
const selectWallet = async () => {
if (connectedWallets.length < 1) {
const wallet = await connect();
if (wallet && getRegisteredWalletSignProviders().includes(wallet.providerName?.toLowerCase() ?? '')) {
const provider = providers.find(p => p.name === wallet?.providerName)
if (wallet && provider && getRegisteredWalletSignProviders().includes(provider.id.toLowerCase())) {
onConnectFinish(wallet);
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ const WalletSelect = ({ startWalletLogin }: WalletSelectProps) => {
type="button"
onClick={async () => {
const wallet = await connect();
if (wallet && getRegisteredWalletSignProviders().includes(wallet.providerName?.toLowerCase())) {
const provider = providers.find(p => p.name === wallet?.providerName)
if (wallet && provider && getRegisteredWalletSignProviders().includes(provider.id.toLowerCase())) {
startWalletLogin(wallet);
}
}}
Expand Down
3 changes: 2 additions & 1 deletion apps/app/components/Swap/AtomicChat/Actions/UserActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ export const UserLockAction: FC<UserCommitActionProps> = ({ quote, type }) => {
}
}
catch (e) {
setError({ message: e.details || e.message })
console.error('[UserLock] failed', e?.message ?? String(e), ...(e?.logs ? [e.logs] : []))
setError({ message: e?.details || e?.message || e?.code || e?.name || 'Unknown error' })
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export const MultichainConnectorPicker: FC<MultichainConnectorModalProps> = ({ s
/>
}
<p>
{connector?.providerName}
{provider?.name}
</p>
</button>
)
Expand Down
2 changes: 1 addition & 1 deletion apps/app/context/evmConnectorsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export function EvmConnectorsProvider({ children }) {

const initialRecentConnectors = useMemo(() => {
const evmRecentConnectors = recentConnectors.filter(c =>
c.providerName === 'eip155'
c.providerName === 'EVM'
&& c.connectorName
&& !featuredWalletsIds.includes(c.connectorName.toLowerCase())
)
Expand Down
13 changes: 11 additions & 2 deletions apps/app/context/walletLoginContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { deriveKeyFromEvmSignature } from '@/lib/htlc/secretDerivation/walletSig
import { deriveKeyFromWallet } from '@train-protocol/sdk'
import { deriveKeyFromStarknetSignature } from '@/lib/htlc/secretDerivation/walletSign/starknet'
import useWallet from '@/hooks/useWallet'
import { useWallet as useSolanaWallet } from '@solana/wallet-adapter-react'
interface WalletLoginContextValue {
deriveKey: (providerName: string, address: string) => Promise<Buffer>
}
Expand All @@ -15,12 +16,13 @@ export function WalletLoginProvider({ children }: { children: ReactNode }) {
const evmConfig = useConfig()
const { getWallet: getAztecWallet } = useAztecWalletContext()
const { wallets } = useWallet()
const { wallets: solanaAdapterWallets } = useSolanaWallet()

const deriveKey = useCallback(
async (providerName: string, address: string): Promise<Buffer> => {
const provider = providerName.toLowerCase()

if (provider === 'eip155') {
if (provider === 'evm') {
return deriveKeyFromEvmSignature(evmConfig, address as `0x${string}`)
}

Expand All @@ -41,9 +43,16 @@ export function WalletLoginProvider({ children }: { children: ReactNode }) {
return deriveKeyFromStarknetSignature(starknetAccount, address)
}

if (provider === 'solana') {
const connectedAdapter = solanaAdapterWallets.find(w => w.adapter.connected)?.adapter
const signMessage = connectedAdapter && 'signMessage' in connectedAdapter ? (msg: Uint8Array) => (connectedAdapter as any).signMessage(msg) : undefined
if (!signMessage) throw new Error('Solana wallet is not connected')
return deriveKeyFromWallet('solana', { wallet: { signMessage } })
}

throw new Error(`Unsupported wallet provider for login: ${providerName}`)
},
[evmConfig, getAztecWallet],
[evmConfig, getAztecWallet, wallets, solanaAdapterWallets],
)

return (
Expand Down
41 changes: 41 additions & 0 deletions apps/app/helpers/getSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,31 @@ export async function getServerSideProps(context) {
} as any)
}

// Inject Solana devnet if the API doesn't return it
const hasSolanaDevnet = resolvedNetworks.some(n => n.caip2Id === KnownInternalNames.Networks.SolanaDevnet)
if (!hasSolanaDevnet) {
const solanaMock = mockData.data.find(n => n.caip2Id === KnownInternalNames.Networks.SolanaDevnet)
resolvedNetworks.push({
caip2Id: KnownInternalNames.Networks.SolanaDevnet,
displayName: "Solana Devnet",
chainId: 'devnet',
nativeTokenAddress: null,
type: { name: "solana" },
logoUrl: 'https://raw.githubusercontent.com/TrainProtocol/icons/main/networks/solana.png',
tokens: [{
symbol: "SOL",
contractAddress: null,
decimals: 9,
priceInUsd: prices["solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:So11111111111111111111111111111111111111112"]
?? prices["SOLANA_MAINNET:So11111111111111111111111111111111111111112"]
?? 150,
}],
nodes: solanaMock?.nodes ?? [],
contracts: (solanaMock?.contracts as NetworkContract[]) ?? [],
metadata: [],
} as any)
}

const settings = {
networks: resolvedNetworks,
}
Expand Down Expand Up @@ -182,6 +207,22 @@ const mockData = {
"address": "0x2b9192d4571cceb33c689f750bcf380a7baae350846cc55616a278523cfd0dfc"
}
],
},
{
"caip2Id": KnownInternalNames.Networks.SolanaDevnet,
"nodes": [
{
"providerName": "solana-devnet",
"url": "https://api.devnet.solana.com",
"protocol": "Http"
}
],
"contracts": [
{
"type": "Train",
"address": "6zasug6x5AY93zNVjPZPGoqQfdTBd3C1w6CU9NDKtNH8"
}
],
}
]
}
24 changes: 22 additions & 2 deletions apps/app/hooks/htlc/useHTLCWriteClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import { useCallback } from 'react'
import { useConfig } from 'wagmi'
import { getWalletClient } from 'wagmi/actions'
import { getConnections } from '@wagmi/core'
import { createHTLCClient as createClient, getRegisteredNamespaces, IHTLCClient, TrainApiClient as SdkTrainApiClient } from '@train-protocol/sdk'
import { createHTLCClient as createClient, IHTLCClient, TrainApiClient as SdkTrainApiClient } from '@train-protocol/sdk'
import type { EvmSigner } from '@train-protocol/evm'
import type { AztecSigner } from '@train-protocol/aztec'
import type { SolanaSigner } from '@train-protocol/solana'
import { useWallet, useConnection } from '@solana/wallet-adapter-react'
import type { StarknetSigner } from '@train-protocol/starknet'
import { Network } from '../../Models/Network'
import { Wallet } from '@/Models/WalletProvider'
Expand All @@ -20,11 +22,29 @@ export function useHTLCWriteClient() {
const config = useConfig()
const getEffectiveRpcUrls = useRpcConfigStore(s => s.getEffectiveRpcUrls)
const { wallet: aztecWallet, accountAddress: aztecAccountAddress } = useAztecWalletContext()
const { connection: solanaConnection } = useConnection()
const { wallets: solanaWallets } = useWallet()

return useCallback(async (network: Network, wallet?: Wallet): Promise<IHTLCClient> => {
const chainType = network.caip2Id.split(':')[0]
const rpcUrl = getEffectiveRpcUrls(network)[0] ?? network.nodes?.[0]?.url ?? ''

// Solana chain path
if (chainType === 'solana') {
let signer: SolanaSigner | undefined
const connectedWallet = solanaWallets.find(w => w.adapter.connected)
const connectedPublicKey = connectedWallet?.adapter.publicKey
if (connectedWallet && connectedPublicKey) {
signer = {
publicKey: connectedPublicKey.toBase58(),
sendTransaction: async (tx) => connectedWallet.adapter.sendTransaction(tx as any, solanaConnection),
}
} else {
console.error('[useHTLCWriteClient] Solana signer unavailable', { hasPubkey: !!connectedPublicKey })
}
return createClient(chainType, { rpcUrl, signer, apiClient })
}

// Aztec chain path
if (chainType === 'aztec') {
let signer: AztecSigner | undefined
Expand Down Expand Up @@ -96,5 +116,5 @@ export function useHTLCWriteClient() {
}

return createClient(chainType, { rpcUrl, signer, apiClient })
}, [config, getEffectiveRpcUrls, aztecWallet, aztecAccountAddress])
}, [config, getEffectiveRpcUrls, aztecWallet, aztecAccountAddress, solanaWallets, solanaConnection])
}
1 change: 0 additions & 1 deletion apps/app/hooks/htlc/useUserLockPolling.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useEffect, useRef } from "react"
import useSWR from "swr"
import { Network, Token } from "../../Models/Network"
import { LockDetails } from "../../Models/phtlc/PHTLC"
Expand Down
27 changes: 27 additions & 0 deletions apps/app/hooks/useFee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,33 @@ export function useQuoteData(formValues: Props | undefined, refreshInterval?: nu
setLoading(true)
}

// Mock quote for Solana devnet — real API doesn't support it yet
const urlParams = new URLSearchParams(url.split('?')[1])
if (urlParams.get('sourceNetwork')?.startsWith('solana:')) {
setKey(url)
setLoading(false)
const amount = urlParams.get('amount') ?? '1000000000'
return {
quote: {
signature: 'mock-solana-devnet-quote',
totalFee: '5000000',
receiveAmount: String(BigInt(amount) * 95n / 100n),
sourceSolverAddress: '9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM',
destinationSolverAddress: '0x0000000000000000000000000000000000000001',
quoteExpirationTimestampInSeconds: Math.floor(Date.now() / 1000) + 3600,
route: {
source: { networkSlug: urlParams.get('sourceNetwork')!, tokenSymbol: 'SOL', tokenContract: '', tokenDecimals: 9 },
destination: { networkSlug: urlParams.get('destinationNetwork')!, tokenSymbol: 'ETH', tokenContract: '', tokenDecimals: 18 },
minAmountInSource: '10000000',
maxAmountInSource: '10000000000000',
},
timelock: { timelockTimeSpanInSeconds: 69 },
reward: { amount: '0', rewardTimelockTimeSpanInSeconds: 3600, rewardToken: '', rewardRecipientAddress: '' },
},
solverId: 'mock-solver',
}
}

const response = await apiClient.fetcher(url) as { data?: AggregatedQuoteResponse; error?: { message: string } }

setKey(url)
Expand Down
6 changes: 6 additions & 0 deletions apps/app/lib/NetworkSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const sourceOrder = [
KnownInternalNames.Networks.BNBChainMainnet,
KnownInternalNames.Networks.OptimismMainnet,
KnownInternalNames.Networks.SolanaMainnet,
KnownInternalNames.Networks.SolanaDevnet,
KnownInternalNames.Networks.ZksyncEraMainnet,
KnownInternalNames.Networks.PolygonMainnet,
KnownInternalNames.Networks.AvalancheMainnet,
Expand Down Expand Up @@ -117,6 +118,11 @@ export default class NetworkSettings {
ChainId: 'aztec-devnet',
TransactionExplorerTemplate: 'https://aztecexplorer.xyz/tx/{0}',
};
NetworkSettings.KnownSettings[KnownInternalNames.Networks.SolanaDevnet] = {
ChainId: 'devnet',
TransactionExplorerTemplate: 'https://explorer.solana.com/tx/{0}?cluster=devnet',
AccountExplorerTemplate: 'https://explorer.solana.com/address/{0}?cluster=devnet',
};


for (var k in NetworkSettings.KnownSettings) {
Expand Down
2 changes: 2 additions & 0 deletions apps/app/lib/gases/gasResolver.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@

import { GasProps } from "../../Models/Balance";
import { EVMGasProvider } from "./providers/evmGasProvider";
import { SolanaGasProvider } from "./providers/solanaGasProvider";
import { StarknetGasProvider } from "./providers/starknetGasProvider";

export class GasResolver {
private providers = [
new EVMGasProvider(),
new SolanaGasProvider(),
new StarknetGasProvider(),
];

Expand Down
94 changes: 67 additions & 27 deletions apps/app/lib/gases/providers/solanaGasProvider.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,84 @@
import { GasProps } from "../../../Models/Balance";
import { Network, getNativeToken } from "../../../Models/Network";
import { Network, getNativeToken, NetworkContractType } from "../../../Models/Network";
import { formatUnits } from "viem";
import KnownInternalNames from "../../knownIds";

export class SolanaGasProvider {
supportsNetwork(network: Network): boolean {
return KnownInternalNames.Networks.SolanaMainnet.includes(network.caip2Id)
return network.caip2Id.toLowerCase().startsWith('solana')
}

getGas = async ({ address, network, token }: GasProps) => {
if (!address)
return
const { PublicKey, Connection } = await import("@solana/web3.js");

const walletPublicKey = new PublicKey(address)
if (!address) return

const connection = new Connection(
`${network.nodes?.[0]?.url}`,
"confirmed"
);
const atomicContract = network.contracts?.find(c => c.type === NetworkContractType.Train)?.address
if (!atomicContract) return

if (!walletPublicKey) return
const nativeToken = getNativeToken(network)
if (!nativeToken) return

try {
const transactionBuilder = ((await import("../../wallets/solana/transactionBuilder")).transactionBuilder);

const transaction = await transactionBuilder(network, token, walletPublicKey)
const lamports = await estimateSolanaGas({
rpcUrl: network.nodes?.[0]?.url ?? '',
contractAddress: atomicContract,
address,
tokenSymbol: token.symbol,
tokenContractAddress: token.contractAddress,
decimals: token.decimals ?? 6,
})

const nativeToken = getNativeToken(network)
const gas = lamports ? Number(formatUnits(BigInt(lamports), nativeToken.decimals)) : undefined
return gas !== undefined ? { gas, token: nativeToken } : undefined
} catch (e) {
console.error(e)
}
}
}

if (!transaction || !nativeToken) return
async function estimateSolanaGas(params: {
rpcUrl: string
contractAddress: string
address: string
tokenSymbol: string
tokenContractAddress?: string | null
decimals: number
}): Promise<number | undefined> {
const { Connection, PublicKey } = await import('@solana/web3.js')
const { AnchorProvider, Program } = await import('@coral-xyz/anchor')
const { userLockTransactionBuilder, TrainHtlc } = await import('@train-protocol/solana')

const message = transaction.compileMessage();
const result = await connection.getFeeForMessage(message)
const connection = new Connection(params.rpcUrl, 'confirmed')
const walletPublicKey = new PublicKey(params.address)
const wallet = {
publicKey: walletPublicKey,
signTransaction: async (tx: any) => tx,
signAllTransactions: async (txs: any) => txs,
}
const provider = new AnchorProvider(connection, wallet as any, AnchorProvider.defaultOptions())
const program = new Program(TrainHtlc(params.contractAddress), provider)

const formatedGas = result.value ? Number(formatUnits(BigInt(result.value), nativeToken.decimals)) : undefined
const { transaction } = await userLockTransactionBuilder({
connection,
program,
walletPublicKey,
hashlock: '0x' + Buffer.alloc(32).toString('hex'),
sourceChain: 'solana',
destinationChain: 'eip155:1',
destinationAsset: 'ETH',
destinationAddress: params.address,
destinationAmount: '1',
srcLpAddress: 'bD5zQpd6RkbNJDtW7cBf1mw6wHzFxZ71wPCMwAmMh6n',
sourceAsset: { symbol: params.tokenSymbol, contractAddress: params.tokenContractAddress ?? '', decimals: params.decimals },
amount: '1',
decimals: params.decimals,
timelockDelta: 69,
quoteExpiry: Math.floor(Date.now() / 1000) + 3600,
rewardAmount: '0',
rewardToken: '',
rewardRecipient: '',
rewardTimelockDelta: 34,
} as any)

return formatedGas
}
catch (e) {
console.log(e)
}
}
const message = transaction.compileMessage()
const result = await connection.getFeeForMessage(message)
return result.value ?? undefined
}
Loading