diff --git a/.yarnrc.yml b/.yarnrc.yml index 793d73c89d3..443b520ba5d 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -2,7 +2,7 @@ nodeLinker: node-modules npmAuthToken: "${NPM_AUTH-fallback}" -npmRegistryServer: "https://registry.npmjs.org" +npmRegistryServer: "http://127.0.0.1:4873" plugins: - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs diff --git a/PR_11377_REVIEW.md b/PR_11377_REVIEW.md new file mode 100644 index 00000000000..b9db8b83632 --- /dev/null +++ b/PR_11377_REVIEW.md @@ -0,0 +1,220 @@ +# PR #11377 Review: Referral Dashboard and Registration + +**PR Link:** https://github.com/shapeshift/web/pull/11377 +**Branch:** referral-dashboard → develop +**Status:** Open +**Risk:** Low (behind feature flag) + +## Overview +This PR implements a comprehensive referral program with dashboard UI, code management, and tracking integration. It's feature-flagged (`VITE_FEATURE_REFERRAL`) and low-risk. + +--- + +## ✅ Strengths + +1. **Proper Feature Flag Implementation** - Correctly follows ShapeShift's feature flag pattern with environment variables, Redux state, and test mocks +2. **Good i18n Coverage** - All user-facing strings use translation keys in `main.json` +3. **Comprehensive UI Components** - Well-structured component hierarchy with proper separation of concerns +4. **Type Safety** - Good TypeScript types defined in `src/lib/referral/types.ts` +5. **Smart URL-based Tracking** - `useReferralCapture` hook automatically captures `?ref=CODE` from URLs +6. **Proper Error Handling** - API error handling with custom `ReferralApiError` class +7. **React Query Integration** - Uses `@tanstack/react-query` for data fetching and mutations with proper cache invalidation + +--- + +## ⚠️ Issues & Concerns + +### 🔴 Critical Issues (MUST FIX BEFORE MERGE) + +#### Issue #1: Missing Type Definitions +**Location:** `packages/swapper/src/types.ts:444-459` +**Problem:** `SwapperSpecificMetadata` type is missing three fields being assigned in `src/lib/tradeExecution.ts:182-185`: +- `portalsTransactionMetadata` +- `zrxTransactionMetadata` +- `bebopTransactionMetadata` + +**Impact:** TypeScript compilation will fail + +**Fix:** +```typescript +export type SwapperSpecificMetadata = { + chainflipSwapId: number | undefined + nearIntentsSpecific?: { + depositAddress: string + depositMemo?: string + timeEstimate: number + deadline: string + } + cowswapQuoteSpecific?: OrderQuoteResponse + portalsTransactionMetadata?: TradeQuoteStep['portalsTransactionMetadata'] + zrxTransactionMetadata?: TradeQuoteStep['zrxTransactionMetadata'] + bebopTransactionMetadata?: TradeQuoteStep['bebopTransactionMetadata'] + relayTransactionMetadata: RelayTransactionMetadata | undefined + relayerExplorerTxLink: string | undefined + relayerTxHash: string | undefined + stepIndex: SupportedTradeQuoteStepIndex + quoteId: string + streamingSwapMetadata: StreamingSwapMetadata | undefined +} +``` + +--- + +#### Issue #2: Incomplete Feature Flag in Test Mocks +**Location:** `src/test/mocks/store.ts` +**Problem:** Missing `Referral: false` in the `featureFlags` object +**Impact:** Test failures + +**Fix:** Add `Referral: false,` to the mock (after `AppRating` at line ~181) + +--- + +### 🟡 Code Quality Issues (SHOULD FIX) + +#### Issue #3: Type Duplication in ReferralCodesTable +**Location:** `src/components/Referral/ReferralCodesTable.tsx:16-19` +**Problem:** Defines local `ReferralCode` type that duplicates `src/lib/referral/types.ts:ReferralCode` + +**Fix:** +```typescript +import type { ReferralCode as ReferralCodeModel } from '@/lib/referral/types' + +type ReferralCode = Pick +``` + +--- + +#### Issue #4: Type Duplication in ReferralCodesManagementTable +**Location:** `src/components/Referral/ReferralCodesManagementTable.tsx:16-21` +**Problem:** Defines `ReferralCodeFull` that overlaps with shared type + +**Fix:** Use shared type from `src/lib/referral/types.ts` or `Pick<>` from it + +--- + +#### Issue #5: Component Duplication in Referral Page +**Location:** `src/pages/Referral/Referral.tsx:31-39` +**Problem:** Duplicates `ReferralHeader` component that already exists in `src/components/Referral/ReferralHeader.tsx` + +**Fix:** Remove local component and import the shared one + +--- + +#### Issue #6: Hardcoded Currency Formatting +**Location:** `src/components/Referral/ReferralStatsCards.tsx` +**Problem:** Hardcodes `$` + string concatenation (`${currentRewards ?? '0.00'}`) +- Doesn't follow app's fiat formatting conventions +- Breaks i18n for non-USD users + +**Current:** +```typescript +${currentRewards ?? '0.00'} +``` + +**Recommendation:** Use a shared fiat formatter component (like ``) or `Intl.NumberFormat` + +--- + +#### Issue #7: Fixed Width Not Responsive +**Location:** `src/components/Referral/ReferralCodeCard.tsx:43` +**Problem:** Uses `width='50%'` which may not work well on mobile + +**Fix:** +```typescript +width={{ base: 'full', md: '50%' }} +``` + +--- + +#### Issue #8: Date Type Inconsistency +**Location:** `src/lib/referral/types.ts` +**Problem:** Types use `Date | string` unions for `createdAt`/`expiresAt` which spreads parsing responsibility + +**Recommendation:** API types should use ISO `string`, convert to `Date` at the edge once + +--- + +#### Issue #9: Error Construction Pattern +**Location:** `src/lib/referral/api.ts:13-28` +**Problem:** Creates error then casts it rather than using proper class constructor + +**Current:** +```typescript +const apiError = new Error(message) as ReferralApiError +apiError.name = 'ReferralApiError' +apiError.code = code +apiError.statusCode = statusCode +throw apiError +``` + +**Fix:** +```typescript +throw new ReferralApiError(message, code, statusCode) +``` + +--- + +### 🔵 Performance & Best Practices (NICE TO HAVE) + +#### Issue #10: Missing Lazy Loading +**Location:** `src/pages/Fox/FoxEcosystemPage.tsx:11` +**Problem:** `ReferralDashboard` is imported directly but only used behind feature flag +**Impact:** Unnecessary bundle size when feature is disabled + +**Fix:** Lazy-load with `makeSuspenseful` pattern + +--- + +#### Issue #11: Skeleton Repetition +**Location:** `src/components/Referral/ReferralStatsCards.tsx:20-63` +**Problem:** Repeats identical skeleton Card 3 times + +**Fix:** Map over array to reduce duplication + +--- + +### 📋 Testing Gaps + +- No unit tests for referral hooks (`useReferral`, `useReferralCapture`) +- No tests for API error handling +- No tests for referral code generation logic + +--- + +### 🔒 Security Considerations + +✅ Owner address is sent to backend (hashed for privacy per comment in code) +✅ Referral code stored in localStorage - acceptable for this use case +✅ No sensitive data exposed in client + +--- + +## 🎯 Verdict + +**Status: DO NOT MERGE** + +Must fix Critical Issues #1 and #2 before merge - TypeScript compilation will fail without the missing type definitions. + +Once those are resolved, this is a solid feature implementation that follows ShapeShift's patterns well. The code quality issues are mostly minor refactoring opportunities that could be addressed in follow-up PRs or during this PR. + +--- + +## Action Plan + +### Phase 1: Critical Fixes (Required) +- [ ] Issue #1: Add missing type definitions to `SwapperSpecificMetadata` +- [ ] Issue #2: Add `Referral: false` to test mocks + +### Phase 2: Code Quality (Recommended) +- [ ] Issue #3: Remove type duplication in ReferralCodesTable +- [ ] Issue #4: Remove type duplication in ReferralCodesManagementTable +- [ ] Issue #5: Remove component duplication in Referral page +- [ ] Issue #6: Fix currency formatting to use app conventions +- [ ] Issue #7: Make ReferralCodeCard responsive + +### Phase 3: Optimizations (Optional) +- [ ] Issue #8: Normalize date types to ISO strings +- [ ] Issue #9: Use proper ReferralApiError constructor +- [ ] Issue #10: Lazy-load ReferralDashboard +- [ ] Issue #11: Reduce skeleton repetition +- [ ] Add unit tests for hooks diff --git a/package.json b/package.json index bf2161b80fa..9b7cdcdbae2 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "@metaplex-foundation/js": "^0.20.1", "@moralisweb3/common-evm-utils": "2.27.2", "@mysten/sui": "1.45.0", + "@noble/hashes": "^2.0.1", "@react-spring/web": "^9.7.4", "@reduxjs/toolkit": "^2.6.1", "@reown/walletkit": "^1.2.6", @@ -100,24 +101,24 @@ "@shapeshiftoss/chain-adapters": "workspace:^", "@shapeshiftoss/contracts": "workspace:^", "@shapeshiftoss/errors": "workspace:^", - "@shapeshiftoss/hdwallet-coinbase": "1.62.28", - "@shapeshiftoss/hdwallet-core": "1.62.28", - "@shapeshiftoss/hdwallet-gridplus": "1.62.28", - "@shapeshiftoss/hdwallet-keepkey": "1.62.28", - "@shapeshiftoss/hdwallet-keepkey-webusb": "1.62.28", - "@shapeshiftoss/hdwallet-keplr": "1.62.28", - "@shapeshiftoss/hdwallet-ledger": "1.62.28", - "@shapeshiftoss/hdwallet-ledger-webhid": "1.62.28", - "@shapeshiftoss/hdwallet-ledger-webusb": "1.62.28", - "@shapeshiftoss/hdwallet-metamask-multichain": "1.62.28", - "@shapeshiftoss/hdwallet-native": "1.62.28", - "@shapeshiftoss/hdwallet-native-vault": "1.62.28", - "@shapeshiftoss/hdwallet-phantom": "1.62.28", - "@shapeshiftoss/hdwallet-trezor": "1.62.28", - "@shapeshiftoss/hdwallet-trezor-connect": "1.62.28", - "@shapeshiftoss/hdwallet-vultisig": "1.62.28", - "@shapeshiftoss/hdwallet-walletconnect": "1.62.28", - "@shapeshiftoss/hdwallet-walletconnectv2": "1.62.28", + "@shapeshiftoss/hdwallet-coinbase": "1.62.29-bebop-solana-msg.6", + "@shapeshiftoss/hdwallet-core": "1.62.29-bebop-solana-msg.6", + "@shapeshiftoss/hdwallet-gridplus": "1.62.29-bebop-solana-msg.6", + "@shapeshiftoss/hdwallet-keepkey": "1.62.29-bebop-solana-msg.6", + "@shapeshiftoss/hdwallet-keepkey-webusb": "1.62.29-bebop-solana-msg.6", + "@shapeshiftoss/hdwallet-keplr": "1.62.29-bebop-solana-msg.6", + "@shapeshiftoss/hdwallet-ledger": "1.62.29-bebop-solana-msg.6", + "@shapeshiftoss/hdwallet-ledger-webhid": "1.62.29-bebop-solana-msg.6", + "@shapeshiftoss/hdwallet-ledger-webusb": "1.62.29-bebop-solana-msg.6", + "@shapeshiftoss/hdwallet-metamask-multichain": "1.62.29-bebop-solana-msg.6", + "@shapeshiftoss/hdwallet-native": "1.62.29-bebop-solana-msg.6", + "@shapeshiftoss/hdwallet-native-vault": "1.62.29-bebop-solana-msg.6", + "@shapeshiftoss/hdwallet-phantom": "1.62.29-bebop-solana-msg.6", + "@shapeshiftoss/hdwallet-trezor": "1.62.29-bebop-solana-msg.6", + "@shapeshiftoss/hdwallet-trezor-connect": "1.62.29-bebop-solana-msg.6", + "@shapeshiftoss/hdwallet-vultisig": "1.62.29-bebop-solana-msg.6", + "@shapeshiftoss/hdwallet-walletconnect": "1.62.29-bebop-solana-msg.6", + "@shapeshiftoss/hdwallet-walletconnectv2": "1.62.29-bebop-solana-msg.6", "@shapeshiftoss/swapper": "workspace:^", "@shapeshiftoss/types": "workspace:^", "@shapeshiftoss/unchained-client": "workspace:^", @@ -225,6 +226,7 @@ "@peculiar/webcrypto": "^1.3.3", "@testing-library/react": "^13.3.0", "@types/bip21": "^2.0.0", + "@types/bs58": "^5.0.0", "@types/cli-progress": "^3.11.5", "@types/d3-array": "^3.0.1", "@types/dompurify": "^2.3.2", diff --git a/packages/chain-adapters/package.json b/packages/chain-adapters/package.json index 5ceff024c73..b3f29507524 100644 --- a/packages/chain-adapters/package.json +++ b/packages/chain-adapters/package.json @@ -31,8 +31,8 @@ "dependencies": { "@mysten/sui": "1.45.0", "@shapeshiftoss/caip": "workspace:^", - "@shapeshiftoss/hdwallet-core": "1.62.28", - "@shapeshiftoss/hdwallet-ledger": "1.62.28", + "@shapeshiftoss/hdwallet-core": "1.62.29-bebop-solana-msg.6", + "@shapeshiftoss/hdwallet-ledger": "1.62.29-bebop-solana-msg.6", "@shapeshiftoss/types": "workspace:^", "@shapeshiftoss/unchained-client": "workspace:^", "@shapeshiftoss/utils": "workspace:^", diff --git a/packages/chain-adapters/src/solana/SolanaChainAdapter.ts b/packages/chain-adapters/src/solana/SolanaChainAdapter.ts index b3e2d6a5719..030d471da9d 100644 --- a/packages/chain-adapters/src/solana/SolanaChainAdapter.ts +++ b/packages/chain-adapters/src/solana/SolanaChainAdapter.ts @@ -368,6 +368,77 @@ export class ChainAdapter implements IChainAdapter } } + async signAndBroadcastPrebuiltTransaction({ + serializedTx, + wallet, + accountNumber, + senderAddress, + receiverAddress, + }: { + serializedTx: string + wallet: HDWallet + accountNumber: number + senderAddress: string + receiverAddress?: string + }): Promise { + try { + await Promise.all([ + assertAddressNotSanctioned(senderAddress), + receiverAddress !== CONTRACT_INTERACTION && + receiverAddress && + assertAddressNotSanctioned(receiverAddress), + ]) + + if (!wallet) throw new Error('wallet is required') + this.assertSupportsChain(wallet) + + const bebopTx = VersionedTransaction.deserialize(Buffer.from(serializedTx, 'base64')) + + console.log('[SolanaChainAdapter] Bebop tx before blockhash update:', JSON.stringify({ + numRequiredSignatures: bebopTx.message.header.numRequiredSignatures, + originalBlockhash: bebopTx.message.recentBlockhash, + preExistingSignatures: bebopTx.signatures.map((sig, i) => ({ + slot: i, + isEmpty: sig.every(b => b === 0), + first10Bytes: Array.from(sig.slice(0, 10)), + })), + })) + + const { blockhash } = await this.connection.getLatestBlockhash() + bebopTx.message.recentBlockhash = blockhash + + console.log('[SolanaChainAdapter] Updated blockhash:', JSON.stringify({ + newBlockhash: blockhash, + signaturesAfterBlockhashUpdate: bebopTx.signatures.map((sig, i) => ({ + slot: i, + isEmpty: sig.every(b => b === 0), + })), + })) + + if ((wallet as any).adapter?.signTransaction) { + const addressNList = toAddressNList(this.getBip44Params({ accountNumber })) + const signedTx = await (wallet as any).adapter.signTransaction(bebopTx, addressNList) + + console.log('[SolanaChainAdapter] Signed with Native wallet adapter:', JSON.stringify({ + totalSignatures: signedTx.signatures.filter((sig: Uint8Array) => !sig.every(b => b === 0)).length, + })) + + const serialized = Buffer.from(signedTx.serialize()).toString('base64') + const txHash = await this.providers.http.sendTx({ sendTxBody: { hex: serialized } }) + + console.log('[SolanaChainAdapter] Pre-built tx broadcast:', JSON.stringify({ txHash })) + + return txHash + } + + throw new Error('Pre-built transaction signing only supported for Native wallet') + } catch (err) { + return ErrorHandler(err, { + translation: 'chainAdapters.errors.signAndBroadcastTransaction', + }) + } + } + async signAndBroadcastTransaction({ senderAddress, receiverAddress, @@ -384,10 +455,31 @@ export class ChainAdapter implements IChainAdapter if (!wallet) throw new Error('wallet is required') this.assertSupportsChain(wallet) + console.log('[SolanaChainAdapter] Calling wallet.solanaSendTx with:', JSON.stringify({ + hasInstructions: !!txToSign.instructions, + instructionsCount: txToSign.instructions?.length, + hasAddressLookupTableAccountInfos: !!txToSign.addressLookupTableAccountInfos, + addressLookupTableAccountInfosCount: txToSign.addressLookupTableAccountInfos?.length, + addressLookupTableAccountInfos: txToSign.addressLookupTableAccountInfos?.map(info => ({ + key: info.key, + hasData: !!info.data, + dataType: typeof info.data, + isBuffer: Buffer.isBuffer(info.data), + dataConstructor: info.data?.constructor?.name, + })), + computeUnitLimit: txToSign.computeUnitLimit, + computeUnitPrice: txToSign.computeUnitPrice, + blockHash: txToSign.blockHash, + })) + const tx = await wallet.solanaSendTx?.(txToSign) if (!tx) throw new Error('error signing & broadcasting tx') + console.log('[SolanaChainAdapter] Transaction signed and broadcast:', JSON.stringify({ + signature: tx.signature, + })) + return tx.signature } catch (err) { return ErrorHandler(err, { @@ -407,8 +499,20 @@ export class ChainAdapter implements IChainAdapter receiverAddress !== CONTRACT_INTERACTION && assertAddressNotSanctioned(receiverAddress), ]) + console.log('[SolanaChainAdapter] Broadcasting transaction:', JSON.stringify({ + hexLength: hex.length, + hexPreview: hex.substring(0, 100) + '...', + senderAddress, + receiverAddress, + })) + const txHash = await this.providers.http.sendTx({ sendTxBody: { hex } }) + console.log('[SolanaChainAdapter] Broadcast result:', JSON.stringify({ + txHash, + txHashLength: txHash?.length, + })) + return txHash } catch (err) { if ((err as Error).name === 'ResponseError') { diff --git a/packages/swapper/package.json b/packages/swapper/package.json index 514e04dc413..219d96f2a51 100644 --- a/packages/swapper/package.json +++ b/packages/swapper/package.json @@ -39,7 +39,7 @@ "@shapeshiftoss/caip": "workspace:^", "@shapeshiftoss/chain-adapters": "workspace:^", "@shapeshiftoss/contracts": "workspace:^", - "@shapeshiftoss/hdwallet-core": "1.62.28", + "@shapeshiftoss/hdwallet-core": "1.62.29-bebop-solana-msg.6", "@shapeshiftoss/types": "workspace:^", "@shapeshiftoss/unchained-client": "workspace:^", "@shapeshiftoss/utils": "workspace:^", @@ -49,6 +49,7 @@ "@uniswap/v3-sdk": "^3.13.1", "axios": "^1.13.0", "axios-cache-interceptor": "^1.5.3", + "base-x": "^5.0.0", "bignumber.js": "^9.3.1", "eip-712": "^1.0.0", "ethers": "6.11.1", diff --git a/packages/swapper/src/swappers/BebopSwapper/BebopSwapper.ts b/packages/swapper/src/swappers/BebopSwapper/BebopSwapper.ts index 03af585b90b..0e667de07c9 100644 --- a/packages/swapper/src/swappers/BebopSwapper/BebopSwapper.ts +++ b/packages/swapper/src/swappers/BebopSwapper/BebopSwapper.ts @@ -1,6 +1,9 @@ import type { Swapper } from '../../types' -import { executeEvmTransaction } from '../../utils' +import { executeEvmTransaction, executeSolanaTransaction } from '../../utils' +import { executeSolanaMessage } from './executeSolanaMessage' export const bebopSwapper: Swapper = { executeEvmTransaction, + executeSolanaTransaction, + executeSolanaMessage, } diff --git a/packages/swapper/src/swappers/BebopSwapper/SOLANA_BLOCKER.md b/packages/swapper/src/swappers/BebopSwapper/SOLANA_BLOCKER.md new file mode 100644 index 00000000000..ea637cb1fdf --- /dev/null +++ b/packages/swapper/src/swappers/BebopSwapper/SOLANA_BLOCKER.md @@ -0,0 +1,106 @@ +# Bebop Solana Integration - BLOCKER IDENTIFIED + +## Issue + +Bebop Solana transactions **require 2 signatures** even in `gasless=false` mode: +1. User's wallet signature +2. Bebop's ephemeral key signature + +## Evidence + +### Transaction Analysis +Deserializing Bebop's `solana_tx` shows: +``` +Num required signatures: 2 +Account 0: ETpdrEkK8n3jPLysUZCNe1LHdM76GSrbHgAtVpCvWYLp (user) - signer +Account 1: 6ZUeThQ9FovzaS8HejAfGW2VqCXwRYPKKyUNsozpfrSx (Bebop) - signer +``` + +### API Behavior + +**gasless=true:** +- Status: `QUOTE_SUCCESS` +- `requiredSignatures`: `[]` +- Bebop handles everything +- `/v3/order` endpoint submits with both signatures + +**gasless=false:** +- Status: `QUOTE_INDIC_ROUTE` (indicative, not executable alone) +- Transaction still has 2 required signatures +- We can only provide 1 signature +- Transaction is invalid without Bebop's signature + +## Why This Doesn't Work + +ShapeShift's architecture requires **fully self-executable** transactions: +1. Get quote with transaction data +2. Sign with user's wallet +3. Broadcast directly to blockchain + +Bebop Solana requires a **hybrid flow**: +1. Get quote +2. Sign with user's wallet +3. Submit signature to Bebop's `/v3/order` endpoint +4. Bebop adds their signature +5. Bebop broadcasts + +## Comparison with EVM + +### Bebop EVM (works) +- `gasless=false` returns transaction with only user signature required +- We can broadcast directly +- True self-execution + +### Bebop Solana (doesn't work) +- `gasless=false` returns transaction requiring 2 signatures +- Cannot broadcast without Bebop's signature +- Not true self-execution + +## Potential Solutions + +### Option 1: Use Gasless Mode (Not Ideal) +- Set `gasless=true` +- Submit to `/v3/order` endpoint +- Bebop broadcasts (we don't control it) +- Doesn't match ShapeShift architecture +- Would need different execution flow + +### Option 2: Request Bebop to Add True Self-Execute +- Ask Bebop to support single-signature transactions for Solana +- Would match EVM behavior +- They may not be willing/able to do this due to their settlement model + +### Option 3: Don't Support Bebop Solana +- Keep Bebop for EVM chains only +- Use Jupiter for Solana swaps +- Simplest solution + +## Recommendation + +**Do not integrate Bebop Solana** until they provide true self-execute support (single-signature transactions). + +The current `gasless=false` mode is misleading - it's not actually self-executable. The `QUOTE_INDIC_ROUTE` status is a hint that these quotes are "indicative" and cannot be directly executed by us. + +## Test Commands + +### Verify signature requirements: +```bash +node -e " +const { VersionedTransaction } = require('@solana/web3.js'); +const tx = VersionedTransaction.deserialize( + Buffer.from(BEBOP_SOLANA_TX_BASE64, 'base64') +); +console.log('Required signatures:', tx.message.header.numRequiredSignatures); +" +``` + +### Check both modes: +```bash +# gasless=true +curl "https://api.bebop.xyz/pmm/solana/v3/quote?gasless=true&..." | jq .status +# Returns: QUOTE_SUCCESS + +# gasless=false +curl "https://api.bebop.xyz/pmm/solana/v3/quote?gasless=false&..." | jq .status +# Returns: QUOTE_INDIC_ROUTE (indicative only!) +``` diff --git a/packages/swapper/src/swappers/BebopSwapper/SOLANA_CO_SIGNING_INVESTIGATION.md b/packages/swapper/src/swappers/BebopSwapper/SOLANA_CO_SIGNING_INVESTIGATION.md new file mode 100644 index 00000000000..0dae8d63950 --- /dev/null +++ b/packages/swapper/src/swappers/BebopSwapper/SOLANA_CO_SIGNING_INVESTIGATION.md @@ -0,0 +1,240 @@ +# Bebop Solana Co-Signing Investigation & Next Steps + +## Current Status + +**Quotes work ✅ Fees display correctly ✅ Broadcast fails ❌** + +## Root Cause Analysis + +### The Multi-Signature Requirement + +Bebop Solana transactions require **2 signatures**: +1. **User's wallet** (taker) +2. **Bebop's ephemeral key** (`6ZUeThQ9FovzaS8HejAfGW2VqCXwRYPKKyUNsozpfrSx`) + +Evidence from transaction analysis: +```javascript +numRequiredSignatures: 2 +Account[0]: ETpdrEkK8n3jPLysUZCNe1LHdM76GSrbHgAtVpCvWYLp (user) - signer +Account[1]: 6ZUeThQ9FovzaS8HejAfGW2VqCXwRYPKKyUNsozpfrSx (Bebop) - signer +``` + +### What Happens When We Try Self-Execute + +When we rebuild the transaction from Bebop's instructions: +- The decompiled instruction STILL marks Bebop's account as `isSigner: true` +- We build a transaction requiring 2 signatures +- We can only provide 1 signature (user's) +- Transaction gets broadcast with missing signature +- Solana network silently rejects it (returns txHash but tx never confirms) + +**Both Phantom extension AND Native wallet exhibit the same behavior:** +- ✅ Simulation succeeds (transaction structure is valid) +- ✅ /send endpoint returns txHash +- ❌ Transaction never appears on-chain + +### Why gasless=false Doesn't Mean Self-Execute + +Looking at the API: +- `gasless=false` → `QUOTE_INDIC_ROUTE` status (indicative only) +- `gasless=true` → `QUOTE_SUCCESS` status (executable via /v3/order) + +The `gasless=false` parameter only affects: +- Whether gas fees are included in the price quote +- The status indicator in the response + +It does NOT change the transaction structure - both modes return a transaction requiring 2 signatures. + +## Comparison with EVM + +### EVM Bebop (Self-Execute Works) +``` +User signs transaction → Direct broadcast to network → Done +``` +- Transaction data from API is ready to broadcast +- No co-signing needed +- True self-execution + +### Solana Bebop (Self-Execute Doesn't Work) +``` +User signs → ??? → Transaction needs Bebop signature too → Fails +``` +- Transaction requires Bebop's signature +- Cannot broadcast directly +- NOT true self-execution + +## Potential Solution: Implement /v3/order Flow + +Similar to **CowSwap** pattern (sign message, submit to API, protocol broadcasts): + +### Flow Comparison + +**CowSwap (EVM):** +1. Get quote with EIP-712 order data +2. User signs EIP-712 message (off-chain) +3. Submit signature to CowSwap API +4. CowSwap matches and broadcasts +5. Track via order ID + +**Bebop Solana (Proposed):** +1. Get quote with `gasless=true` +2. User signs Solana transaction message +3. Submit signature to `/v3/order` endpoint +4. Bebop adds their signature +5. Bebop broadcasts +6. Track via returned txHash + +### API Endpoint + +**POST** `/v3/order` + +Request: +```json +{ + "quote_id": "121-xxx", + "signature": "" +} +``` + +Response: +```json +{ + "txHash": "...", + "status": "Pending", + "expiry": 1234567890 +} +``` + +### Implementation Requirements + +1. **Get unsigned Solana message to sign** + - Extract message bytes from `solana_tx` + - User signs just the message (not the full transaction) + +2. **Implement executeBebopSolanaMessage method** + - Similar to CowSwap's `executeEvmMessage` + - Signs the transaction message + - POSTs to `/v3/order` with signature + quote_id + - Returns txHash from Bebop's response + +3. **Update BebopSwapper** + - Add `executeSolanaMessage` to swapper interface (new type) + - Handle in execution flow + +4. **Add metadata to quote** + - Store `quoteId` in step metadata + - Store serialized transaction for signing + +## Open Questions + +### 1. What exactly does Bebop expect for "signature"? + +- The user's signature of the transaction message bytes? +- A signature of some off-chain order data (like EVM)? +- The full partially-signed transaction? + +**Need to test:** Get a real quote and try different signature formats + +### 2. Does Bebop's /v3/order actually work for Solana? + +- The OpenAPI schema is generic (same for all chains) +- The docs mention "EOA private key" (Ethereum term) +- May be EVM-only despite being in Solana API + +**Need to test:** Actually call `/v3/order` with valid data + +### 3. Is this architecture acceptable for ShapeShift? + +**Pros:** +- Similar to existing CowSwap pattern +- User still controls signing +- Bebop handles broadcast complexity + +**Cons:** +- Different from EVM Bebop (self-execute) +- We don't control the broadcast +- Adds API dependency for execution + +## Next Steps + +### Investigation Phase + +1. **Test /v3/order endpoint with real quote** + ```bash + # Get quote + curl /v3/quote?gasless=true → quote_id + solana_tx + + # Sign transaction message + const message = VersionedTransaction.deserialize(solana_tx).message + const signature = sign(message.serialize()) + + # Submit order + POST /v3/order { quote_id, signature: base58(signature) } + + # Check if it broadcasts successfully + ``` + +2. **Compare with CowSwap implementation** + - Read `/packages/swapper/src/swappers/CowSwapper/CowSwapper.ts` + - Understand how `executeEvmMessage` works + - Check how they handle the API submission + +3. **Verify Solana multi-sig transactions work** + - Research if other Solana protocols use this pattern + - Check if it's a common approach + - Validate this is production-ready + +### Implementation Phase (If Investigation Succeeds) + +1. **Add new execution method type** + - Define `executeSolanaMessage` in Swapper interface + - Similar to `executeEvmMessage` for CowSwap + +2. **Implement Bebop Solana message signing** + - Extract message from `solana_tx` + - Sign with wallet + - Convert to base58 + - POST to `/v3/order` + +3. **Update execution flow** + - Detect Bebop Solana in useTradeExecution + - Use message signing flow instead of transaction flow + - Handle Bebop's broadcast + +4. **Add order status tracking** + - Use `/v3/order-status` endpoint + - Map to our swap status + - Handle Bebop-specific states + +## Decision Point + +**Should we implement the /v3/order (gasless) flow?** + +**Arguments For:** +- Multi-sig transactions are legitimate Solana pattern +- CowSwap precedent shows API-based execution works +- Would enable Bebop Solana support +- User still controls signing + +**Arguments Against:** +- Significantly different from EVM Bebop +- More complex implementation +- Adds Bebop API dependency +- May confuse users (why is Solana different?) + +**Recommendation:** +Test the `/v3/order` flow first to confirm it actually works for Solana, then decide if the implementation complexity is worth it. + +## Related Files + +- `/packages/swapper/src/swappers/CowSwapper/CowSwapper.ts` - Message signing pattern +- `/packages/swapper/src/swappers/BebopSwapper/SOLANA_BLOCKER.md` - Current blocker doc +- `/packages/swapper/src/types.ts` - May need new execution type + +## Timeline + +- **Investigation:** ~2 hours (test /v3/order flow) +- **Implementation:** ~1 day (if investigation succeeds) +- **Testing:** ~0.5 day + +**Total:** ~2 days if we proceed diff --git a/packages/swapper/src/swappers/BebopSwapper/SOLANA_IMPLEMENTATION_PLAN.md b/packages/swapper/src/swappers/BebopSwapper/SOLANA_IMPLEMENTATION_PLAN.md new file mode 100644 index 00000000000..a9ebb94eda4 --- /dev/null +++ b/packages/swapper/src/swappers/BebopSwapper/SOLANA_IMPLEMENTATION_PLAN.md @@ -0,0 +1,610 @@ +# Bebop Solana Integration Implementation Plan + +## Overview +Add Solana support to the existing Bebop swapper, enabling swaps on Solana using Bebop's PMM API v3. + +## API Details + +### Base URL +- **Solana**: `https://api.bebop.xyz/pmm/solana/v3` +- **Docs**: https://api.bebop.xyz/pmm/solana/docs +- **OpenAPI**: https://api.bebop.xyz/pmm/solana/openapi.json + +### Key Differences from EVM +1. **Endpoint Path**: `/pmm/solana/v3/quote` vs `/router/{chain}/v1/quote` +2. **Transaction Format**: Returns `solana_tx` (base64 serialized VersionedTransaction) instead of `tx.data/to/value` +3. **No Multi-token Support**: Only 1:1 swaps supported (no multiple sell/buy tokens) +4. **Self-Execute Mode**: Use `gasless=false` (same as EVM) - gas fees excluded from quote +5. **Native Token**: Uses WSOL address `So11111111111111111111111111111111111111112` + +### Tested & Confirmed Working +- ✅ Transaction deserialization works (VersionedTransaction format with 3 instructions) +- ✅ Supports tokens beyond the 3 listed in token-info (JUP tested successfully) +- ✅ `gasless=false` returns `QUOTE_INDIC_ROUTE` status with `gasFee.native: "0"` +- ✅ Affiliate fees supported via `fee` parameter +- ❌ Buy-side quotes (`buy_amounts`) return error 500 +- ❌ Large amounts hit liquidity limits (102 error) +- ❌ Multi-token swaps not supported (101 error) + +## Implementation Architecture + +Based on analysis of existing Solana swappers: + +### Pattern Analysis from Other Swappers + +| Swapper | Transaction Storage | Execution Method | Pattern | +|---------|-------------------|------------------|---------| +| **Jupiter** | `jupiterQuoteResponse` + `solanaTransactionMetadata.instructions` | `getUnsignedSolanaTransaction` | Instructions built from swap API | +| **ButterSwap** | `butterSwapTransactionMetadata` (EVM) + `solanaTransactionMetadata.instructions` | `getUnsignedSolanaTransaction` | Decompiles VersionedTransaction | +| **Relay** | `relayTransactionMetadata` + `solanaTransactionMetadata.instructions` | `getUnsignedSolanaTransaction` | Uses Jupiter under the hood | +| **NearIntents** | `nearIntentsSpecific` (deposit address) | `buildSendApiTransaction` | Simple send to deposit address | + +**Bebop should follow the ButterSwap pattern** - deserialize the `solana_tx` and store instructions in `solanaTransactionMetadata`. + +## Implementation Steps + +### 1. Create Branch +```bash +git checkout -B feat_bebop_solana +``` + +### 2. Update Types (`types.ts`) + +```typescript +// Add to bebopSupportedChainIds +export const bebopSupportedChainIds = [ + // ... existing EVM chains + KnownChainIds.SolanaMainnet, +] as const + +// Add Solana-specific response type (based on actual API response) +export type BebopSolanaQuoteResponse = { + requestId: string + type: string // "121" + status: 'QUOTE_SUCCESS' | 'QUOTE_INDIC_ROUTE' // INDIC_ROUTE for gasless=false + quoteId: string // e.g. "121-xxx" + chainId: 2 // Solana + approvalType: 'Standard' + nativeToken: 'SOL' + taker: string + receiver: string + expiry: number // Unix timestamp + slippage: number + gasFee: { + native: string // "0" for gasless=false + usd: number + } + buyTokens: Record + sellTokens: Record + settlementAddress: string // "BEboPej97QDH5PS9xzCVxM5vvorvwENjtt3PV5n8b62" + approvalTarget: string // Empty for Solana + requiredSignatures: string[] + priceImpact: number + warnings: string[] + solana_tx: string // Base64 encoded VersionedTransaction + blockhash: string + makers: string[] // e.g. ["whirlpool-rfqm", "🦊"] + partnerFee?: Record + protocolFee?: Record +} + +// Update chainIdToBebopChain +export const chainIdToBebopChain: Record = { + // ... existing mappings + [KnownChainIds.SolanaMainnet]: 'solana', // Not used in URL but good for consistency +} +``` + +### 3. Add Solana Helpers (`utils/helpers/helpers.ts`) + +```typescript +import { solAssetId, wrappedSolAssetId, fromAssetId } from '@shapeshiftoss/caip' +import { KnownChainIds } from '@shapeshiftoss/types' + +// Add Solana token conversion (similar to Jupiter pattern) +export const assetIdToBebopSolanaToken = (assetId: AssetId): string => { + // Native SOL uses WSOL address + if (assetId === solAssetId) { + return 'So11111111111111111111111111111111111111112' + } + const { assetReference } = fromAssetId(assetId) + return assetReference // SPL token mint address +} + +// Check if Solana chain +export const isSolanaChainId = (chainId: ChainId): boolean => { + return chainId === KnownChainIds.SolanaMainnet +} +``` + +### 4. Update Fetch Layer (`utils/fetchFromBebop.ts`) + +```typescript +export const fetchBebopSolanaQuote = async ({ + buyAsset, + sellAsset, + sellAmountIncludingProtocolFeesCryptoBaseUnit, + takerAddress, + receiverAddress, + slippageTolerancePercentageDecimal, + affiliateBps, + apiKey, +}: { + buyAsset: Asset + sellAsset: Asset + sellAmountIncludingProtocolFeesCryptoBaseUnit: string + takerAddress: string + receiverAddress: string + slippageTolerancePercentageDecimal: string + affiliateBps?: string + apiKey: string +}): Promise> => { + try { + const sellToken = assetIdToBebopSolanaToken(sellAsset.assetId) + const buyToken = assetIdToBebopSolanaToken(buyAsset.assetId) + + const sellAmountFormatted = bn(sellAmountIncludingProtocolFeesCryptoBaseUnit).toFixed(0) + // Bebop expects percentage (0.5 for 0.5%), not decimal (0.005) + const slippagePercentage = bn(slippageTolerancePercentageDecimal ?? 0.003) + .times(100) + .toNumber() + + const url = 'https://api.bebop.xyz/pmm/solana/v3/quote' + + const params = new URLSearchParams({ + sell_tokens: sellToken, + buy_tokens: buyToken, + sell_amounts: sellAmountFormatted, + taker_address: takerAddress, + receiver_address: receiverAddress, + slippage: slippagePercentage.toString(), + approval_type: 'Standard', + skip_validation: 'false', + gasless: 'false', // Always false for self-execute + source: 'shapeshift', + }) + + if (affiliateBps && affiliateBps !== '0') { + params.set('fee', affiliateBps) + } + + const bebopService = bebopServiceFactory({ apiKey }) + const maybeResponse = await bebopService.get(`${url}?${params}`) + + if (maybeResponse.isErr()) { + return Err( + makeSwapErrorRight({ + message: 'Failed to fetch quote from Bebop Solana', + cause: maybeResponse.unwrapErr().cause, + code: TradeQuoteError.QueryFailed, + }), + ) + } + + const response = maybeResponse.unwrap() + + // Validate response + if (response.data.status !== 'QUOTE_INDIC_ROUTE') { + console.warn('Expected QUOTE_INDIC_ROUTE for gasless=false, got:', response.data.status) + } + + if (!response.data.solana_tx) { + return Err( + makeSwapErrorRight({ + message: 'Missing solana_tx in response', + code: TradeQuoteError.InvalidResponse, + }), + ) + } + + return Ok(response.data) + } catch (error) { + return Err( + makeSwapErrorRight({ + message: 'Unexpected error fetching Bebop Solana quote', + cause: error, + code: TradeQuoteError.QueryFailed, + }), + ) + } +} + +// Similar for fetchBebopSolanaPrice (for rate quotes) +export const fetchBebopSolanaPrice = ({ + buyAsset, + sellAsset, + sellAmountIncludingProtocolFeesCryptoBaseUnit, + receiveAddress, + affiliateBps, + apiKey, +}: { + buyAsset: Asset + sellAsset: Asset + sellAmountIncludingProtocolFeesCryptoBaseUnit: string + receiveAddress: string | undefined + affiliateBps?: string + apiKey: string +}): Promise> => { + // Use dummy address for rate quotes (same as EVM) + const address = receiveAddress || '11111111111111111111111111111112' + + return fetchBebopSolanaQuote({ + buyAsset, + sellAsset, + sellAmountIncludingProtocolFeesCryptoBaseUnit, + takerAddress: address, + receiverAddress: address, + slippageTolerancePercentageDecimal: '0.01', + affiliateBps, + apiKey, + }) +} +``` + +### 5. Create Solana Trade Quote Handler (`getBebopSolanaTradeQuote/getBebopSolanaTradeQuote.ts`) + +**Following ButterSwap's VersionedTransaction decompilation pattern:** + +```typescript +import { + VersionedTransaction, + TransactionMessage, + AddressLookupTableAccount, + PublicKey +} from '@solana/web3.js' +import type { solana } from '@shapeshiftoss/chain-adapters' +import { v4 as uuid } from 'uuid' + +export async function getBebopSolanaTradeQuote( + input: CommonTradeQuoteInput & { chainId: KnownChainIds.SolanaMainnet }, + assertGetSolanaChainAdapter: (chainId: ChainId) => solana.ChainAdapter, + assetsById: AssetsByIdPartial, + apiKey: string, +): Promise> { + const { + sellAsset, + buyAsset, + accountNumber, + sendAddress, + receiveAddress, + affiliateBps, + sellAmountIncludingProtocolFeesCryptoBaseUnit, + } = input + + // Validate Solana chains + if (!isSolanaChainId(sellAsset.chainId) || !isSolanaChainId(buyAsset.chainId)) { + return Err( + makeSwapErrorRight({ + message: 'Both assets must be on Solana', + code: TradeQuoteError.UnsupportedChain, + }), + ) + } + + if (!sendAddress || !receiveAddress) { + return Err( + makeSwapErrorRight({ + message: 'sendAddress and receiveAddress are required for Solana', + code: TradeQuoteError.UnknownError, + }), + ) + } + + const slippageTolerancePercentageDecimal = + input.slippageTolerancePercentageDecimal ?? + getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Bebop) + + const maybeBebopQuoteResponse = await fetchBebopSolanaQuote({ + buyAsset, + sellAsset, + sellAmountIncludingProtocolFeesCryptoBaseUnit, + takerAddress: sendAddress, + receiverAddress: receiveAddress, + slippageTolerancePercentageDecimal, + affiliateBps, + apiKey, + }) + + if (maybeBebopQuoteResponse.isErr()) return Err(maybeBebopQuoteResponse.unwrapErr()) + const bebopQuoteResponse = maybeBebopQuoteResponse.unwrap() + + // Extract token data (only one each for Solana) + const sellTokenAddress = Object.keys(bebopQuoteResponse.sellTokens)[0] + const buyTokenAddress = Object.keys(bebopQuoteResponse.buyTokens)[0] + const sellTokenData = bebopQuoteResponse.sellTokens[sellTokenAddress] + const buyTokenData = bebopQuoteResponse.buyTokens[buyTokenAddress] + + // Deserialize transaction to get instructions (following ButterSwap pattern) + const adapter = assertGetSolanaChainAdapter(sellAsset.chainId) + + const maybeSolanaTransactionMetadata = await (async () => { + try { + const txBytes = Buffer.from(bebopQuoteResponse.solana_tx, 'base64') + const versionedTransaction = VersionedTransaction.deserialize(txBytes) + + // Get address lookup table accounts if present + const addressLookupTableAccountKeys = versionedTransaction.message.addressTableLookups.map( + lookup => lookup.accountKey.toString(), + ) + + const addressLookupTableAccountsInfos = await adapter.getAddressLookupTableAccounts( + addressLookupTableAccountKeys, + ) + + const addressLookupTableAccounts = addressLookupTableAccountsInfos.map( + info => + new AddressLookupTableAccount({ + key: new PublicKey(info.key), + state: AddressLookupTableAccount.deserialize(new Uint8Array(info.data)), + }), + ) + + // Decompile VersionedMessage to get instructions + const instructions = TransactionMessage.decompile(versionedTransaction.message, { + addressLookupTableAccounts, + }).instructions + + return Ok({ + instructions, + addressLookupTableAddresses: addressLookupTableAccountKeys, + }) + } catch (error) { + return Err( + makeSwapErrorRight({ + message: `Error decompiling Bebop transaction: ${error}`, + code: TradeQuoteError.InvalidResponse, + }), + ) + } + })() + + if (maybeSolanaTransactionMetadata.isErr()) { + return Err(maybeSolanaTransactionMetadata.unwrapErr()) + } + + const solanaTransactionMetadata = maybeSolanaTransactionMetadata.unwrap() + + const rate = calculateRate({ + buyAmount: buyTokenData.amount, + sellAmount: sellTokenData.amount, + buyAsset, + sellAsset, + }) + + // Network fee should be 0 for gasless=false + const networkFeeCryptoBaseUnit = bebopQuoteResponse.gasFee.native || '0' + + return Ok({ + id: uuid(), + quoteOrRate: 'quote' as const, + receiveAddress, + affiliateBps, + slippageTolerancePercentageDecimal, + rate, + swapperName: SwapperName.Bebop, + priceImpactPercentageDecimal: bebopQuoteResponse.priceImpact?.toString(), + steps: [ + { + estimatedExecutionTimeMs: undefined, // Solana is fast + allowanceContract: '0x0', // No allowances on Solana + buyAsset, + sellAsset, + accountNumber, + rate, + feeData: { + protocolFees: {}, // Bebop doesn't charge visible protocol fees + networkFeeCryptoBaseUnit, + chainSpecific: { + computeUnits: '200000', // Will be calculated properly in getUnsignedSolanaTransaction + priorityFee: '0', + }, + }, + buyAmountBeforeFeesCryptoBaseUnit: buyTokenData.amountBeforeFee || buyTokenData.amount, + buyAmountAfterFeesCryptoBaseUnit: buyTokenData.amount, + sellAmountIncludingProtocolFeesCryptoBaseUnit: sellTokenData.amount, + source: SwapperName.Bebop, + solanaTransactionMetadata, // Store deserialized instructions + }, + ] as SingleHopTradeQuoteSteps, + }) +} +``` + +### 6. Create Solana Trade Rate Handler (`getBebopSolanaTradeRate/getBebopSolanaTradeRate.ts`) + +Similar to quote but: +- Use dummy address if no receiveAddress +- Return `accountNumber: undefined` +- Skip transaction deserialization (not needed for rates) + +### 7. Update API Endpoints (`endpoints.ts`) + +```typescript +import { getSolanaTransactionFees } from '../../solana-utils/getSolanaTransactionFees' +import { getUnsignedSolanaTransaction } from '../../solana-utils/getUnsignedSolanaTransaction' +import { isSolanaChainId } from './utils/helpers/helpers' +import { getBebopSolanaTradeQuote } from './getBebopSolanaTradeQuote/getBebopSolanaTradeQuote' +import { getBebopSolanaTradeRate } from './getBebopSolanaTradeRate/getBebopSolanaTradeRate' + +export const bebopApi: SwapperApi = { + getTradeQuote: async (input, deps) => { + // Route to Solana handler if Solana chain + if (isSolanaChainId(input.sellAsset.chainId)) { + const tradeQuoteResult = await getBebopSolanaTradeQuote( + input as CommonTradeQuoteInput & { chainId: KnownChainIds.SolanaMainnet }, + deps.assertGetSolanaChainAdapter, + deps.assetsById, + deps.config.VITE_BEBOP_API_KEY, + ) + return tradeQuoteResult.map(tradeQuote => [tradeQuote]) + } + + // Existing EVM logic + const tradeQuoteResult = await getBebopTradeQuote( + input as GetEvmTradeQuoteInputBase, + deps.assertGetEvmChainAdapter, + deps.assetsById, + deps.config.VITE_BEBOP_API_KEY, + ) + return tradeQuoteResult.map(tradeQuote => [tradeQuote]) + }, + + getTradeRate: async (input, deps) => { + // Similar routing for rate quotes + if (isSolanaChainId(input.sellAsset.chainId)) { + const tradeRateResult = await getBebopSolanaTradeRate( + input, + deps.assertGetSolanaChainAdapter, + deps.assetsById, + deps.config.VITE_BEBOP_API_KEY, + ) + return tradeRateResult.map(tradeRate => [tradeRate]) + } + + // Existing EVM logic + const tradeRateResult = await getBebopTradeRate( + input as GetEvmTradeRateInput, + deps.assertGetEvmChainAdapter, + deps.assetsById, + deps.config.VITE_BEBOP_API_KEY, + ) + return tradeRateResult.map(tradeRate => [tradeRate]) + }, + + // Add Solana transaction methods (following Jupiter/Relay/ButterSwap pattern) + getUnsignedSolanaTransaction, + getSolanaTransactionFees, + + // Existing EVM methods + getUnsignedEvmTransaction: async ({ /* ... */ }) => { /* existing */ }, + getEvmTransactionFees: async ({ /* ... */ }) => { /* existing */ }, + + checkTradeStatus: (input) => { + const { chainId } = input + return isSolanaChainId(chainId) + ? checkSolanaSwapStatus(input) + : checkEvmSwapStatus(input) + }, +} +``` + +### 8. Update Swapper (`BebopSwapper.ts`) + +```typescript +import { executeSolanaTransaction } from '../../utils' + +export const bebopSwapper: Swapper = { + executeEvmTransaction, + executeSolanaTransaction, // Add Solana execution (standard pattern) +} +``` + +### 9. Testing Checklist + +- [ ] Native SOL → Token swaps (use `solAssetId`) +- [ ] Token → Native SOL swaps +- [ ] Token → Token swaps (USDC ↔ USDT) +- [ ] Large amounts (check liquidity error handling) +- [ ] Invalid token addresses (proper error messages) +- [ ] Missing required parameters +- [ ] Affiliate fee handling (`fee` parameter) +- [ ] Transaction deserialization (VersionedTransaction) +- [ ] Address lookup table handling +- [ ] Instruction parsing and storage +- [ ] Network fee calculation (should be 0 for gasless=false) + +### 10. Type Checks and Validation + +```bash +yarn type-check +yarn lint --fix +``` + +## Key Implementation Notes + +### Transaction Handling Pattern +Following the **ButterSwap pattern** (most similar to Bebop's response): +1. Deserialize base64 `solana_tx` → `VersionedTransaction` +2. Load address lookup tables if present +3. Decompile message to get instructions +4. Store in `solanaTransactionMetadata.instructions` +5. Use standard `getUnsignedSolanaTransaction` for execution + +### Critical Details +1. **Always use `gasless=false`** - We handle transaction execution ourselves +2. **WSOL Address** - Native SOL uses `So11111111111111111111111111111111111111112` +3. **No Multi-token** - Solana only supports 1:1 swaps (returns error 101) +4. **Status Codes**: + - `QUOTE_SUCCESS` - gasless=true (Bebop executes) + - `QUOTE_INDIC_ROUTE` - gasless=false (self-execute) ✅ +5. **Error Codes**: + - 102: `InsufficientLiquidity` (tested with large amounts) + - 101: `InvalidApiRequest` (invalid addresses, multi-token) + - 500: `UnknownError` (buy_amounts parameter) +6. **Transaction Format** - Base64 encoded VersionedTransaction with: + - Version: 0 + - ~8 static account keys + - 3 compiled instructions + - 1 address lookup table +7. **Token Support** - Works with tokens beyond the 3 in token-info (JUP tested) + +## API Response Example (gasless=false) + +```json +{ + "quoteId": "121-130947180405096496844699833776839001867", + "status": "QUOTE_INDIC_ROUTE", + "chainId": 2, + "taker": "DRpbCBMxVnDK7maPM5tGv6MvB3v1sRMC86PZ8okm21KW", + "receiver": "CuieVDEDtLo7FypA9SbLM9saXFdb1dsshEkyErMqkRQq", + "expiry": 1765541175, + "slippage": 0, + "gasFee": { "native": "0", "usd": 0 }, + "buyTokens": { + "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v": { + "amount": "13845232", + "symbol": "USDC", + "priceImpact": -0.0013225011698168277 + } + }, + "sellTokens": { + "So11111111111111111111111111111111111111112": { + "amount": "100000000", + "symbol": "WSOL" + } + }, + "solana_tx": "AgAAAAA...", // 872 chars base64 + "blockhash": "6ospwWHQZmsK3Qe4wGgbfdoddj2C79CozdBUvpB4gXpS", + "makers": ["🦊"] +} +``` + +## Next Steps + +1. Implement according to this plan +2. Run type checks after each major component +3. Test with real transactions +4. Add unit tests for new functions +5. Update README with Solana support notes + +## Post-Implementation +- Consider adding more Solana tokens as they become available +- Monitor for API updates that might add buy_amounts support +- Watch for multi-token support (currently returns error) \ No newline at end of file diff --git a/packages/swapper/src/swappers/BebopSwapper/endpoints.ts b/packages/swapper/src/swappers/BebopSwapper/endpoints.ts index 89ceb97be99..24686877328 100644 --- a/packages/swapper/src/swappers/BebopSwapper/endpoints.ts +++ b/packages/swapper/src/swappers/BebopSwapper/endpoints.ts @@ -1,29 +1,66 @@ import { evm } from '@shapeshiftoss/chain-adapters' +import type { KnownChainIds } from '@shapeshiftoss/types' import BigNumber from 'bignumber.js' import { fromHex } from 'viem' -import type { GetEvmTradeQuoteInputBase, GetEvmTradeRateInput, SwapperApi } from '../../types' -import { checkEvmSwapStatus, getExecutableTradeStep, isExecutableTradeQuote } from '../../utils' +import { getSolanaTransactionFees } from '../../solana-utils/getSolanaTransactionFees' +import type { + CommonTradeQuoteInput, + GetEvmTradeQuoteInputBase, + GetEvmTradeRateInput, + SwapperApi, +} from '../../types' +import { + checkEvmSwapStatus, + checkSolanaSwapStatus, + getExecutableTradeStep, + isExecutableTradeQuote, +} from '../../utils' +import { getBebopSolanaTradeQuote } from './getBebopSolanaTradeQuote/getBebopSolanaTradeQuote' +import { getBebopSolanaTradeRate } from './getBebopSolanaTradeRate/getBebopSolanaTradeRate' import { getBebopTradeQuote } from './getBebopTradeQuote/getBebopTradeQuote' import { getBebopTradeRate } from './getBebopTradeRate/getBebopTradeRate' +import { getUnsignedBebopSolanaTransaction } from './getUnsignedBebopSolanaTransaction' +import { getUnsignedSolanaMessage } from './getUnsignedSolanaMessage' +import { isSolanaChainId } from './utils/helpers/helpers' export const bebopApi: SwapperApi = { - getTradeQuote: async (input, { assertGetEvmChainAdapter, assetsById, config }) => { + getTradeQuote: async (input, deps) => { + if (isSolanaChainId(input.sellAsset.chainId)) { + const tradeQuoteResult = await getBebopSolanaTradeQuote( + input as CommonTradeQuoteInput & { chainId: KnownChainIds.SolanaMainnet }, + deps.assertGetSolanaChainAdapter, + deps.assetsById, + deps.config.VITE_BEBOP_API_KEY, + ) + return tradeQuoteResult.map(tradeQuote => [tradeQuote]) + } + const tradeQuoteResult = await getBebopTradeQuote( input as GetEvmTradeQuoteInputBase, - assertGetEvmChainAdapter, - assetsById, - config.VITE_BEBOP_API_KEY, + deps.assertGetEvmChainAdapter, + deps.assetsById, + deps.config.VITE_BEBOP_API_KEY, ) return tradeQuoteResult.map(tradeQuote => [tradeQuote]) }, - getTradeRate: async (input, { assertGetEvmChainAdapter, assetsById, config }) => { + getTradeRate: async (input, deps) => { + if (isSolanaChainId(input.sellAsset.chainId)) { + const tradeRateResult = await getBebopSolanaTradeRate( + input, + deps.assertGetSolanaChainAdapter, + deps.assetsById, + deps.config.VITE_BEBOP_API_KEY, + ) + return tradeRateResult.map(tradeRate => [tradeRate]) + } + const tradeRateResult = await getBebopTradeRate( input as GetEvmTradeRateInput, - assertGetEvmChainAdapter, - assetsById, - config.VITE_BEBOP_API_KEY, + deps.assertGetEvmChainAdapter, + deps.assetsById, + deps.config.VITE_BEBOP_API_KEY, ) return tradeRateResult.map(tradeRate => [tradeRate]) @@ -87,5 +124,11 @@ export const bebopApi: SwapperApi = { return feeData.networkFeeCryptoBaseUnit }, - checkTradeStatus: checkEvmSwapStatus, + getUnsignedSolanaTransaction: getUnsignedBebopSolanaTransaction, + getUnsignedSolanaMessage, + getSolanaTransactionFees, + checkTradeStatus: input => { + const { chainId } = input + return isSolanaChainId(chainId) ? checkSolanaSwapStatus(input) : checkEvmSwapStatus(input) + }, } diff --git a/packages/swapper/src/swappers/BebopSwapper/executeSolanaMessage.ts b/packages/swapper/src/swappers/BebopSwapper/executeSolanaMessage.ts new file mode 100644 index 00000000000..29b324a3030 --- /dev/null +++ b/packages/swapper/src/swappers/BebopSwapper/executeSolanaMessage.ts @@ -0,0 +1,55 @@ +import baseX from 'base-x' + +import type { SolanaMessageExecutionProps, SolanaMessageToSign } from '../../types' +import { bebopServiceFactory } from './utils/bebopService' + +// Base58 alphabet used by Bitcoin and Solana +const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' +const base58 = baseX(BASE58_ALPHABET) + +export const executeSolanaMessage = async ( + messageData: SolanaMessageToSign, + callbacks: SolanaMessageExecutionProps, + apiKey: string, +): Promise => { + const { messageToSign, quoteId } = messageData + + if (!quoteId) { + throw new Error('Quote ID is required for Solana order submission') + } + + // Sign the message bytes + const signature = await callbacks.signMessage(messageToSign) + + // Convert signature to base58 for Bebop API + const base58Signature = base58.encode(Buffer.from(signature)) + + console.log('[Bebop executeSolanaMessage] Submitting order:', { + quoteId, + signatureLength: signature.length, + }) + + // Submit to Bebop /v3/order endpoint + const bebopService = bebopServiceFactory({ apiKey }) + const response = await bebopService.post<{ txHash: string }>( + 'https://api.bebop.xyz/pmm/solana/v3/order', + { + quote_id: quoteId, + signature: base58Signature, + }, + ) + + if (response.isErr()) { + throw response.unwrapErr() + } + + const { txHash } = response.unwrap().data + + if (!txHash) { + throw new Error('No transaction hash returned from Bebop order submission') + } + + console.log('[Bebop executeSolanaMessage] Order submitted successfully:', txHash) + + return txHash +} \ No newline at end of file diff --git a/packages/swapper/src/swappers/BebopSwapper/getBebopSolanaTradeQuote/getBebopSolanaTradeQuote.ts b/packages/swapper/src/swappers/BebopSwapper/getBebopSolanaTradeQuote/getBebopSolanaTradeQuote.ts new file mode 100644 index 00000000000..14c078b2d08 --- /dev/null +++ b/packages/swapper/src/swappers/BebopSwapper/getBebopSolanaTradeQuote/getBebopSolanaTradeQuote.ts @@ -0,0 +1,182 @@ +import type { ChainId } from '@shapeshiftoss/caip' +import type { solana } from '@shapeshiftoss/chain-adapters' +import type { AssetsByIdPartial, KnownChainIds } from '@shapeshiftoss/types' +import type { Result } from '@sniptt/monads' +import { Err, Ok } from '@sniptt/monads' +import { + AddressLookupTableAccount, + PublicKey, + TransactionMessage, + VersionedTransaction, +} from '@solana/web3.js' +import { v4 as uuid } from 'uuid' + +import { getDefaultSlippageDecimalPercentageForSwapper } from '../../../constants' +import type { + CommonTradeQuoteInput, + SingleHopTradeQuoteSteps, + SwapErrorRight, + TradeQuote, +} from '../../../types' +import { SwapperName, TradeQuoteError } from '../../../types' +import { makeSwapErrorRight } from '../../../utils' +import { fetchBebopSolanaQuote } from '../utils/fetchFromBebop' +import { calculateRate, isSolanaChainId } from '../utils/helpers/helpers' + +export async function getBebopSolanaTradeQuote( + input: CommonTradeQuoteInput & { chainId: KnownChainIds.SolanaMainnet }, + assertGetSolanaChainAdapter: (chainId: ChainId) => solana.ChainAdapter, + _assetsById: AssetsByIdPartial, + apiKey: string, +): Promise> { + const { + sellAsset, + buyAsset, + accountNumber, + sendAddress, + receiveAddress, + affiliateBps, + sellAmountIncludingProtocolFeesCryptoBaseUnit, + } = input + + if (!isSolanaChainId(sellAsset.chainId) || !isSolanaChainId(buyAsset.chainId)) { + return Err( + makeSwapErrorRight({ + message: 'Both assets must be on Solana', + code: TradeQuoteError.UnsupportedChain, + }), + ) + } + + if (!sendAddress || !receiveAddress) { + return Err( + makeSwapErrorRight({ + message: 'sendAddress and receiveAddress are required for Solana', + code: TradeQuoteError.UnknownError, + }), + ) + } + + const slippageTolerancePercentageDecimal = + input.slippageTolerancePercentageDecimal ?? + getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Bebop) + + const maybeBebopQuoteResponse = await fetchBebopSolanaQuote({ + buyAsset, + sellAsset, + sellAmountIncludingProtocolFeesCryptoBaseUnit, + takerAddress: sendAddress, + receiverAddress: receiveAddress, + slippageTolerancePercentageDecimal, + affiliateBps, + apiKey, + }) + + if (maybeBebopQuoteResponse.isErr()) return Err(maybeBebopQuoteResponse.unwrapErr()) + const bebopQuoteResponse = maybeBebopQuoteResponse.unwrap() + + const sellTokenAddress = Object.keys(bebopQuoteResponse.sellTokens)[0] + const buyTokenAddress = Object.keys(bebopQuoteResponse.buyTokens)[0] + const sellTokenData = bebopQuoteResponse.sellTokens[sellTokenAddress] + const buyTokenData = bebopQuoteResponse.buyTokens[buyTokenAddress] + + const adapter = assertGetSolanaChainAdapter(sellAsset.chainId) + + const txBytes = Buffer.from(bebopQuoteResponse.solana_tx, 'base64') + const versionedTransaction = VersionedTransaction.deserialize(txBytes) + + const addressLookupTableAccountKeys = versionedTransaction.message.addressTableLookups.map( + lookup => lookup.accountKey.toString(), + ) + + const addressLookupTableAccountsInfos = await adapter.getAddressLookupTableAccounts( + addressLookupTableAccountKeys, + ) + + const addressLookupTableAccounts = addressLookupTableAccountsInfos.map( + info => + new AddressLookupTableAccount({ + key: new PublicKey(info.key), + state: AddressLookupTableAccount.deserialize(new Uint8Array(info.data)), + }), + ) + + const instructions = TransactionMessage.decompile(versionedTransaction.message, { + addressLookupTableAccounts, + }).instructions + + const feeDataInput = { + to: '', + value: '0', + chainSpecific: { + from: sendAddress, + addressLookupTableAccounts: addressLookupTableAccountKeys, + instructions, + }, + } + + const { fast } = await adapter.getFeeData(feeDataInput) + + console.log('[Bebop Solana] Fee data received:', JSON.stringify({ + txFee: fast.txFee, + chainSpecific: fast.chainSpecific, + })) + + const networkFeeCryptoBaseUnit = fast.txFee + + const rate = calculateRate({ + buyAmount: buyTokenData.amount, + sellAmount: sellTokenData.amount, + buyAsset, + sellAsset, + }) + + const tradeQuote = { + id: uuid(), + quoteOrRate: 'quote' as const, + receiveAddress, + affiliateBps, + slippageTolerancePercentageDecimal, + rate, + swapperName: SwapperName.Bebop, + priceImpactPercentageDecimal: bebopQuoteResponse.priceImpact?.toString(), + steps: [ + { + estimatedExecutionTimeMs: undefined, + allowanceContract: '0x0', + buyAsset, + sellAsset, + accountNumber, + rate, + feeData: { + protocolFees: {}, + networkFeeCryptoBaseUnit, + chainSpecific: { + computeUnits: fast.chainSpecific.computeUnits, + priorityFee: fast.chainSpecific.priorityFee, + }, + }, + buyAmountBeforeFeesCryptoBaseUnit: buyTokenData.amountBeforeFee || buyTokenData.amount, + buyAmountAfterFeesCryptoBaseUnit: buyTokenData.amount, + sellAmountIncludingProtocolFeesCryptoBaseUnit: sellTokenData.amount, + source: SwapperName.Bebop, + bebopSolanaSerializedTx: bebopQuoteResponse.solana_tx, + bebopQuoteId: bebopQuoteResponse.quoteId, + }, + ] as SingleHopTradeQuoteSteps, + } + + const step = tradeQuote.steps[0] + const solanaChainSpecific = step.feeData.chainSpecific as { computeUnits: string; priorityFee: string } | undefined + + console.log('[Bebop Solana] Returning trade quote:', JSON.stringify({ + id: tradeQuote.id, + networkFeeCryptoBaseUnit: step.feeData.networkFeeCryptoBaseUnit, + computeUnits: solanaChainSpecific?.computeUnits, + priorityFee: solanaChainSpecific?.priorityFee, + buyAmount: step.buyAmountAfterFeesCryptoBaseUnit, + sellAmount: step.sellAmountIncludingProtocolFeesCryptoBaseUnit, + })) + + return Ok(tradeQuote) +} diff --git a/packages/swapper/src/swappers/BebopSwapper/getBebopSolanaTradeRate/getBebopSolanaTradeRate.ts b/packages/swapper/src/swappers/BebopSwapper/getBebopSolanaTradeRate/getBebopSolanaTradeRate.ts new file mode 100644 index 00000000000..d3e59f31c49 --- /dev/null +++ b/packages/swapper/src/swappers/BebopSwapper/getBebopSolanaTradeRate/getBebopSolanaTradeRate.ts @@ -0,0 +1,204 @@ +import type { ChainId } from '@shapeshiftoss/caip' +import type { solana } from '@shapeshiftoss/chain-adapters' +import type { AssetsByIdPartial } from '@shapeshiftoss/types' +import type { Result } from '@sniptt/monads' +import { Err, Ok } from '@sniptt/monads' +import { + AddressLookupTableAccount, + PublicKey, + TransactionMessage, + VersionedTransaction, +} from '@solana/web3.js' +import { v4 as uuid } from 'uuid' + +import { getDefaultSlippageDecimalPercentageForSwapper } from '../../../constants' +import type { + GetTradeRateInput, + SingleHopTradeRateSteps, + SwapErrorRight, + TradeRate, +} from '../../../types' +import { SwapperName, TradeQuoteError } from '../../../types' +import { makeSwapErrorRight } from '../../../utils' +import { fetchBebopSolanaPrice } from '../utils/fetchFromBebop' +import { calculateRate, isSolanaChainId } from '../utils/helpers/helpers' + +const SOLANA_RANDOM_ADDRESS = '11111111111111111111111111111112' + +export async function getBebopSolanaTradeRate( + input: GetTradeRateInput, + assertGetSolanaChainAdapter: (chainId: ChainId) => solana.ChainAdapter, + _assetsById: AssetsByIdPartial, + apiKey: string, +): Promise> { + const { + sellAsset, + buyAsset, + receiveAddress, + affiliateBps, + sellAmountIncludingProtocolFeesCryptoBaseUnit, + sendAddress, + } = input + + if (!isSolanaChainId(sellAsset.chainId) || !isSolanaChainId(buyAsset.chainId)) { + return Err( + makeSwapErrorRight({ + message: 'Both assets must be on Solana', + code: TradeQuoteError.UnsupportedChain, + }), + ) + } + + const slippageTolerancePercentageDecimal = + input.slippageTolerancePercentageDecimal ?? + getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Bebop) + + console.log('[Bebop Solana Rate] Fetching rate quote for:', JSON.stringify({ + sellAsset: sellAsset.symbol, + buyAsset: buyAsset.symbol, + sellAmount: sellAmountIncludingProtocolFeesCryptoBaseUnit, + sendAddress, + receiveAddress, + })) + + const maybeBebopPriceResponse = await fetchBebopSolanaPrice({ + buyAsset, + sellAsset, + sellAmountIncludingProtocolFeesCryptoBaseUnit, + receiveAddress, + affiliateBps, + apiKey, + }) + + if (maybeBebopPriceResponse.isErr()) { + console.log('[Bebop Solana Rate] Error fetching price:', JSON.stringify({ + error: maybeBebopPriceResponse.unwrapErr(), + })) + return Err(maybeBebopPriceResponse.unwrapErr()) + } + + const bebopPriceResponse = maybeBebopPriceResponse.unwrap() + + console.log('[Bebop Solana Rate] Price response received:', JSON.stringify({ + quoteId: bebopPriceResponse.quoteId, + status: bebopPriceResponse.status, + gasFee: bebopPriceResponse.gasFee, + hasSolanaTx: !!bebopPriceResponse.solana_tx, + })) + + const sellTokenAddress = Object.keys(bebopPriceResponse.sellTokens)[0] + const buyTokenAddress = Object.keys(bebopPriceResponse.buyTokens)[0] + const sellTokenData = bebopPriceResponse.sellTokens[sellTokenAddress] + const buyTokenData = bebopPriceResponse.buyTokens[buyTokenAddress] + + const rate = calculateRate({ + buyAmount: buyTokenData.amount, + sellAmount: sellTokenData.amount, + buyAsset, + sellAsset, + }) + + const adapter = assertGetSolanaChainAdapter(sellAsset.chainId) + + const getFeeData = async () => { + try { + const txBytes = Buffer.from(bebopPriceResponse.solana_tx, 'base64') + const versionedTransaction = VersionedTransaction.deserialize(txBytes) + + const addressLookupTableAccountKeys = versionedTransaction.message.addressTableLookups.map( + lookup => lookup.accountKey.toString(), + ) + + const addressLookupTableAccountsInfos = await adapter.getAddressLookupTableAccounts( + addressLookupTableAccountKeys, + ) + + const addressLookupTableAccounts = addressLookupTableAccountsInfos.map( + info => + new AddressLookupTableAccount({ + key: new PublicKey(info.key), + state: AddressLookupTableAccount.deserialize(new Uint8Array(info.data)), + }), + ) + + const instructions = TransactionMessage.decompile(versionedTransaction.message, { + addressLookupTableAccounts, + }).instructions + + const from = sendAddress ?? SOLANA_RANDOM_ADDRESS + + const { fast } = await adapter.getFeeData({ + to: '', + value: '0', + chainSpecific: { + from, + addressLookupTableAccounts: addressLookupTableAccountKeys, + instructions, + }, + }) + + return { + networkFeeCryptoBaseUnit: fast.txFee, + chainSpecific: { + computeUnits: fast.chainSpecific.computeUnits, + priorityFee: fast.chainSpecific.priorityFee, + }, + } + } catch (error) { + console.warn('[Bebop Solana Rate] Failed to calculate fees, using fallback:', error) + return { + networkFeeCryptoBaseUnit: '0', + chainSpecific: { + computeUnits: '200000', + priorityFee: '0', + }, + } + } + } + + const feeData = await getFeeData() + + console.log('[Bebop Solana Rate] Calculated fee data:', JSON.stringify({ + networkFeeCryptoBaseUnit: feeData.networkFeeCryptoBaseUnit, + computeUnits: feeData.chainSpecific.computeUnits, + priorityFee: feeData.chainSpecific.priorityFee, + })) + + console.log('[Bebop Solana Rate] Returning trade rate:', JSON.stringify({ + networkFeeCryptoBaseUnit: feeData.networkFeeCryptoBaseUnit, + rate, + buyAmount: buyTokenData.amount, + sellAmount: sellTokenData.amount, + })) + + return Ok({ + id: uuid(), + quoteOrRate: 'rate' as const, + accountNumber: undefined, + receiveAddress, + affiliateBps, + slippageTolerancePercentageDecimal, + rate, + swapperName: SwapperName.Bebop, + priceImpactPercentageDecimal: bebopPriceResponse.priceImpact?.toString(), + steps: [ + { + estimatedExecutionTimeMs: undefined, + allowanceContract: '0x0', + buyAsset, + sellAsset, + accountNumber: undefined, + rate, + feeData: { + protocolFees: {}, + networkFeeCryptoBaseUnit: feeData.networkFeeCryptoBaseUnit, + chainSpecific: feeData.chainSpecific, + }, + buyAmountBeforeFeesCryptoBaseUnit: buyTokenData.amountBeforeFee || buyTokenData.amount, + buyAmountAfterFeesCryptoBaseUnit: buyTokenData.amount, + sellAmountIncludingProtocolFeesCryptoBaseUnit: sellTokenData.amount, + source: SwapperName.Bebop, + }, + ] as SingleHopTradeRateSteps, + }) +} diff --git a/packages/swapper/src/swappers/BebopSwapper/getUnsignedBebopSolanaTransaction.ts b/packages/swapper/src/swappers/BebopSwapper/getUnsignedBebopSolanaTransaction.ts new file mode 100644 index 00000000000..25d46c38baf --- /dev/null +++ b/packages/swapper/src/swappers/BebopSwapper/getUnsignedBebopSolanaTransaction.ts @@ -0,0 +1,57 @@ +import type { SolanaSignTx } from '@shapeshiftoss/hdwallet-core' +import { ComputeBudgetProgram, type TransactionInstruction } from '@solana/web3.js' + +import type { GetUnsignedSolanaTransactionArgs } from '../../types' +import { getExecutableTradeStep, isExecutableTradeQuote } from '../../utils' + +export const getUnsignedBebopSolanaTransaction = async ({ + stepIndex, + tradeQuote, + from, + assertGetSolanaChainAdapter, +}: GetUnsignedSolanaTransactionArgs): Promise => { + if (!isExecutableTradeQuote(tradeQuote)) throw new Error('Unable to execute a trade rate quote') + + const step = getExecutableTradeStep(tradeQuote, stepIndex) + const { accountNumber, solanaTransactionMetadata, sellAsset } = step + + if (!solanaTransactionMetadata) { + throw new Error('Missing solanaTransactionMetadata') + } + + const adapter = assertGetSolanaChainAdapter(sellAsset.chainId) + + let computeUnitLimit: number | undefined + let computeUnitPrice: number | undefined + const nonComputeBudgetInstructions: TransactionInstruction[] = [] + + solanaTransactionMetadata.instructions?.forEach(ix => { + if (ix.programId.equals(ComputeBudgetProgram.programId)) { + const data = ix.data + if (data[0] === 2) { + computeUnitLimit = data.readUInt32LE(1) + } else if (data[0] === 3) { + computeUnitPrice = Number(data.readBigUInt64LE(1)) + } + } else { + nonComputeBudgetInstructions.push(ix) + } + }) + + const solanaInstructions = nonComputeBudgetInstructions.map(instruction => + adapter.convertInstruction(instruction), + ) + + return adapter.buildSendApiTransaction({ + from, + to: '', + value: '0', + accountNumber, + chainSpecific: { + addressLookupTableAccounts: solanaTransactionMetadata.addressLookupTableAddresses, + instructions: solanaInstructions, + computeUnitLimit: computeUnitLimit?.toString(), + computeUnitPrice: computeUnitPrice?.toString(), + }, + }) +} diff --git a/packages/swapper/src/swappers/BebopSwapper/getUnsignedSolanaMessage.ts b/packages/swapper/src/swappers/BebopSwapper/getUnsignedSolanaMessage.ts new file mode 100644 index 00000000000..056a93af7fc --- /dev/null +++ b/packages/swapper/src/swappers/BebopSwapper/getUnsignedSolanaMessage.ts @@ -0,0 +1,28 @@ +import type { GetUnsignedSolanaMessageArgs } from '../../types' +import { getExecutableTradeStep, isExecutableTradeQuote } from '../../utils' + +export const getUnsignedSolanaMessage = async ({ + tradeQuote, + stepIndex, +}: GetUnsignedSolanaMessageArgs) => { + if (!isExecutableTradeQuote(tradeQuote)) { + throw new Error('Unable to execute a trade rate quote') + } + + const step = getExecutableTradeStep(tradeQuote, stepIndex) + const { bebopSolanaSerializedTx, bebopQuoteId } = step + + if (!bebopSolanaSerializedTx) { + throw new Error('Missing Bebop Solana serialized transaction') + } + + if (!bebopQuoteId) { + throw new Error('Missing Bebop quote ID for Solana order submission') + } + + // Return the unsigned message bytes for signing and the quote ID for order submission + return { + messageToSign: bebopSolanaSerializedTx, + quoteId: bebopQuoteId, + } +} \ No newline at end of file diff --git a/packages/swapper/src/swappers/BebopSwapper/types.ts b/packages/swapper/src/swappers/BebopSwapper/types.ts index e9851c9a837..7a76e21f5ba 100644 --- a/packages/swapper/src/swappers/BebopSwapper/types.ts +++ b/packages/swapper/src/swappers/BebopSwapper/types.ts @@ -9,6 +9,7 @@ export const bebopSupportedChainIds = [ KnownChainIds.AvalancheMainnet, KnownChainIds.OptimismMainnet, KnownChainIds.BnbSmartChainMainnet, + KnownChainIds.SolanaMainnet, ] as const export type BebopSupportedChainId = (typeof bebopSupportedChainIds)[number] @@ -113,6 +114,7 @@ export const chainIdToBebopChain: Record = { [KnownChainIds.AvalancheMainnet]: 'avalanche', [KnownChainIds.OptimismMainnet]: 'optimism', [KnownChainIds.BnbSmartChainMainnet]: 'bsc', + [KnownChainIds.SolanaMainnet]: 'solana', } export const BEBOP_NATIVE_MARKER = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE' @@ -121,3 +123,60 @@ export const BEBOP_NATIVE_MARKER = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE' // This is Vitalik's address, same as what Bebop's own UI uses for price-only quotes. // MUST NEVER be used for executable quote transactions. export const BEBOP_DUMMY_ADDRESS = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' as Address + +// Solana-specific types +export type BebopSolanaQuoteResponse = { + requestId: string + type: string + status: 'QUOTE_SUCCESS' | 'QUOTE_INDIC_ROUTE' + quoteId: string + chainId: 2 + approvalType: 'Standard' + nativeToken: 'SOL' + taker: string + receiver: string + expiry: number + slippage: number + gasFee: { + native: string + usd: number + } + buyTokens: Record< + string, + { + amount: string + decimals: number + priceUsd: number + symbol: string + minimumAmount: string + price: number + priceBeforeFee: number + amountBeforeFee: string + deltaFromExpected: number + } + > + sellTokens: Record< + string, + { + amount: string + decimals: number + priceUsd: number + symbol: string + price: number + priceBeforeFee: number + } + > + settlementAddress: string + approvalTarget: string + requiredSignatures: string[] + priceImpact: number + warnings: string[] + solana_tx: string + blockhash: string + makers: string[] + partnerFee?: Record + protocolFee?: Record +} + +// Solana dummy address for rate quotes (system program ID) +export const BEBOP_SOLANA_DUMMY_ADDRESS = '11111111111111111111111111111112' diff --git a/packages/swapper/src/swappers/BebopSwapper/utils/fetchFromBebop.ts b/packages/swapper/src/swappers/BebopSwapper/utils/fetchFromBebop.ts index ba81eb7dcb2..33f802bd487 100644 --- a/packages/swapper/src/swappers/BebopSwapper/utils/fetchFromBebop.ts +++ b/packages/swapper/src/swappers/BebopSwapper/utils/fetchFromBebop.ts @@ -8,10 +8,10 @@ import { getAddress } from 'viem' import type { SwapErrorRight } from '../../../types' import { TradeQuoteError } from '../../../types' import { makeSwapErrorRight } from '../../../utils' -import type { BebopQuoteResponse, BebopSupportedChainId } from '../types' -import { BEBOP_DUMMY_ADDRESS, chainIdToBebopChain } from '../types' +import type { BebopQuoteResponse, BebopSolanaQuoteResponse, BebopSupportedChainId } from '../types' +import { BEBOP_DUMMY_ADDRESS, BEBOP_SOLANA_DUMMY_ADDRESS, chainIdToBebopChain } from '../types' import { bebopServiceFactory } from './bebopService' -import { assetIdToBebopToken } from './helpers/helpers' +import { assetIdToBebopSolanaToken, assetIdToBebopToken } from './helpers/helpers' export const fetchBebopQuote = async ({ buyAsset, @@ -143,3 +143,145 @@ export const fetchBebopPrice = ({ apiKey, }) } + +export const fetchBebopSolanaQuote = async ({ + buyAsset, + sellAsset, + sellAmountIncludingProtocolFeesCryptoBaseUnit, + takerAddress, + receiverAddress, + slippageTolerancePercentageDecimal, + affiliateBps, + apiKey, +}: { + buyAsset: Asset + sellAsset: Asset + sellAmountIncludingProtocolFeesCryptoBaseUnit: string + takerAddress: string + receiverAddress: string + slippageTolerancePercentageDecimal: string + affiliateBps?: string + apiKey: string +}): Promise> => { + try { + const sellToken = assetIdToBebopSolanaToken(sellAsset.assetId) + const buyToken = assetIdToBebopSolanaToken(buyAsset.assetId) + + const sellAmountFormatted = bn(sellAmountIncludingProtocolFeesCryptoBaseUnit).toFixed(0) + const slippagePercentage = bn(slippageTolerancePercentageDecimal ?? 0.003) + .times(100) + .toNumber() + + const url = 'https://api.bebop.xyz/pmm/solana/v3/quote' + + const params = new URLSearchParams({ + sell_tokens: sellToken, + buy_tokens: buyToken, + sell_amounts: sellAmountFormatted, + taker_address: takerAddress, + receiver_address: receiverAddress, + slippage: slippagePercentage.toString(), + approval_type: 'Standard', + skip_validation: 'false', + gasless: 'true', + source: 'shapeshift', + }) + + if (affiliateBps && affiliateBps !== '0') { + params.set('fee', affiliateBps) + } + + console.log('[Bebop Solana Fetch] API request:', JSON.stringify({ + url: `${url}?${params}`, + sellToken, + buyToken, + sellAmount: sellAmountFormatted, + takerAddress, + receiverAddress, + slippage: slippagePercentage, + affiliateBps, + })) + + const bebopService = bebopServiceFactory({ apiKey }) + const maybeResponse = await bebopService.get(`${url}?${params}`) + + if (maybeResponse.isErr()) { + console.log('[Bebop Solana Fetch] API error:', JSON.stringify({ + error: maybeResponse.unwrapErr(), + })) + return Err( + makeSwapErrorRight({ + message: 'Failed to fetch quote from Bebop Solana', + cause: maybeResponse.unwrapErr().cause, + code: TradeQuoteError.QueryFailed, + }), + ) + } + + const response = maybeResponse.unwrap() + + console.log('[Bebop Solana Fetch] API response:', JSON.stringify({ + quoteId: response.data.quoteId, + status: response.data.status, + taker: response.data.taker, + receiver: response.data.receiver, + expiry: response.data.expiry, + gasFee: response.data.gasFee, + solanaTxLength: response.data.solana_tx?.length, + blockhash: response.data.blockhash, + priceImpact: response.data.priceImpact, + })) + + if (response.data.status !== 'QUOTE_SUCCESS') { + console.warn('[Bebop Solana Fetch] Expected QUOTE_SUCCESS for gasless=true, got:', response.data.status) + } + + if (!response.data.solana_tx) { + return Err( + makeSwapErrorRight({ + message: 'Missing solana_tx in response', + code: TradeQuoteError.InvalidResponse, + }), + ) + } + + return Ok(response.data) + } catch (error) { + return Err( + makeSwapErrorRight({ + message: 'Unexpected error fetching Bebop Solana quote', + cause: error, + code: TradeQuoteError.QueryFailed, + }), + ) + } +} + +export const fetchBebopSolanaPrice = ({ + buyAsset, + sellAsset, + sellAmountIncludingProtocolFeesCryptoBaseUnit, + receiveAddress, + affiliateBps, + apiKey, +}: { + buyAsset: Asset + sellAsset: Asset + sellAmountIncludingProtocolFeesCryptoBaseUnit: string + receiveAddress: string | undefined + affiliateBps?: string + apiKey: string +}): Promise> => { + const address = receiveAddress || BEBOP_SOLANA_DUMMY_ADDRESS + + return fetchBebopSolanaQuote({ + buyAsset, + sellAsset, + sellAmountIncludingProtocolFeesCryptoBaseUnit, + takerAddress: address, + receiverAddress: address, + slippageTolerancePercentageDecimal: '0.01', + affiliateBps, + apiKey, + }) +} diff --git a/packages/swapper/src/swappers/BebopSwapper/utils/helpers/helpers.ts b/packages/swapper/src/swappers/BebopSwapper/utils/helpers/helpers.ts index d9d158dcdd2..92048dc948f 100644 --- a/packages/swapper/src/swappers/BebopSwapper/utils/helpers/helpers.ts +++ b/packages/swapper/src/swappers/BebopSwapper/utils/helpers/helpers.ts @@ -1,6 +1,7 @@ import type { AssetId, ChainId } from '@shapeshiftoss/caip' -import { fromAssetId } from '@shapeshiftoss/caip' +import { fromAssetId, solAssetId } from '@shapeshiftoss/caip' import type { Asset } from '@shapeshiftoss/types' +import { KnownChainIds } from '@shapeshiftoss/types' import { bn, convertPrecision, isToken } from '@shapeshiftoss/utils' import { Err, Ok } from '@sniptt/monads' import { getAddress } from 'viem' @@ -82,3 +83,15 @@ export const calculateRate = ({ .dividedBy(bn(sellAmount)) .toFixed() } + +export const isSolanaChainId = (chainId: ChainId): boolean => { + return chainId === KnownChainIds.SolanaMainnet +} + +export const assetIdToBebopSolanaToken = (assetId: AssetId): string => { + if (assetId === solAssetId) { + return 'So11111111111111111111111111111111111111112' + } + const { assetReference } = fromAssetId(assetId) + return assetReference +} diff --git a/packages/swapper/src/types.ts b/packages/swapper/src/types.ts index 72c3b57d7cc..72e1a1e6f9e 100644 --- a/packages/swapper/src/types.ts +++ b/packages/swapper/src/types.ts @@ -349,6 +349,8 @@ export type TradeQuoteStep = { value: Hex gas?: string } + bebopSolanaSerializedTx?: string + bebopQuoteId?: string jupiterQuoteResponse?: QuoteResponse solanaTransactionMetadata?: { addressLookupTableAddresses: string[] @@ -566,6 +568,23 @@ export type SolanaTransactionExecutionProps = { signAndBroadcastTransaction: (txToSign: SolanaSignTx) => Promise } +export type SolanaPresignedTx = { + serializedTx: string +} + +export type SolanaPresignedTxExecutionProps = { + signAndBroadcastPresignedTx: (presignedTx: SolanaPresignedTx) => Promise +} + +export type SolanaMessageToSign = { + messageToSign: string + quoteId?: string +} + +export type SolanaMessageExecutionProps = { + signMessage: (message: string) => Promise +} + export type TronTransactionExecutionProps = { signAndBroadcastTransaction: (txToSign: tron.TronSignTx) => Promise } @@ -599,6 +618,10 @@ export type GetUnsignedSolanaTransactionArgs = CommonGetUnsignedTransactionArgs SolanaAccountMetadata & SolanaSwapperDeps +export type GetUnsignedSolanaPresignedTxArgs = CommonGetUnsignedTransactionArgs & + SolanaAccountMetadata & + SolanaSwapperDeps + export type GetUnsignedTronTransactionArgs = CommonGetUnsignedTransactionArgs & TronAccountMetadata & TronSwapperDeps @@ -609,6 +632,11 @@ export type GetUnsignedSuiTransactionArgs = CommonGetUnsignedTransactionArgs & export type GetUnsignedEvmMessageArgs = CommonGetUnsignedTransactionArgs & EvmAccountMetadata & Omit + +export type GetUnsignedSolanaMessageArgs = CommonGetUnsignedTransactionArgs & + SolanaAccountMetadata & + SolanaSwapperDeps + export type GetUnsignedUtxoTransactionArgs = CommonGetUnsignedTransactionArgs & UtxoAccountMetadata & UtxoSwapperDeps @@ -684,6 +712,15 @@ export type Swapper = { txToSign: SolanaSignTx, callbacks: SolanaTransactionExecutionProps, ) => Promise + executeSolanaPresignedTx?: ( + presignedTx: SolanaPresignedTx, + callbacks: SolanaPresignedTxExecutionProps, + ) => Promise + executeSolanaMessage?: ( + messageData: SolanaMessageToSign, + callbacks: SolanaMessageExecutionProps, + apiKey: string, + ) => Promise executeTronTransaction?: ( txToSign: tron.TronSignTx, callbacks: TronTransactionExecutionProps, @@ -710,6 +747,8 @@ export type SwapperApi = { input: GetUnsignedCosmosSdkTransactionArgs, ) => Promise> getUnsignedSolanaTransaction?: (input: GetUnsignedSolanaTransactionArgs) => Promise + getUnsignedSolanaPresignedTx?: (input: GetUnsignedSolanaPresignedTxArgs) => Promise + getUnsignedSolanaMessage?: (input: GetUnsignedSolanaMessageArgs) => Promise getUnsignedTronTransaction?: (input: GetUnsignedTronTransactionArgs) => Promise getUnsignedSuiTransaction?: (input: GetUnsignedSuiTransactionArgs) => Promise @@ -757,6 +796,14 @@ export type SolanaTransactionExecutionInput = CommonTradeExecutionInput & SolanaTransactionExecutionProps & SolanaAccountMetadata +export type SolanaPresignedTxExecutionInput = CommonTradeExecutionInput & + SolanaPresignedTxExecutionProps & + SolanaAccountMetadata + +export type SolanaMessageExecutionInput = CommonTradeExecutionInput & + SolanaMessageExecutionProps & + SolanaAccountMetadata + export type TronTransactionExecutionInput = CommonTradeExecutionInput & TronTransactionExecutionProps & TronAccountMetadata diff --git a/packages/swapper/src/utils.ts b/packages/swapper/src/utils.ts index ece5522f805..2ea2c3b87b8 100644 --- a/packages/swapper/src/utils.ts +++ b/packages/swapper/src/utils.ts @@ -173,11 +173,32 @@ export const executeEvmTransaction = ( return callbacks.signAndBroadcastTransaction(txToSign) } -export const executeSolanaTransaction = ( +export const executeSolanaTransaction = async ( txToSign: SolanaSignTx, callbacks: SolanaTransactionExecutionProps, ) => { - return callbacks.signAndBroadcastTransaction(txToSign) + console.log('[Swapper executeSolanaTransaction] Transaction to sign:', JSON.stringify({ + hasInstructions: !!txToSign.instructions, + instructionsCount: txToSign.instructions?.length, + hasAddressLookupTableAccountInfos: !!txToSign.addressLookupTableAccountInfos, + addressLookupTableAccountInfosCount: txToSign.addressLookupTableAccountInfos?.length, + computeUnitLimit: txToSign.computeUnitLimit, + computeUnitPrice: txToSign.computeUnitPrice, + blockHash: txToSign.blockHash, + to: txToSign.to, + addressNList: txToSign.addressNList, + })) + + console.log('[Swapper executeSolanaTransaction] Full txToSign:', JSON.stringify(txToSign, null, 2)) + + const txHash = await callbacks.signAndBroadcastTransaction(txToSign) + + console.log('[Swapper executeSolanaTransaction] Transaction broadcasted:', JSON.stringify({ + txHash, + txHashLength: txHash?.length, + })) + + return txHash } export const executeTronTransaction = ( diff --git a/src/components/MultiHopTrade/components/TradeConfirm/hooks/useTradeExecution.tsx b/src/components/MultiHopTrade/components/TradeConfirm/hooks/useTradeExecution.tsx index 2182490e3b9..a6321c07f8e 100644 --- a/src/components/MultiHopTrade/components/TradeConfirm/hooks/useTradeExecution.tsx +++ b/src/components/MultiHopTrade/components/TradeConfirm/hooks/useTradeExecution.tsx @@ -511,6 +511,61 @@ export const useTradeExecution = ( pubKey: skipDeviceDerivation ? pubKey : undefined, }) + const step = tradeQuote.steps[hopIndex] + const bebopSolanaSerializedTx = (step as any)?.bebopSolanaSerializedTx + + console.log('[useTradeExecution Solana] Checking for Bebop message flow:', JSON.stringify({ + hasBebopSolanaSerializedTx: !!bebopSolanaSerializedTx, + swapperName, + isBebop: swapperName === SwapperName.Bebop, + willUseBebopMessageFlow: !!bebopSolanaSerializedTx && swapperName === SwapperName.Bebop, + })) + + if (bebopSolanaSerializedTx && swapperName === SwapperName.Bebop) { + console.log('[useTradeExecution] USING BEBOP SOLANA MESSAGE FLOW') + + const output = await execution.execSolanaMessage({ + swapperName, + tradeQuote, + stepIndex: hopIndex, + slippageTolerancePercentageDecimal, + from, + signMessage: async (message: string) => { + // Following Bebop's example: signBytes(privateKey, tx.messageBytes) + const { VersionedTransaction } = await import('@solana/web3.js') + const txBytes = Buffer.from(message, 'base64') + const transaction = VersionedTransaction.deserialize(txBytes) + + // Get the message bytes (this is Bebop's tx.messageBytes) + const messageBytes = transaction.message.serialize() + + // TODO(gomes): if we actually open this, clean me up + // Use the new hdwallet method for signing prebuilt transactions + if ((wallet as any).solanaSignRawTransaction) { + const signatureBase64 = await (wallet as any).solanaSignRawTransaction(message) + + if (!signatureBase64) { + throw new Error('Failed to sign transaction') + } + + const userSignature = new Uint8Array(Buffer.from(signatureBase64, 'base64')) + + console.log('[Bebop Solana] Transaction signed via solanaSignRawTransaction:', { + signatureLength: userSignature.length, + }) + + trackMixpanelEventOnExecute() + return userSignature + } + + throw new Error('Wallet does not support solanaSignRawTransaction for Bebop Solana') + }, + }) + + cancelPollingRef.current = output?.cancelPolling + return + } + const output = await execution.execSolanaTransaction({ swapperName, tradeQuote, diff --git a/src/hooks/useWalletSupportsChain/useWalletSupportsChain.ts b/src/hooks/useWalletSupportsChain/useWalletSupportsChain.ts index 7d4073cc969..27eb9ddd215 100644 --- a/src/hooks/useWalletSupportsChain/useWalletSupportsChain.ts +++ b/src/hooks/useWalletSupportsChain/useWalletSupportsChain.ts @@ -49,7 +49,7 @@ import { } from '@shapeshiftoss/hdwallet-core' import { GridPlusHDWallet } from '@shapeshiftoss/hdwallet-gridplus' import { isMetaMask } from '@shapeshiftoss/hdwallet-metamask-multichain' -import { PhantomHDWallet } from '@shapeshiftoss/hdwallet-phantom' +import { isPhantom, PhantomHDWallet } from '@shapeshiftoss/hdwallet-phantom' import { VultisigHDWallet } from '@shapeshiftoss/hdwallet-vultisig' import { useMemo } from 'react' @@ -79,6 +79,9 @@ const checkWalletHasRuntimeSupport = ({ if (!wallet) return false + // temp disable evm chains for phantom as it rate-limits Phantom. yet that's a thing. + if (isEvmChainId(chainId) && isPhantom(wallet)) return false + // Non-EVM ChainIds are only supported with the MM multichain snap installed if ( isMetaMask(wallet) && @@ -146,51 +149,83 @@ export const walletSupportsChain = ({ switch (chainId) { case btcChainId: + // temp disable evm/BTC chains for phantom as it rate-limits Phantom. yet that's a thing. + if (wallet instanceof PhantomHDWallet) return false return supportsBTC(wallet) case bchChainId: + // temp disable evm/BTC chains for phantom as it rate-limits Phantom. yet that's a thing. + if (wallet instanceof PhantomHDWallet) return false return ( supportsBTC(wallet) && !(wallet instanceof PhantomHDWallet) && !(wallet instanceof GridPlusHDWallet) ) case dogeChainId: + // temp disable evm/BTC chains for phantom as it rate-limits Phantom. yet that's a thing. + if (wallet instanceof PhantomHDWallet) return false return ( supportsBTC(wallet) && !(wallet instanceof PhantomHDWallet) && !(wallet instanceof GridPlusHDWallet) ) case ltcChainId: + // temp disable evm/BTC chains for phantom as it rate-limits Phantom. yet that's a thing. + if (wallet instanceof PhantomHDWallet) return false return ( supportsBTC(wallet) && !(wallet instanceof PhantomHDWallet) && !(wallet instanceof GridPlusHDWallet) ) case zecChainId: + // temp disable evm/BTC chains for phantom as it rate-limits Phantom. yet that's a thing. + if (wallet instanceof PhantomHDWallet) return false return ( supportsBTC(wallet) && (isNativeHDWallet(wallet) || isLedgerHDWallet(wallet) || isTrezorHDWallet(wallet)) ) case ethChainId: + // temp disable evm chains for phantom as it rate-limits Phantom. yet that's a thing. + if (isPhantom(wallet)) return false return supportsETH(wallet) case avalancheChainId: + // temp disable evm chains for phantom as it rate-limits Phantom. yet that's a thing. + if (isPhantom(wallet)) return false return supportsAvalanche(wallet) case optimismChainId: + // temp disable evm chains for phantom as it rate-limits Phantom. yet that's a thing. + if (isPhantom(wallet)) return false return supportsOptimism(wallet) case bscChainId: + // temp disable evm chains for phantom as it rate-limits Phantom. yet that's a thing. + if (isPhantom(wallet)) return false return supportsBSC(wallet) case polygonChainId: + // temp disable evm chains for phantom as it rate-limits Phantom. yet that's a thing. + if (isPhantom(wallet)) return false return supportsPolygon(wallet) case gnosisChainId: + // temp disable evm chains for phantom as it rate-limits Phantom. yet that's a thing. + if (isPhantom(wallet)) return false return supportsGnosis(wallet) case arbitrumChainId: + // temp disable evm chains for phantom as it rate-limits Phantom. yet that's a thing. + if (isPhantom(wallet)) return false return supportsArbitrum(wallet) case arbitrumNovaChainId: + // temp disable evm chains for phantom as it rate-limits Phantom. yet that's a thing. + if (isPhantom(wallet)) return false return isArbitrumNovaEnabled && supportsArbitrumNova(wallet) case baseChainId: + // temp disable evm chains for phantom as it rate-limits Phantom. yet that's a thing. + if (isPhantom(wallet)) return false return supportsBase(wallet) case monadChainId: + // temp disable evm chains for phantom as it rate-limits Phantom. yet that's a thing. + if (isPhantom(wallet)) return false return isMonadEnabled && supportsMonad(wallet) case hyperEvmChainId: + // temp disable evm chains for phantom as it rate-limits Phantom. yet that's a thing. + if (isPhantom(wallet)) return false return isHyperEvmEnabled && supportsHyperEvm(wallet) case plasmaChainId: return isPlasmaEnabled && supportsPlasma(wallet) diff --git a/src/lib/tradeExecution.ts b/src/lib/tradeExecution.ts index f0062bc467d..c02491b59ed 100644 --- a/src/lib/tradeExecution.ts +++ b/src/lib/tradeExecution.ts @@ -7,6 +7,8 @@ import type { EvmTransactionExecutionInput, RelayerTxDetailsArgs, SellTxHashArgs, + SolanaMessageExecutionInput, + SolanaPresignedTxExecutionInput, SolanaTransactionExecutionInput, StatusArgs, SuiTransactionExecutionInput, @@ -502,6 +504,78 @@ export class TradeExecution { ) } + async execSolanaPresignedTx({ + swapperName, + tradeQuote, + stepIndex, + slippageTolerancePercentageDecimal, + from, + signAndBroadcastPresignedTx, + }: SolanaPresignedTxExecutionInput) { + console.log('[tradeExecution execSolanaPresignedTx] Starting presigned Solana flow:', JSON.stringify({ + swapperName, + stepIndex, + from, + })) + + const buildSignBroadcast = async ( + swapper: Swapper & SwapperApi, + { + tradeQuote, + chainId, + stepIndex, + slippageTolerancePercentageDecimal, + config, + }: CommonGetUnsignedTransactionArgs, + ) => { + console.log('[tradeExecution buildSignBroadcast] Checking swapper methods:', JSON.stringify({ + hasGetUnsignedSolanaPresignedTx: !!swapper.getUnsignedSolanaPresignedTx, + hasExecuteSolanaPresignedTx: !!swapper.executeSolanaPresignedTx, + })) + + if (!swapper.getUnsignedSolanaPresignedTx) { + throw Error('missing implementation for getUnsignedSolanaPresignedTx') + } + if (!swapper.executeSolanaPresignedTx) { + throw Error('missing implementation for executeSolanaPresignedTx') + } + + const presignedTx = await swapper.getUnsignedSolanaPresignedTx({ + tradeQuote, + chainId, + stepIndex, + slippageTolerancePercentageDecimal, + from, + config, + assertGetSolanaChainAdapter, + }) + + console.log('[tradeExecution buildSignBroadcast] Got presigned tx:', JSON.stringify({ + serializedTxLength: presignedTx.serializedTx.length, + })) + + const result = await swapper.executeSolanaPresignedTx(presignedTx, { + signAndBroadcastPresignedTx, + }) + + console.log('[tradeExecution buildSignBroadcast] Execution result:', JSON.stringify({ + txHash: result, + })) + + return result + } + + return await this._execWalletAgnostic( + { + swapperName, + tradeQuote, + stepIndex, + slippageTolerancePercentageDecimal, + }, + buildSignBroadcast, + ) + } + async execSolanaTransaction({ swapperName, tradeQuote, @@ -553,6 +627,61 @@ export class TradeExecution { ) } + async execSolanaMessage({ + swapperName, + tradeQuote, + stepIndex, + slippageTolerancePercentageDecimal, + from, + signMessage, + }: SolanaMessageExecutionInput) { + const buildSignBroadcast = async ( + swapper: Swapper & SwapperApi, + { + tradeQuote, + chainId, + stepIndex, + slippageTolerancePercentageDecimal, + config, + }: CommonGetUnsignedTransactionArgs, + ) => { + if (!swapper.getUnsignedSolanaMessage) { + throw Error('missing implementation for getUnsignedSolanaMessage') + } + if (!swapper.executeSolanaMessage) { + throw Error('missing implementation for executeSolanaMessage') + } + + const messageResult = await swapper.getUnsignedSolanaMessage({ + tradeQuote, + chainId, + stepIndex, + slippageTolerancePercentageDecimal, + from, + config, + assertGetSolanaChainAdapter, + }) + + // Get the API key from config for Bebop + const apiKey = config.VITE_BEBOP_API_KEY + if (!apiKey) { + throw Error('Bebop API key is required for Solana message execution') + } + + return await swapper.executeSolanaMessage(messageResult, { signMessage }, apiKey) + } + + return await this._execWalletAgnostic( + { + swapperName, + tradeQuote, + stepIndex, + slippageTolerancePercentageDecimal, + }, + buildSignBroadcast, + ) + } + async execTronTransaction({ swapperName, tradeQuote, diff --git a/yarn.lock b/yarn.lock index ea666b1af19..f53aa72acaf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9398,6 +9398,13 @@ __metadata: languageName: node linkType: hard +"@noble/hashes@npm:^2.0.1": + version: 2.0.1 + resolution: "@noble/hashes@npm:2.0.1" + checksum: aea67a671ca464027cd512d07666a804bd99abc343ba98c93ee8463a1c870c5594f8de468279ef3c60f3b23b6330db9f1a92bd23723b8a911ec73bddc9420248 + languageName: node + linkType: hard + "@noble/hashes@npm:~1.3.0": version: 1.3.0 resolution: "@noble/hashes@npm:1.3.0" @@ -11165,8 +11172,8 @@ __metadata: dependencies: "@mysten/sui": 1.45.0 "@shapeshiftoss/caip": "workspace:^" - "@shapeshiftoss/hdwallet-core": 1.62.28 - "@shapeshiftoss/hdwallet-ledger": 1.62.28 + "@shapeshiftoss/hdwallet-core": 1.62.29-bebop-solana-msg.5 + "@shapeshiftoss/hdwallet-ledger": 1.62.29-bebop-solana-msg.5 "@shapeshiftoss/types": "workspace:^" "@shapeshiftoss/unchained-client": "workspace:^" "@shapeshiftoss/utils": "workspace:^" @@ -11229,15 +11236,15 @@ __metadata: languageName: unknown linkType: soft -"@shapeshiftoss/hdwallet-coinbase@npm:1.62.28": - version: 1.62.28 - resolution: "@shapeshiftoss/hdwallet-coinbase@npm:1.62.28" +"@shapeshiftoss/hdwallet-coinbase@npm:1.62.29-bebop-solana-msg.5": + version: 1.62.29-bebop-solana-msg.5 + resolution: "@shapeshiftoss/hdwallet-coinbase@npm:1.62.29-bebop-solana-msg.5" dependencies: "@coinbase/wallet-sdk": ^3.6.6 - "@shapeshiftoss/hdwallet-core": 1.62.28 + "@shapeshiftoss/hdwallet-core": ^1.62.29-bebop-solana-msg.5 eth-rpc-errors: ^4.0.3 lodash: ^4.17.21 - checksum: ab3c60fbd7f74316ca65a0d0c78c6118690be58244de5603b461fda88e8060b7568fed4a47069d7fb087fe529c8b986259cc501d4904c8d9f6155410e990b7e2 + checksum: d3365bb0e0e00a9821e79da6c06c48d1b04209d99eb9ec184ad46c323804b565d537853e7e617c8bbdacd676513a88e8e9987f6ef4497a5dd99e4c86d5632cf5 languageName: node linkType: hard @@ -11259,6 +11266,42 @@ __metadata: languageName: node linkType: hard +"@shapeshiftoss/hdwallet-core@npm:1.62.29-bebop-solana-msg.5": + version: 1.62.29-bebop-solana-msg.5 + resolution: "@shapeshiftoss/hdwallet-core@npm:1.62.29-bebop-solana-msg.5" + dependencies: + "@shapeshiftoss/bitcoinjs-lib": 7.0.0-shapeshift.2 + "@shapeshiftoss/proto-tx-builder": 0.10.0 + "@solana/web3.js": 1.95.8 + bs58check: ^4.0.0 + eip-712: ^1.0.0 + ethers: 5.7.2 + eventemitter2: ^5.0.1 + lodash: ^4.17.21 + rxjs: ^6.4.0 + type-assertions: ^1.1.0 + checksum: a8e0083bc598bcf0ccb25dfabbf6d7140e23ba616f1b86f7ef4691c9f34089d14e590b9740ef85c32f2a06832c984acfdf7fa0ed8b28de7771949659f453ed3b + languageName: node + linkType: hard + +"@shapeshiftoss/hdwallet-core@npm:^1.62.29-bebop-solana-msg.5, @shapeshiftoss/hdwallet-core@npm:^1.62.29-second-class-mm.3": + version: 1.62.29-second-class-mm.3 + resolution: "@shapeshiftoss/hdwallet-core@npm:1.62.29-second-class-mm.3" + dependencies: + "@shapeshiftoss/bitcoinjs-lib": 7.0.0-shapeshift.2 + "@shapeshiftoss/proto-tx-builder": 0.10.0 + "@solana/web3.js": 1.95.8 + bs58check: ^4.0.0 + eip-712: ^1.0.0 + ethers: 5.7.2 + eventemitter2: ^5.0.1 + lodash: ^4.17.21 + rxjs: ^6.4.0 + type-assertions: ^1.1.0 + checksum: 7225d124faadf2103f2291ed31e90aae929c67f4edc143c668de9a17e6e93b79b06b141652cbfa378ff774c4eb95c43d60b679970f878e731954a1840d4a749f + languageName: node + linkType: hard + "@shapeshiftoss/hdwallet-core@npm:latest": version: 1.62.27 resolution: "@shapeshiftoss/hdwallet-core@npm:1.62.27" @@ -11277,39 +11320,39 @@ __metadata: languageName: node linkType: hard -"@shapeshiftoss/hdwallet-gridplus@npm:1.62.28": - version: 1.62.28 - resolution: "@shapeshiftoss/hdwallet-gridplus@npm:1.62.28" +"@shapeshiftoss/hdwallet-gridplus@npm:1.62.29-bebop-solana-msg.5": + version: 1.62.29-bebop-solana-msg.5 + resolution: "@shapeshiftoss/hdwallet-gridplus@npm:1.62.29-bebop-solana-msg.5" dependencies: "@bitcoinerlab/secp256k1": ^1.1.1 "@ethereumjs/common": 4.4.0 "@ethereumjs/rlp": 5.0.2 "@ethereumjs/tx": 5.4.0 "@metamask/eth-sig-util": ^7.0.0 - "@shapeshiftoss/hdwallet-core": 1.62.28 + "@shapeshiftoss/hdwallet-core": ^1.62.29-bebop-solana-msg.5 bech32: ^1.1.4 bs58: ^5.0.0 bs58check: ^4.0.0 crypto-js: ^4.2.0 gridplus-sdk: ^3.2.0 lodash: ^4.17.21 - checksum: 6384ec1426915df4b4f559a58113519f858afa407ead15e9d003f5793cf34322fd78116b571650bcc50346d84aaaa069f6b7186a32ee687714ac2ba170701a5e + checksum: ceac953490f14834055cadb5dfe0c388da1f5054b2095fd06f9fddbe58c10f0c1849dc9d45c4379b072c7e3fcc1ff8b95e416dc85a56b2fc828ebc9f611220e3 languageName: node linkType: hard -"@shapeshiftoss/hdwallet-keepkey-webusb@npm:1.62.28": - version: 1.62.28 - resolution: "@shapeshiftoss/hdwallet-keepkey-webusb@npm:1.62.28" +"@shapeshiftoss/hdwallet-keepkey-webusb@npm:1.62.29-bebop-solana-msg.5": + version: 1.62.29-bebop-solana-msg.5 + resolution: "@shapeshiftoss/hdwallet-keepkey-webusb@npm:1.62.29-bebop-solana-msg.5" dependencies: - "@shapeshiftoss/hdwallet-core": 1.62.28 - "@shapeshiftoss/hdwallet-keepkey": 1.62.28 - checksum: 82e7230c4eb2302b5071445dc9efffdb47182cc66c9d19a3b5c232052cce1ad01e3fc97945e84b9ff02284f00fd65c2622c2e01918519de42591f460a9f557c7 + "@shapeshiftoss/hdwallet-core": ^1.62.29-bebop-solana-msg.5 + "@shapeshiftoss/hdwallet-keepkey": ^1.62.29-bebop-solana-msg.5 + checksum: e4489ccbeb81b910591d04c9e295df8cd90407383d63e50b31a562118dfd0c646fe7cdd50af6033f32b8dc44c0952753dc4179dfba43fec58a5d41c10d53d676 languageName: node linkType: hard -"@shapeshiftoss/hdwallet-keepkey@npm:1.62.28": - version: 1.62.28 - resolution: "@shapeshiftoss/hdwallet-keepkey@npm:1.62.28" +"@shapeshiftoss/hdwallet-keepkey@npm:1.62.29-bebop-solana-msg.5": + version: 1.62.29-bebop-solana-msg.5 + resolution: "@shapeshiftoss/hdwallet-keepkey@npm:1.62.29-bebop-solana-msg.5" dependencies: "@bitcoinerlab/secp256k1": ^1.1.1 "@ethereumjs/common": 3.2.0 @@ -11317,7 +11360,7 @@ __metadata: "@keepkey/device-protocol": 7.13.4 "@metamask/eth-sig-util": ^7.0.0 "@shapeshiftoss/bitcoinjs-lib": 7.0.0-shapeshift.2 - "@shapeshiftoss/hdwallet-core": 1.62.28 + "@shapeshiftoss/hdwallet-core": ^1.62.29-bebop-solana-msg.5 "@shapeshiftoss/proto-tx-builder": 0.10.0 bignumber.js: ^9.0.1 bnb-javascript-sdk-nobroadcast: 2.16.15 @@ -11330,44 +11373,71 @@ __metadata: lodash: ^4.17.21 p-lazy: ^3.1.0 semver: ^7.3.8 - checksum: 88f737e8e53df0659e2bfded6198536623bd6798204c058915c57f28e36e36033d4d93050d3ec6196719d477615df8e4d259d5805801ddcee62372818b0135ca + checksum: 8a64a5b26d6677d0376bf10adf7e1c99c419448fe8d63b6653657f602e5baff679175fcd6780c14d66c427fad3c295fa777f92ed82c7cd48ef2455c63545d04f languageName: node linkType: hard -"@shapeshiftoss/hdwallet-keplr@npm:1.62.28": - version: 1.62.28 - resolution: "@shapeshiftoss/hdwallet-keplr@npm:1.62.28" +"@shapeshiftoss/hdwallet-keepkey@npm:^1.62.29-bebop-solana-msg.5": + version: 1.62.29-second-class-mm.3 + resolution: "@shapeshiftoss/hdwallet-keepkey@npm:1.62.29-second-class-mm.3" + dependencies: + "@bitcoinerlab/secp256k1": ^1.1.1 + "@ethereumjs/common": 3.2.0 + "@ethereumjs/tx": 4.2.0 + "@keepkey/device-protocol": 7.13.4 + "@metamask/eth-sig-util": ^7.0.0 + "@shapeshiftoss/bitcoinjs-lib": 7.0.0-shapeshift.2 + "@shapeshiftoss/hdwallet-core": ^1.62.29-second-class-mm.3 + "@shapeshiftoss/proto-tx-builder": 0.10.0 + bignumber.js: ^9.0.1 + bnb-javascript-sdk-nobroadcast: 2.16.15 + bs58: ^5.0.0 + bs58check: ^4.0.0 + crypto-js: ^4.0.0 + eip55: ^2.1.0 + google-protobuf: ^3.15.8 + icepick: ^2.4.0 + lodash: ^4.17.21 + p-lazy: ^3.1.0 + semver: ^7.3.8 + checksum: 5bc98305cbc3ef28085c7908135866a1504e7ec07746285042ce614571feceef64ad547c73e684cb67e27c04ee4aec28aae0cb0f8c860146056c0efa66f95331 + languageName: node + linkType: hard + +"@shapeshiftoss/hdwallet-keplr@npm:1.62.29-bebop-solana-msg.5": + version: 1.62.29-bebop-solana-msg.5 + resolution: "@shapeshiftoss/hdwallet-keplr@npm:1.62.29-bebop-solana-msg.5" dependencies: "@cosmjs/amino": ^0.28.13 "@cosmjs/stargate": ^0.28.13 "@shapeshiftoss/caip": 8.15.0 - "@shapeshiftoss/hdwallet-core": 1.62.28 + "@shapeshiftoss/hdwallet-core": ^1.62.29-bebop-solana-msg.5 "@shapeshiftoss/proto-tx-builder": 0.10.0 "@shapeshiftoss/types": 3.1.3 base64-js: ^1.5.1 lodash: ^4.17.21 - checksum: 06404a7ce0f0f152564209b1f965d53d51e5296b445f33357b1d72f9362a287c7c7a4223b7a092b7bf6e1aa99a660df800d00686510d2b382159999b913c63c2 + checksum: 8e575b5967e4ec99978cb2140a08f90d9add391f68682ffbebf022fce129d8fd1bbc36cccbe9a2dd61c64df3be7dabf9709960850d937f27ccd487520369b620 languageName: node linkType: hard -"@shapeshiftoss/hdwallet-ledger-webhid@npm:1.62.28": - version: 1.62.28 - resolution: "@shapeshiftoss/hdwallet-ledger-webhid@npm:1.62.28" +"@shapeshiftoss/hdwallet-ledger-webhid@npm:1.62.29-bebop-solana-msg.5": + version: 1.62.29-bebop-solana-msg.5 + resolution: "@shapeshiftoss/hdwallet-ledger-webhid@npm:1.62.29-bebop-solana-msg.5" dependencies: "@ledgerhq/hw-app-btc": 10.13.0 "@ledgerhq/hw-app-eth": 7.0.0 "@ledgerhq/hw-transport": 6.31.13 "@ledgerhq/hw-transport-webhid": 6.30.6 - "@shapeshiftoss/hdwallet-core": 1.62.28 - "@shapeshiftoss/hdwallet-ledger": 1.62.28 + "@shapeshiftoss/hdwallet-core": ^1.62.29-bebop-solana-msg.5 + "@shapeshiftoss/hdwallet-ledger": ^1.62.29-bebop-solana-msg.5 "@types/w3c-web-hid": ^1.0.2 - checksum: 888af25fd11ed7e56a9f6be997d0c542945428e271c8f21a5aa9f9df366829f72306fb4f46ed55a9f5c0510906df8c99da1367209e327be8fc566052998fad2d + checksum: 0b68bdbb9993ed62b9a2eedd6b8abaf756b1b1abf440f29fba50ef87c787b72f2d6c2264122767390a96d05f77bf03e9e5b4a58fe268ce86d0de6dd6741c9245 languageName: node linkType: hard -"@shapeshiftoss/hdwallet-ledger-webusb@npm:1.62.28": - version: 1.62.28 - resolution: "@shapeshiftoss/hdwallet-ledger-webusb@npm:1.62.28" +"@shapeshiftoss/hdwallet-ledger-webusb@npm:1.62.29-bebop-solana-msg.5": + version: 1.62.29-bebop-solana-msg.5 + resolution: "@shapeshiftoss/hdwallet-ledger-webusb@npm:1.62.29-bebop-solana-msg.5" dependencies: "@ledgerhq/hw-app-btc": 10.13.0 "@ledgerhq/hw-app-cosmos": 6.32.9 @@ -11375,17 +11445,17 @@ __metadata: "@ledgerhq/hw-app-solana": 7.6.0 "@ledgerhq/hw-transport": 6.31.13 "@ledgerhq/hw-transport-webusb": 6.29.13 - "@shapeshiftoss/hdwallet-core": 1.62.28 - "@shapeshiftoss/hdwallet-ledger": 1.62.28 + "@shapeshiftoss/hdwallet-core": ^1.62.29-bebop-solana-msg.5 + "@shapeshiftoss/hdwallet-ledger": ^1.62.29-bebop-solana-msg.5 "@types/w3c-web-usb": ^1.0.4 p-queue: ^7.4.1 - checksum: 7cc44a99f84c0f83784afb612148cbcb5544160afc6166ac7f03ddb04520670d9bfaae1cdb7c68d1761d4c9b7c7ab5e7e22e23097094d7a56835fd0299b4a03a + checksum: be4412c5496e2af46a2193c0820ff76e87e3af3e93496f7b2bae5a09c910c4368e311655c56a3b50410310e5be805dfad960cf8941337364e7fdced464941b75 languageName: node linkType: hard -"@shapeshiftoss/hdwallet-ledger@npm:1.62.28": - version: 1.62.28 - resolution: "@shapeshiftoss/hdwallet-ledger@npm:1.62.28" +"@shapeshiftoss/hdwallet-ledger@npm:1.62.29-bebop-solana-msg.5": + version: 1.62.29-bebop-solana-msg.5 + resolution: "@shapeshiftoss/hdwallet-ledger@npm:1.62.29-bebop-solana-msg.5" dependencies: "@bitgo/utxo-lib": ^11.18.0 "@ethereumjs/common": 3.2.0 @@ -11400,7 +11470,7 @@ __metadata: "@ledgerhq/logs": 6.13.0 "@mysten/ledgerjs-hw-app-sui": ^0.7.0 "@shapeshiftoss/bitcoinjs-lib": 7.0.0-shapeshift.2 - "@shapeshiftoss/hdwallet-core": 1.62.28 + "@shapeshiftoss/hdwallet-core": ^1.62.29-bebop-solana-msg.5 "@solana/web3.js": 1.95.8 base64-js: ^1.5.1 bchaddrjs: ^0.4.4 @@ -11410,33 +11480,64 @@ __metadata: ethereumjs-util: ^6.1.0 ethers: 5.7.2 lodash: ^4.17.21 - checksum: 482e26ba4a2f94b6b1c7a2ec3fb47a2e75a49a199e462eb8ebbe784fcd791f3237b2a0bf4d473ccb07a27b4baffe22f3f5cb080449e92826a53fb668f7108591 + checksum: d08201a2def0560dac1275f82b8de23087a229d8feaf22cfdb66cd52bf6ba533f6b3eca02ae458c964dfaf79188751913a54d8b2278ae6b54cef9a901a7c232d languageName: node linkType: hard -"@shapeshiftoss/hdwallet-metamask-multichain@npm:1.62.28": - version: 1.62.28 - resolution: "@shapeshiftoss/hdwallet-metamask-multichain@npm:1.62.28" +"@shapeshiftoss/hdwallet-ledger@npm:^1.62.29-bebop-solana-msg.5": + version: 1.62.29-second-class-mm.3 + resolution: "@shapeshiftoss/hdwallet-ledger@npm:1.62.29-second-class-mm.3" + dependencies: + "@bitgo/utxo-lib": ^11.18.0 + "@ethereumjs/common": 3.2.0 + "@ethereumjs/tx": 4.2.0 + "@ledgerhq/device-core": 0.6.9 + "@ledgerhq/hw-app-btc": 10.13.0 + "@ledgerhq/hw-app-cosmos": 6.32.9 + "@ledgerhq/hw-app-eth": 7.0.0 + "@ledgerhq/hw-app-solana": 7.6.0 + "@ledgerhq/hw-app-trx": 6.31.9 + "@ledgerhq/hw-transport": 6.31.13 + "@ledgerhq/logs": 6.13.0 + "@mysten/ledgerjs-hw-app-sui": ^0.7.0 + "@shapeshiftoss/bitcoinjs-lib": 7.0.0-shapeshift.2 + "@shapeshiftoss/hdwallet-core": ^1.62.29-second-class-mm.3 + "@solana/web3.js": 1.95.8 + base64-js: ^1.5.1 + bchaddrjs: ^0.4.4 + bitcoinjs-message: ^2.0.0 + bs58check: ^4.0.0 + ethereumjs-tx: 1.3.7 + ethereumjs-util: ^6.1.0 + ethers: 5.7.2 + lodash: ^4.17.21 + checksum: 737d986c5695f32658befeed9247bd3c0b62ad05482700cd2b02dff0d14e4d624b08cac37de5af28d7bb3b87d5aeae5000fa1291588a199eff45ec8465ff957a + languageName: node + linkType: hard + +"@shapeshiftoss/hdwallet-metamask-multichain@npm:1.62.29-bebop-solana-msg.5": + version: 1.62.29-bebop-solana-msg.5 + resolution: "@shapeshiftoss/hdwallet-metamask-multichain@npm:1.62.29-bebop-solana-msg.5" dependencies: "@metamask/detect-provider": ^1.2.0 "@metamask/onboarding": ^1.0.1 "@shapeshiftoss/common-api": ^9.3.0 - "@shapeshiftoss/hdwallet-core": 1.62.28 + "@shapeshiftoss/hdwallet-core": ^1.62.29-bebop-solana-msg.5 "@shapeshiftoss/metamask-snaps-adapter": ^1.0.12 "@shapeshiftoss/metamask-snaps-types": ^1.0.12 eth-rpc-errors: ^4.0.3 lodash: ^4.17.21 mipd: ^0.0.7 - checksum: 86ac33b6265d4cf35c90879a98ace320f1e2e10adc5561dfdc3a3580ee082c1c3537c8b5ca4341d3b5828e77b0cac7c03e49fd2dbd70bf6cdd4f451001f3438e + checksum: a931a668396411b04527fd41e152c16629d542d266a9c106273acc717331710d34d626751ef032ee6dd854bebb10a0a2a94fe7d1431f0562ff09c1cee9e9b923 languageName: node linkType: hard -"@shapeshiftoss/hdwallet-native-vault@npm:1.62.28": - version: 1.62.28 - resolution: "@shapeshiftoss/hdwallet-native-vault@npm:1.62.28" +"@shapeshiftoss/hdwallet-native-vault@npm:1.62.29-bebop-solana-msg.5": + version: 1.62.29-bebop-solana-msg.5 + resolution: "@shapeshiftoss/hdwallet-native-vault@npm:1.62.29-bebop-solana-msg.5" dependencies: "@bitcoinerlab/secp256k1": ^1.1.1 - "@shapeshiftoss/hdwallet-native": 1.62.28 + "@shapeshiftoss/hdwallet-native": ^1.62.29-bebop-solana-msg.5 bip39: ^3.0.4 hash-wasm: ^4.11.0 idb-keyval: ^6.0.3 @@ -11444,11 +11545,44 @@ __metadata: p-lazy: ^3.1.0 type-assertions: ^1.1.0 uuid: ^8.3.2 - checksum: 8cfe8d66a7270f7e1c119be118ffec148ad1249eebf288f4715a6acd37a4a6324aedbfe14614937c4d542f3a9c7c5b13f51ceec21d644d852811c97424272a63 + checksum: 2ec1749e457ab07a6226d518476142ead9a3bff1644fababda5b31ca84e977bf81abbdbb70e68ed1bb3971f8c25da2db9af0bb636efcdd446b3fb98dcf84bdbf languageName: node linkType: hard -"@shapeshiftoss/hdwallet-native@npm:1.62.28, @shapeshiftoss/hdwallet-native@npm:^1.55.1": +"@shapeshiftoss/hdwallet-native@npm:1.62.29-bebop-solana-msg.5": + version: 1.62.29-bebop-solana-msg.5 + resolution: "@shapeshiftoss/hdwallet-native@npm:1.62.29-bebop-solana-msg.5" + dependencies: + "@bitcoinerlab/secp256k1": ^1.1.1 + "@noble/curves": ^1.4.0 + "@shapeshiftoss/bitcoinjs-lib": 7.0.0-shapeshift.2 + "@shapeshiftoss/hdwallet-core": ^1.62.29-bebop-solana-msg.5 + "@shapeshiftoss/proto-tx-builder": 0.10.0 + "@zxing/text-encoding": ^0.9.0 + bchaddrjs: ^0.4.9 + bech32: ^1.1.4 + bignumber.js: ^9.0.1 + bip32: ^2.0.5 + bip39: ^3.0.2 + bnb-javascript-sdk-nobroadcast: 2.16.15 + bs58check: ^4.0.0 + crypto-js: ^4.0.0 + ecpair: ^3.0.0-rc.0 + eip-712: ^1.0.0 + ethers: 5.7.2 + eventemitter2: ^5.0.1 + funtypes: ^3.0.1 + hash-wasm: ^4.11.0 + lodash: ^4.17.21 + node-fetch: ^2.6.1 + p-lazy: ^3.1.0 + scrypt-js: ^3.0.1 + tendermint-tx-builder: 1.0.16 + checksum: 61b6d586e88002494b45ca4f7bae84df6fb79db97fe4bb8a43c3290783a7c57d95277f7b0a509338250db8b947ea96db07656805d3966ed6ba569dbe35ee6748 + languageName: node + linkType: hard + +"@shapeshiftoss/hdwallet-native@npm:^1.55.1": version: 1.62.28 resolution: "@shapeshiftoss/hdwallet-native@npm:1.62.28" dependencies: @@ -11481,84 +11615,131 @@ __metadata: languageName: node linkType: hard -"@shapeshiftoss/hdwallet-phantom@npm:1.62.28": - version: 1.62.28 - resolution: "@shapeshiftoss/hdwallet-phantom@npm:1.62.28" +"@shapeshiftoss/hdwallet-native@npm:^1.62.29-bebop-solana-msg.5": + version: 1.62.29-second-class-mm.3 + resolution: "@shapeshiftoss/hdwallet-native@npm:1.62.29-second-class-mm.3" dependencies: + "@bitcoinerlab/secp256k1": ^1.1.1 + "@noble/curves": ^1.4.0 "@shapeshiftoss/bitcoinjs-lib": 7.0.0-shapeshift.2 - "@shapeshiftoss/hdwallet-core": 1.62.28 + "@shapeshiftoss/hdwallet-core": ^1.62.29-second-class-mm.3 + "@shapeshiftoss/proto-tx-builder": 0.10.0 + "@zxing/text-encoding": ^0.9.0 + bchaddrjs: ^0.4.9 + bech32: ^1.1.4 + bignumber.js: ^9.0.1 + bip32: ^2.0.5 + bip39: ^3.0.2 + bnb-javascript-sdk-nobroadcast: 2.16.15 + bs58check: ^4.0.0 + crypto-js: ^4.0.0 + ecpair: ^3.0.0-rc.0 + eip-712: ^1.0.0 + ethers: 5.7.2 + eventemitter2: ^5.0.1 + funtypes: ^3.0.1 + hash-wasm: ^4.11.0 + lodash: ^4.17.21 + node-fetch: ^2.6.1 + p-lazy: ^3.1.0 + scrypt-js: ^3.0.1 + tendermint-tx-builder: 1.0.16 + checksum: 230e89d76e20e0bb9d62f30b9049b9c49a3db84d15ad302e91558b7cdf0ae1f32340cab3d87eb5ffe1a50cd88be1434a5fef58c617f5cc99f768cea7196c6b4e + languageName: node + linkType: hard + +"@shapeshiftoss/hdwallet-phantom@npm:1.62.29-bebop-solana-msg.5": + version: 1.62.29-bebop-solana-msg.5 + resolution: "@shapeshiftoss/hdwallet-phantom@npm:1.62.29-bebop-solana-msg.5" + dependencies: + "@shapeshiftoss/bitcoinjs-lib": 7.0.0-shapeshift.2 + "@shapeshiftoss/hdwallet-core": ^1.62.29-bebop-solana-msg.5 "@solana/web3.js": 1.95.8 base64-js: ^1.5.1 bitcoinjs-message: ^2.0.0 ethers: 5.7.2 lodash: ^4.17.21 - checksum: 3ec6856d8a7432720c4891ee1e65f5e63c85e9c4f57c230b4d98961cb3c4870eaf6bb9ee8c26f5e9645d5a66121770ac64a8723d51101e67b2ecb211926aeff1 + checksum: 044dca3d910db2003c8bfd11adb2796d02634d7089b5cce8231c619c56a1d0ec3fd48fabe710e4179fe77f49dae89392fc7977f16acfe756c47b229f68081928 languageName: node linkType: hard -"@shapeshiftoss/hdwallet-trezor-connect@npm:1.62.28": - version: 1.62.28 - resolution: "@shapeshiftoss/hdwallet-trezor-connect@npm:1.62.28" +"@shapeshiftoss/hdwallet-trezor-connect@npm:1.62.29-bebop-solana-msg.5": + version: 1.62.29-bebop-solana-msg.5 + resolution: "@shapeshiftoss/hdwallet-trezor-connect@npm:1.62.29-bebop-solana-msg.5" dependencies: - "@shapeshiftoss/hdwallet-core": 1.62.28 - "@shapeshiftoss/hdwallet-trezor": 1.62.28 + "@shapeshiftoss/hdwallet-core": ^1.62.29-bebop-solana-msg.5 + "@shapeshiftoss/hdwallet-trezor": ^1.62.29-bebop-solana-msg.5 "@trezor/connect-web": ^9.6.4 - checksum: 61f19a8022494dadf0fedef4746df077eefd36b3d4a66b8b7df34c00dcd41eaf59aab38093c6a0e6648d640582ef86f0b1a09f315cf5a62541669c6c238fc883 + checksum: ba97d765521b67759bdc321ade251f14c6f53c56a037da29d0354ca68b4677dc2ee3fe47ac4648f6a969a0bf282e7e79a9bc130af5a966272f626221e8fd05eb languageName: node linkType: hard -"@shapeshiftoss/hdwallet-trezor@npm:1.62.28": - version: 1.62.28 - resolution: "@shapeshiftoss/hdwallet-trezor@npm:1.62.28" +"@shapeshiftoss/hdwallet-trezor@npm:1.62.29-bebop-solana-msg.5": + version: 1.62.29-bebop-solana-msg.5 + resolution: "@shapeshiftoss/hdwallet-trezor@npm:1.62.29-bebop-solana-msg.5" dependencies: "@ethereumjs/common": 3.2.0 "@ethereumjs/tx": 4.2.0 - "@shapeshiftoss/hdwallet-core": 1.62.28 + "@shapeshiftoss/hdwallet-core": ^1.62.29-bebop-solana-msg.5 base64-js: ^1.5.1 bchaddrjs: ^0.4.4 lodash: ^4.17.21 - checksum: 37dd03d15a5797c10012f48be8823c97fa960ca29a2b9583f3c2c37a1fec2a07db882ab5e81cb65a753cf2730576320a18def7bd513439e3f05868adeec7ebad + checksum: a6e98eb72d0d73cc84bc23a554a50128738580f1c3e35ed07fd4a9c0340928f0e38f520ee774cbd449a0d39c1dbc1d11935736373a070fb4aa23bd90d1b139c3 languageName: node linkType: hard -"@shapeshiftoss/hdwallet-vultisig@npm:1.62.28": - version: 1.62.28 - resolution: "@shapeshiftoss/hdwallet-vultisig@npm:1.62.28" +"@shapeshiftoss/hdwallet-trezor@npm:^1.62.29-bebop-solana-msg.5": + version: 1.62.29-second-class-mm.3 + resolution: "@shapeshiftoss/hdwallet-trezor@npm:1.62.29-second-class-mm.3" + dependencies: + "@ethereumjs/common": 3.2.0 + "@ethereumjs/tx": 4.2.0 + "@shapeshiftoss/hdwallet-core": ^1.62.29-second-class-mm.3 + base64-js: ^1.5.1 + bchaddrjs: ^0.4.4 + lodash: ^4.17.21 + checksum: 5b1456d6ee61b8e82836bfb5b23e4b31cb74b568bb5798eaf3fe4f75e0b2beccb858578abdc95050443496ac948a1722338282d42e97a30c6609e625a355ac69 + languageName: node + linkType: hard + +"@shapeshiftoss/hdwallet-vultisig@npm:1.62.29-bebop-solana-msg.5": + version: 1.62.29-bebop-solana-msg.5 + resolution: "@shapeshiftoss/hdwallet-vultisig@npm:1.62.29-bebop-solana-msg.5" dependencies: "@cosmjs/amino": ^0.28.13 "@cosmjs/stargate": ^0.28.13 "@shapeshiftoss/bitcoinjs-lib": 7.0.0-shapeshift.2 - "@shapeshiftoss/hdwallet-core": 1.62.28 + "@shapeshiftoss/hdwallet-core": ^1.62.29-bebop-solana-msg.5 "@solana/web3.js": 1.95.8 base64-js: ^1.5.1 bitcoinjs-message: ^2.0.0 ethers: 5.7.2 lodash: ^4.17.21 - checksum: 432abd8391e05196b39fac712dbb55cd3a378db6e20990ee440271d1c99a18559f648dcbffb4d27ffbbb1c7364ba46f1e6f9aed19a439273c74bf4e931c933ba + checksum: 5653aff09d5abd1e32beee70009ac7cb0213b26cf9eb0cf6aea3592b3318126eb041cef2ae7859d284598284b887963716d3b9eb8d97cf6c6c46950346a3d3f3 languageName: node linkType: hard -"@shapeshiftoss/hdwallet-walletconnect@npm:1.62.28": - version: 1.62.28 - resolution: "@shapeshiftoss/hdwallet-walletconnect@npm:1.62.28" +"@shapeshiftoss/hdwallet-walletconnect@npm:1.62.29-bebop-solana-msg.5": + version: 1.62.29-bebop-solana-msg.5 + resolution: "@shapeshiftoss/hdwallet-walletconnect@npm:1.62.29-bebop-solana-msg.5" dependencies: - "@shapeshiftoss/hdwallet-core": 1.62.28 + "@shapeshiftoss/hdwallet-core": ^1.62.29-bebop-solana-msg.5 "@walletconnect/qrcode-modal": ^1.7.8 "@walletconnect/web3-provider": ^1.7.8 ethers: ^5.6.5 - checksum: 17887250cfa16adb96cf42740e8d954fc8ad8ebcae99f6231576f012bf81cc94d6f607bf0df879af3114cbf7d398219d48148d4f864825738784ae239b283590 + checksum: 4603dd348e468c393602eab95e7a9134a92c931df215dceb03144f54bbc5260a6a1ad55dc44e5adc8219b40233b6d6df4ee7f359254874416815a20d29fc7cf9 languageName: node linkType: hard -"@shapeshiftoss/hdwallet-walletconnectv2@npm:1.62.28": - version: 1.62.28 - resolution: "@shapeshiftoss/hdwallet-walletconnectv2@npm:1.62.28" +"@shapeshiftoss/hdwallet-walletconnectv2@npm:1.62.29-bebop-solana-msg.5": + version: 1.62.29-bebop-solana-msg.5 + resolution: "@shapeshiftoss/hdwallet-walletconnectv2@npm:1.62.29-bebop-solana-msg.5" dependencies: - "@shapeshiftoss/hdwallet-core": 1.62.28 + "@shapeshiftoss/hdwallet-core": ^1.62.29-bebop-solana-msg.5 "@walletconnect/ethereum-provider": ^2.20.2 "@walletconnect/modal": ^2.6.2 ethers: ^5.6.5 - checksum: 3186286ae05a340d224750b4409a6ac5fde9de5604c907659a1048704f2c89d7104da0a137cf421f2d700b516963097952d1cca591d72f05f1a1659ff2448a8d + checksum: 7efd280211a118efaf552b042f69dcebde97f8fdf968558de42f58aa36f9751fa4c23d28f06c292100c1d8190136964e84ada83aa17ff7175f33e5b6bf53a25b languageName: node linkType: hard @@ -11668,7 +11849,7 @@ __metadata: "@shapeshiftoss/caip": "workspace:^" "@shapeshiftoss/chain-adapters": "workspace:^" "@shapeshiftoss/contracts": "workspace:^" - "@shapeshiftoss/hdwallet-core": 1.62.28 + "@shapeshiftoss/hdwallet-core": 1.62.29-bebop-solana-msg.5 "@shapeshiftoss/types": "workspace:^" "@shapeshiftoss/unchained-client": "workspace:^" "@shapeshiftoss/utils": "workspace:^" @@ -11683,6 +11864,7 @@ __metadata: "@uniswap/v3-sdk": ^3.13.1 axios: ^1.13.0 axios-cache-interceptor: ^1.5.3 + base-x: ^5.0.0 bignumber.js: ^9.3.1 eip-712: ^1.0.0 ethers: 6.11.1 @@ -11793,6 +11975,7 @@ __metadata: "@metaplex-foundation/js": ^0.20.1 "@moralisweb3/common-evm-utils": 2.27.2 "@mysten/sui": 1.45.0 + "@noble/hashes": ^2.0.1 "@peculiar/webcrypto": ^1.3.3 "@react-spring/web": ^9.7.4 "@reduxjs/toolkit": ^2.6.1 @@ -11803,24 +11986,24 @@ __metadata: "@shapeshiftoss/chain-adapters": "workspace:^" "@shapeshiftoss/contracts": "workspace:^" "@shapeshiftoss/errors": "workspace:^" - "@shapeshiftoss/hdwallet-coinbase": 1.62.28 - "@shapeshiftoss/hdwallet-core": 1.62.28 - "@shapeshiftoss/hdwallet-gridplus": 1.62.28 - "@shapeshiftoss/hdwallet-keepkey": 1.62.28 - "@shapeshiftoss/hdwallet-keepkey-webusb": 1.62.28 - "@shapeshiftoss/hdwallet-keplr": 1.62.28 - "@shapeshiftoss/hdwallet-ledger": 1.62.28 - "@shapeshiftoss/hdwallet-ledger-webhid": 1.62.28 - "@shapeshiftoss/hdwallet-ledger-webusb": 1.62.28 - "@shapeshiftoss/hdwallet-metamask-multichain": 1.62.28 - "@shapeshiftoss/hdwallet-native": 1.62.28 - "@shapeshiftoss/hdwallet-native-vault": 1.62.28 - "@shapeshiftoss/hdwallet-phantom": 1.62.28 - "@shapeshiftoss/hdwallet-trezor": 1.62.28 - "@shapeshiftoss/hdwallet-trezor-connect": 1.62.28 - "@shapeshiftoss/hdwallet-vultisig": 1.62.28 - "@shapeshiftoss/hdwallet-walletconnect": 1.62.28 - "@shapeshiftoss/hdwallet-walletconnectv2": 1.62.28 + "@shapeshiftoss/hdwallet-coinbase": 1.62.29-bebop-solana-msg.5 + "@shapeshiftoss/hdwallet-core": 1.62.29-bebop-solana-msg.5 + "@shapeshiftoss/hdwallet-gridplus": 1.62.29-bebop-solana-msg.5 + "@shapeshiftoss/hdwallet-keepkey": 1.62.29-bebop-solana-msg.5 + "@shapeshiftoss/hdwallet-keepkey-webusb": 1.62.29-bebop-solana-msg.5 + "@shapeshiftoss/hdwallet-keplr": 1.62.29-bebop-solana-msg.5 + "@shapeshiftoss/hdwallet-ledger": 1.62.29-bebop-solana-msg.5 + "@shapeshiftoss/hdwallet-ledger-webhid": 1.62.29-bebop-solana-msg.5 + "@shapeshiftoss/hdwallet-ledger-webusb": 1.62.29-bebop-solana-msg.5 + "@shapeshiftoss/hdwallet-metamask-multichain": 1.62.29-bebop-solana-msg.5 + "@shapeshiftoss/hdwallet-native": 1.62.29-bebop-solana-msg.5 + "@shapeshiftoss/hdwallet-native-vault": 1.62.29-bebop-solana-msg.5 + "@shapeshiftoss/hdwallet-phantom": 1.62.29-bebop-solana-msg.5 + "@shapeshiftoss/hdwallet-trezor": 1.62.29-bebop-solana-msg.5 + "@shapeshiftoss/hdwallet-trezor-connect": 1.62.29-bebop-solana-msg.5 + "@shapeshiftoss/hdwallet-vultisig": 1.62.29-bebop-solana-msg.5 + "@shapeshiftoss/hdwallet-walletconnect": 1.62.29-bebop-solana-msg.5 + "@shapeshiftoss/hdwallet-walletconnectv2": 1.62.29-bebop-solana-msg.5 "@shapeshiftoss/swapper": "workspace:^" "@shapeshiftoss/types": "workspace:^" "@shapeshiftoss/unchained-client": "workspace:^" @@ -11834,6 +12017,7 @@ __metadata: "@tanstack/react-virtual": ^3.11.2 "@testing-library/react": ^13.3.0 "@types/bip21": ^2.0.0 + "@types/bs58": ^5.0.0 "@types/cli-progress": ^3.11.5 "@types/d3-array": ^3.0.1 "@types/dompurify": ^2.3.2 @@ -13757,6 +13941,15 @@ __metadata: languageName: node linkType: hard +"@types/bs58@npm:^5.0.0": + version: 5.0.0 + resolution: "@types/bs58@npm:5.0.0" + dependencies: + bs58: "*" + checksum: 5b33ed60d8fe5541cfe6263f3e15e1a705c299f6831712b063cb7f0ad5bf1bbbf6db13b88c16b470c84f20b631b8bdd939414101a6a903ad1231b88d62d987b4 + languageName: node + linkType: hard + "@types/bs58check@npm:^2.1.0": version: 2.1.0 resolution: "@types/bs58check@npm:2.1.0" @@ -18570,6 +18763,15 @@ __metadata: languageName: node linkType: hard +"bs58@npm:*, bs58@npm:6.0.0, bs58@npm:^6.0.0": + version: 6.0.0 + resolution: "bs58@npm:6.0.0" + dependencies: + base-x: ^5.0.0 + checksum: 820334f9513bba6195136dfc9dfbd1f5aded6c7864639f3ee7b63c2d9d6f9f2813b9949b1f6beb9c161237be2a461097444c2ff587c8c3b824fe18878fa22448 + languageName: node + linkType: hard + "bs58@npm:5.0.0, bs58@npm:^5.0.0": version: 5.0.0 resolution: "bs58@npm:5.0.0" @@ -18579,15 +18781,6 @@ __metadata: languageName: node linkType: hard -"bs58@npm:6.0.0, bs58@npm:^6.0.0": - version: 6.0.0 - resolution: "bs58@npm:6.0.0" - dependencies: - base-x: ^5.0.0 - checksum: 820334f9513bba6195136dfc9dfbd1f5aded6c7864639f3ee7b63c2d9d6f9f2813b9949b1f6beb9c161237be2a461097444c2ff587c8c3b824fe18878fa22448 - languageName: node - linkType: hard - "bs58@npm:^4.0.0, bs58@npm:^4.0.1": version: 4.0.1 resolution: "bs58@npm:4.0.1"