Skip to content
Merged
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
126 changes: 58 additions & 68 deletions assets/js/webapp/swapUtils.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// assets/js/webapp/swapUtils.js

// Helper to handle cancellations silently
// Helper to handle user cancellations
const handleCancel = (err) => {
const msg = err?.message || "";
if (msg.includes("rejected") || msg.includes("cancelled") || msg.includes("user declined")) {
console.log("[Ultra] Swap cancelled by user");
console.log("[Swap] Cancelled by user");
throw new Error("USER_REJECTED");
}
return false;
Expand All @@ -13,110 +13,96 @@ const handleCancel = (err) => {
// ==================== ULTRA SWAP (Primary) ====================
async function performUltraSwap(inputMint, outputMint, rawAmount, provider, connectedWallet) {
if (rawAmount < 100000) {
console.warn("[Ultra] Amount too small, skipping Ultra quote");
console.warn("[Ultra] Amount too small");
throw new Error("Amount too small — try a larger swap");
}

try {
console.log(`[Ultra] Requesting quote: ${rawAmount} from ${inputMint} to ${outputMint}`);
console.log(`[Ultra] Quote request: ${rawAmount} ${inputMint} ${outputMint}`);
const ultraRes = await fetch(
`https://lite-api.jup.ag/ultra/v1/order?inputMint=${inputMint}&outputMint=${outputMint}&amount=${rawAmount}&taker=${connectedWallet}`
);

if (!ultraRes.ok) throw new Error(`Ultra HTTP ${ultraRes.status}`);
if (!ultraRes.ok) {
const errText = await ultraRes.text().catch(() => '');
throw new Error(`Ultra Quote Failed ${ultraRes.status}: ${errText}`);
}

const quote = await ultraRes.json();
if (quote.error) throw new Error(`Ultra Quote Error: ${JSON.stringify(quote.error)}`);

if (quote.error) throw new Error(`Quote Error: ${JSON.stringify(quote.error)}`);
if (!quote.transaction) throw new Error("No transaction in Ultra response");

return await executeUltraTransaction(quote, provider);
} catch (error) {
if (!handleCancel(error)) console.error("[Ultra] Failed:", error.message);
throw error;
}
}

// ==================== SMART SIGNING WITH CLEVER DETECTION ====================
// ==================== SMART SIGNING (Mobile-Friendly) ====================
async function executeUltraTransaction(quote, providerParam) {
const connectionMethod = window.connectionMethod || localStorage.getItem('connection_method') || 'injected';
console.log(`[Ultra] Detected connection method: ${connectionMethod}`);
console.log(`[Ultra] Connection: ${connectionMethod} | Wallet: ${window.connectedWallet?.slice(0,8)}...`);

let signer = null;
let isEmbedded = false;

// Smart routing based on wallet type
if (connectionMethod === 'google' || connectionMethod === 'apple') {
// === EMBEDDED WALLETS (Google / Apple) ===
isEmbedded = true;
console.log("[Ultra] Embedded wallet detected — forcing SDK signer only");
// Google / Apple MUST use SDK
console.log("[Ultra] Embedded wallet detected → using SDK");
const sdk = await window.getPhantomSDK?.();
if (sdk?.solana) {
signer = sdk.solana;
console.log("✅ Using Phantom SDK signer (Google/Apple)");
}
} else {
// === INJECTED WALLETS (Phantom extension / app) ===
console.log("[Ultra] Injected wallet detected — prioritizing window.solana");

// Injected (Phantom extension / app) → prefer legacy
if (providerParam && typeof providerParam.signTransaction === 'function') {
signer = providerParam;
console.log("✅ Using legacy provider (injected)");
}
else if (window.solana?.isPhantom) {
console.log("✅ Using injected provider");
} else if (window.solana?.isPhantom) {
signer = window.solana;
console.log("✅ Using direct window.solana (injected)");
}
else {
// Last resort fallback for injected
console.log("✅ Using window.solana");
} else {
const sdk = await window.getPhantomSDK?.();
if (sdk?.solana) {
signer = sdk.solana;
console.log("✅ Using SDK signer as fallback for injected");
}
if (sdk?.solana) signer = sdk.solana;
}
}

if (!signer) {
throw new Error(`No valid signer found for connection type: ${connectionMethod}`);
}
if (!signer) throw new Error("No valid signer found for this wallet type");

// For embedded wallets we prefer the stable v1 proxy path
if (isEmbedded) {
console.log("[Ultra] Embedded wallet → using v1 proxy for reliable signing");
return await performV1Swap(
quote.inputMint || "So11111111111111111111111111111111111111112",
quote.outputMint,
quote.inAmount || quote.amount,
providerParam,
window.connectedWallet
try {
console.log("[Ultra] Deserializing transaction...");
const vtx = solanaWeb3.VersionedTransaction.deserialize(
Buffer.from(quote.transaction, 'base64')
);
}

// Injected path → try direct Jupiter Ultra execute first
try {
console.log("[Ultra] Injected → attempting direct Jupiter Ultra execute");
const vtx = solanaWeb3.VersionedTransaction.deserialize(Buffer.from(quote.transaction, 'base64'));
console.log("[Ultra] Signing transaction...");
const signed = await signer.signTransaction(vtx);

const signedTxBase64 = btoa(String.fromCharCode(...new Uint8Array(signed.serialize())));

// Safe base64 encoding
const signedTx = btoa(String.fromCharCode(...new Uint8Array(signed.serialize())));

const executeRes = await fetch('https://lite-api.jup.ag/ultra/v1/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
signedTransaction: signedTxBase64,
requestId: quote.requestId
body: JSON.stringify({
signedTransaction: signedTx,
requestId: quote.requestId
})
});

if (!executeRes.ok) throw new Error(`Ultra Execute HTTP ${executeRes.status}`);
const result = await executeRes.json();

if (!result.signature) throw new Error("Ultra execute did not return signature");
if (!executeRes.ok) {
const errText = await executeRes.text().catch(() => '');
throw new Error(`Ultra Execute 400: ${executeRes.status} ${errText}`);
}

console.log(`✅ SUCCESS (Ultra direct)! TX: ${result.signature}`);
const result = await executeRes.json();
console.log(`✅ Ultra Success! TX: ${result.signature}`);
return { success: true, txid: result.signature, router: "Ultra" };

} catch (ultraErr) {
console.warn("[Ultra] Direct Ultra execute failed, falling back to v1 proxy...", ultraErr.message);
if (!handleCancel(ultraErr)) {
} catch (error) {
console.warn("[Ultra] Direct execute failed, falling back to v1 proxy...", error.message);
if (!handleCancel(error)) {
return await performV1Swap(
quote.inputMint || "So11111111111111111111111111111111111111112",
quote.outputMint,
Expand All @@ -125,48 +111,52 @@ async function executeUltraTransaction(quote, providerParam) {
window.connectedWallet
);
}
throw ultraErr;
throw error;
}
}

// ==================== V1 PROXY FALLBACK ====================
// ==================== V1 PROXY FALLBACK (Most Reliable on Mobile) ====================
async function performV1Swap(inputMint, outputMint, rawAmount, provider, connectedWallet) {
try {
console.log("[V1 Proxy] Using fallback proxy");
console.log("[V1 Proxy] Using backend proxy for reliable signing");
const proxyRes = await fetch('https://giddy-key-swaps-production.up.railway.app/api/swap/v1', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ inputMint, outputMint, rawAmount, connectedWallet })
});

const data = await proxyRes.json();
if (!data.success) throw new Error(data.error || "v1 proxy failed");
if (!data.success) throw new Error(data.error || "Proxy failed");

const tx = solanaWeb3.VersionedTransaction.deserialize(
Buffer.from(data.swapTransaction, "base64")
);

const tx = solanaWeb3.VersionedTransaction.deserialize(Buffer.from(data.swapTransaction, "base64"));

// Re-detect signer for v1 path (respects Google/Apple vs injected)
// Re-resolve signer for v1 path
const connectionMethod = window.connectionMethod || localStorage.getItem('connection_method') || 'injected';
let signer = null;

if (connectionMethod === 'google' || connectionMethod === 'apple') {
const sdk = await window.getPhantomSDK?.();
signer = sdk?.solana || null;
signer = sdk?.solana;
} else {
signer = provider || window.solana || (await window.getPhantomSDK?.())?.solana;
}

if (!signer) throw new Error("No signer available for v1 fallback");

const result = await signer.signAndSendTransaction(tx);
console.log(`✅ v1 Proxy Success! TX: ${result.signature}`);
return { success: true, txid: result.signature, version: "v1" };

} catch (error) {
if (!handleCancel(error)) console.error("[V1 Proxy] Failed:", error.message);
throw error;
}
}

// Global Export
// Global exports
window.performUltraSwap = performUltraSwap;
window.performV1Swap = performV1Swap;

console.log("✅ swapUtils.js loaded — Smart detection active (Google/Apple → SDK only | Injected → window.solana priority)");
console.log("✅ swapUtils.js loaded — Mobile-optimized (Smart signer + Ultra + v1 fallback)");
31 changes: 29 additions & 2 deletions assets/js/webapp/wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,31 @@ function isMobileDevice() {
return /Android|iPhone|iPad|iPod|webOS|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}

// Get the best available signer for swaps (prioritizes window.solana for injected)
function getBestSigner() {
// 1. Legacy injected (best for Phantom mobile app + extension)
if (window.phantom?.solana) {
console.log("✅ Using window.phantom.solana (injected priority)");
return window.phantom.solana;
}
if (window.solana?.isPhantom) {
console.log("✅ Using window.solana");
return window.solana;
}

// 2. SDK fallback
if (window.getPhantomSDK) {
const sdk = window.getPhantomSDK(); // sync call since it's cached
if (sdk?.solana) {
console.log("✅ Using SDK signer as fallback");
return sdk.solana;
}
}

console.warn("⚠️ No signer found");
return null;
}

// ====================== PHANTOM WALLET INTEGRATION (SDK) ======================

// Main Wallet Dropdown (Add Wallet)
Expand Down Expand Up @@ -1254,7 +1279,8 @@ async function confirmValueLock() {
? "GsKuLQsKCEnfQxuk4icTEQjc11Av8WiqW31CxZqZpump"
: "8kQzvMELBQGSiFmrXqLuDSpYVLKkNoXE4bUQCC14wj3Z";

const result = await performUltraSwap(inputMint, outputMint, rawAmount, provider, connectedWallet);
// Use smart signer instead of old 'provider'
const result = await performUltraSwap(inputMint, outputMint, rawAmount, getBestSigner(), connectedWallet);
showTxSuccess(result);

setTimeout(() => { if (connectedWallet) updateWalletBalances(); }, 1500);
Expand Down Expand Up @@ -1315,7 +1341,8 @@ async function confirmGiddySwap() {
outputMint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
}

const result = await performUltraSwap(inputMint, outputMint, rawAmount, provider, connectedWallet);
// Use smart signer
const result = await performUltraSwap(inputMint, outputMint, rawAmount, getBestSigner(), connectedWallet);
showTxSuccess(result);

setTimeout(() => { if (connectedWallet) updateWalletBalances(); }, 2500);
Expand Down
Loading