Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 53 additions & 17 deletions packages/common/src/api/tan-query/jupiter/useSwapTokens.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,8 +14,9 @@ import {
import { Feature } from '~/models'
import {
convertJupiterInstructions,
getJupiterQuoteByMint,
jupiterInstance
getJupiterQuoteByMintWithRetry,
jupiterInstance,
JupiterQuoteResult
} from '~/services/Jupiter'
import { useTokens } from '~/store/ui/buy-sell'
import { TokenInfo } from '~/store/ui/buy-sell/types'
Expand All@@ -28,7 +29,11 @@ import {
SwapTokensResult,
UserBankManagedTokenInfo
} from './types'
import { addUserBankToAtaInstructions, getSwapErrorResponse } from './utils'
import {
addUserBankToAtaInstructions,
addJupiterOutputAtaInstruction,
getSwapErrorResponse
} from './utils'

const SWAP_LOOKUP_TABLE_ADDRESS = new PublicKey(
'2WB87JxGZieRd7hi3y87wq6HAsPLyb9zrSx8B5z1QEzM'
Expand DownExpand Up@@ -74,7 +79,7 @@ export const useSwapTokens = () => {
const slippageBps = params.slippageBps ?? 50
const wrapUnwrapSol = params.wrapUnwrapSol ?? true

let quoteResult
let quoteResult: JupiterQuoteResult | undefined
let signature: string | undefined
let errorStage = 'UNKNOWN'
const instructions: TransactionInstruction[] = []
Expand DownExpand Up@@ -117,7 +122,7 @@ export const useSwapTokens = () => {

// ---------- 3. Get Quote from Jupiter ----------
errorStage = 'QUOTE_RETRIEVAL'
quoteResult= await getJupiterQuoteByMint({
const { quoteResult: quote } = await getJupiterQuoteByMintWithRetry({
inputMint: inputMintUiAddress,
outputMint: outputMintUiAddress,
inputDecimals: inputToken.decimals,
Expand All@@ -127,6 +132,7 @@ export const useSwapTokens = () => {
swapMode: 'ExactIn',
onlyDirectRoutes: false
})
quoteResult = quote

// ---------- 4. Prepare Transaction Instructions ----------

Expand All@@ -146,6 +152,7 @@ export const useSwapTokens = () => {
feePayer,
instructions
})
let outputAtaForJupiter: PublicKey | undefined

// --- 3b. Determine Jupiter's Destination Token Account ---
errorStage = 'OUTPUT_TOKEN_PREPARATION'
Expand All@@ -160,34 +167,63 @@ export const useSwapTokens = () => {

// --- 3c. Get Jupiter Swap Instructions ---
errorStage = 'SWAP_INSTRUCTION_RETRIEVAL'
let swapInstructionsResult
const swapRequestParams = {
quoteResponse: quoteResult.quote,
userPublicKey: userPublicKey.toBase58(),
destinationTokenAccount: preferredJupiterDestination,
wrapAndUnwrapSol: wrapUnwrapSol
}
try {
swapInstructionsResult = await jupiterInstance.swapInstructionsPost({
swapRequest: {
...swapRequestParams,
useSharedAccounts: true
}
})
} catch (e) {
// In the case of simple AMMs, we will see an error that looks like this:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should probably check for that error case in particular?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought about this, but I'm worried about casing it on the API, which could change. There's no harm in retrying once here, I think

// {
// error: "Simple AMMs are not supported with shared accounts"
// errorCode: "NOT_SUPPORTED"
// }
// We can retry with useSharedAccounts: false and bail out if that still errors
swapInstructionsResult = await jupiterInstance.swapInstructionsPost({
swapRequest: {
...swapRequestParams,
useSharedAccounts: false
}
})
// If getting the instruction succeeded, we need to manually handle
// the intermediary account for the output mint
outputAtaForJupiter = addJupiterOutputAtaInstruction({
tokenConfig: outputTokenConfig,
userPublicKey,
feePayer,
instructions
})
}
const {
tokenLedgerInstruction,
swapInstruction,
addressLookupTableAddresses
} = await jupiterInstance.swapInstructionsPost({
swapRequest: {
quoteResponse: quoteResult.quote,
userPublicKey: userPublicKey.toBase58(),
destinationTokenAccount: preferredJupiterDestination,
wrapAndUnwrapSol: wrapUnwrapSol,
useSharedAccounts: true
}
})

} = swapInstructionsResult
const jupiterInstructions = convertJupiterInstructions([
tokenLedgerInstruction,
swapInstruction
])

instructions.push(...jupiterInstructions)

// --- 3f. Add Cleanup Instructions for Temporary ATAs ---
const atasToClose: PublicKey[] = []

// Add source ATA if it's temporary
if (sourceAtaForJupiter) {
atasToClose.push(sourceAtaForJupiter)
}
// Add output ATA if it's temporary
if (outputAtaForJupiter) {
atasToClose.push(outputAtaForJupiter)
}

// Add close account instructions for all ATAs that need to be closed
for (const ataToClose of atasToClose) {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,7 @@ import { useMemo } from 'react'
import { QuoteResponse, SwapMode } from '@jup-ag/api'
import { useQuery } from '@tanstack/react-query'

import { getJupiterQuoteByMint } from '~/services/Jupiter'
import { getJupiterQuoteByMint, MAX_ALLOWED_ACCOUNTS } from '~/services/Jupiter'

import { QUERY_KEYS } from '../queryKeys'
import { QueryOptions, type QueryKey } from '../types'
Expand DownExpand Up@@ -103,7 +103,8 @@ export const useTokenExchangeRate = (
amountUi: safeInputAmount,
slippageBps: SLIPPAGE_BPS,
swapMode: params.swapMode ?? 'ExactIn',
onlyDirectRoutes: false
onlyDirectRoutes: false,
maxAccounts: MAX_ALLOWED_ACCOUNTS
})

// Calculate exchange rate (how many output tokens per 1 input token)
Expand Down
39 changes: 39 additions & 0 deletions packages/common/src/api/tan-query/jupiter/utils.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,6 +103,45 @@ export async function addAtaToUserBankInstructions({
return userBankAddress
}

/**
* Creates an Associated Token Account (ATA) for Jupiter when shared accounts are not supported.
* This is used as a fallback when Jupiter's shared account system fails for simple AMMs.
*
* @param tokenConfig - The token configuration containing mint address
* @param userPublicKey - The user's public key
* @param feePayer - The fee payer's public key
* @param instructions - Array to push the ATA creation instruction to
* @returns The created ATA public key
*/
export function addJupiterOutputAtaInstruction({
tokenConfig,
userPublicKey,
feePayer,
instructions
}: {
tokenConfig: UserBankManagedTokenInfo
userPublicKey: PublicKey
feePayer: PublicKey
instructions: TransactionInstruction[]
}): PublicKey {
const outputAtaForJupiter = getAssociatedTokenAddressSync(
new PublicKey(tokenConfig.mintAddress),
userPublicKey,
true
)

instructions.push(
createAssociatedTokenAccountIdempotentInstruction(
feePayer,
outputAtaForJupiter,
userPublicKey,
new PublicKey(tokenConfig.mintAddress)
)
)

return outputAtaForJupiter
}

/**
* Get the appropriate error response for a swap error based on the error stage.
*/
Expand Down
65 changes: 63 additions & 2 deletions packages/common/src/services/Jupiter.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,7 +20,8 @@ export const SLIPPAGE_TOLERANCE_EXCEEDED_ERROR = 6001
// Define JupiterTokenSymbol type here since we can't import it directly
export type JupiterTokenSymbol = keyof typeof TOKEN_LISTING_MAP

const MAX_ACCOUNTS = 20
export const DEFAULT_MAX_ACCOUNTS = 20
export const MAX_ALLOWED_ACCOUNTS = 64

let _jup: ReturnType<typeof createJupiterApiClient>

Expand DownExpand Up@@ -99,7 +100,7 @@ export const getJupiterQuoteByMint = async ({
slippageBps,
swapMode = 'ExactIn',
onlyDirectRoutes = false,
maxAccounts = MAX_ACCOUNTS
maxAccounts = DEFAULT_MAX_ACCOUNTS
}: JupiterMintQuoteParams): Promise<JupiterQuoteResult> => {
const amount =
swapMode === 'ExactIn'
Expand DownExpand Up@@ -137,6 +138,66 @@ export const getJupiterQuoteByMint = async ({
}
}

export type JupiterQuoteWithRetryResult = {
maxAccountsValue: number
quoteResult: JupiterQuoteResult
}

/**
* Gets a Jupiter quote with automatic retry logic for maxAccounts
* Starts with DEFAULT_MAX_ACCOUNTS and increments by 10 until MAX_ALLOWED_ACCOUNTS
* Returns the successful quote along with the maxAccounts value that worked
*/
export const getJupiterQuoteByMintWithRetry = async ({
inputMint,
outputMint,
inputDecimals,
outputDecimals,
amountUi,
slippageBps,
swapMode = 'ExactIn',
onlyDirectRoutes = false
}: Omit<
JupiterMintQuoteParams,
'maxAccounts'
>): Promise<JupiterQuoteWithRetryResult> => {
let maxAccounts = DEFAULT_MAX_ACCOUNTS
let lastError
let quoteResult: JupiterQuoteResult | null = null

while (maxAccounts <= MAX_ALLOWED_ACCOUNTS) {
try {
quoteResult = await getJupiterQuoteByMint({
inputMint,
outputMint,
inputDecimals,
outputDecimals,
amountUi,
slippageBps,
swapMode,
onlyDirectRoutes,
maxAccounts
})
break
} catch (err) {
lastError = err
maxAccounts += 10
if (maxAccounts > MAX_ALLOWED_ACCOUNTS) {
throw lastError
}
}
}

if (quoteResult === null) {
throw lastError
}

return {
maxAccountsValue: maxAccounts,
quoteResult
}
}

/**
* Converts an array of Jupiter instructions to Solana TransactionInstructions
* Filters out undefined instructions and handles the conversion
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
# Solana Relay Plugin

Plugin to facilitate sending Audius-related Solana transactions

## Dev

Create a file `dev.env` and add the following

```
audius_discprov_env
audius_discprov_url
audius_db_url
audius_redis_url
audius_solana_endpoint
audius_solana_listen_rpc_url
audius_solana_track_listen_count_address
audius_solana_waudio_mint
audius_solana_usdc_mint
audius_solana_bonk_mint
audius_solana_user_bank_program_address
audius_solana_payment_router_program_address
audius_solana_rewards_manager_program_address
audius_solana_rewards_manager_account
audius_solana_fee_payer_wallets
audius_delegate_private_key
audius_ipdata_api_key
audius_solana_eth_registry_program
audius_solana_listens_valid_signer
audius_solana_signer_private_key
audius_identity_relayer_public_key
audius_solana_listens_ip_hourly_rate_limit
audius_solana_listens_ip_daily_rate_limit
audius_solana_listens_ip_weekly_rate_limit
audius_solana_listens_track_hourly_rate_limit
audius_solana_listens_track_daily_rate_limit
audius_solana_listens_track_weekly_rate_limit
audius_anti_abuse_oracle
```

Then

```
npm run dev
```

If using with the web app, updat the `SOLANA_RELAY_ENDPOINT` value in `packages/web/src/services/env/env.<env>.ts` to point to the localhost port that the relay server is running on (i.e. localhost:6002)
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 53 additions & 17 deletions packages/common/src/api/tan-query/jupiter/useSwapTokens.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,8 +14,9 @@ import {
import { Feature } from '~/models'
import {
convertJupiterInstructions,
getJupiterQuoteByMint,
jupiterInstance
getJupiterQuoteByMintWithRetry,
jupiterInstance,
JupiterQuoteResult
} from '~/services/Jupiter'
import { useTokens } from '~/store/ui/buy-sell'
import { TokenInfo } from '~/store/ui/buy-sell/types'
Expand All@@ -28,7 +29,11 @@ import {
SwapTokensResult,
UserBankManagedTokenInfo
} from './types'
import { addUserBankToAtaInstructions, getSwapErrorResponse } from './utils'
import {
addUserBankToAtaInstructions,
addJupiterOutputAtaInstruction,
getSwapErrorResponse
} from './utils'

const SWAP_LOOKUP_TABLE_ADDRESS = new PublicKey(
'2WB87JxGZieRd7hi3y87wq6HAsPLyb9zrSx8B5z1QEzM'
Expand DownExpand Up@@ -74,7 +79,7 @@ export const useSwapTokens = () => {
const slippageBps = params.slippageBps ?? 50
const wrapUnwrapSol = params.wrapUnwrapSol ?? true

let quoteResult
let quoteResult: JupiterQuoteResult | undefined
let signature: string | undefined
let errorStage = 'UNKNOWN'
const instructions: TransactionInstruction[] = []
Expand DownExpand Up@@ -117,7 +122,7 @@ export const useSwapTokens = () => {

// ---------- 3. Get Quote from Jupiter ----------
errorStage = 'QUOTE_RETRIEVAL'
quoteResult= await getJupiterQuoteByMint({
const { quoteResult: quote } = await getJupiterQuoteByMintWithRetry({
inputMint: inputMintUiAddress,
outputMint: outputMintUiAddress,
inputDecimals: inputToken.decimals,
Expand All@@ -127,6 +132,7 @@ export const useSwapTokens = () => {
swapMode: 'ExactIn',
onlyDirectRoutes: false
})
quoteResult = quote

// ---------- 4. Prepare Transaction Instructions ----------

Expand All@@ -146,6 +152,7 @@ export const useSwapTokens = () => {
feePayer,
instructions
})
let outputAtaForJupiter: PublicKey | undefined

// --- 3b. Determine Jupiter's Destination Token Account ---
errorStage = 'OUTPUT_TOKEN_PREPARATION'
Expand All@@ -160,34 +167,63 @@ export const useSwapTokens = () => {

// --- 3c. Get Jupiter Swap Instructions ---
errorStage = 'SWAP_INSTRUCTION_RETRIEVAL'
let swapInstructionsResult
const swapRequestParams = {
quoteResponse: quoteResult.quote,
userPublicKey: userPublicKey.toBase58(),
destinationTokenAccount: preferredJupiterDestination,
wrapAndUnwrapSol: wrapUnwrapSol
}
try {
swapInstructionsResult = await jupiterInstance.swapInstructionsPost({
swapRequest: {
...swapRequestParams,
useSharedAccounts: true
}
})
} catch (e) {
// In the case of simple AMMs, we will see an error that looks like this:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should probably check for that error case in particular?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought about this, but I'm worried about casing it on the API, which could change. There's no harm in retrying once here, I think

// {
// error: "Simple AMMs are not supported with shared accounts"
// errorCode: "NOT_SUPPORTED"
// }
// We can retry with useSharedAccounts: false and bail out if that still errors
swapInstructionsResult = await jupiterInstance.swapInstructionsPost({
swapRequest: {
...swapRequestParams,
useSharedAccounts: false
}
})
// If getting the instruction succeeded, we need to manually handle
// the intermediary account for the output mint
outputAtaForJupiter = addJupiterOutputAtaInstruction({
tokenConfig: outputTokenConfig,
userPublicKey,
feePayer,
instructions
})
}
const {
tokenLedgerInstruction,
swapInstruction,
addressLookupTableAddresses
} = await jupiterInstance.swapInstructionsPost({
swapRequest: {
quoteResponse: quoteResult.quote,
userPublicKey: userPublicKey.toBase58(),
destinationTokenAccount: preferredJupiterDestination,
wrapAndUnwrapSol: wrapUnwrapSol,
useSharedAccounts: true
}
})

} = swapInstructionsResult
const jupiterInstructions = convertJupiterInstructions([
tokenLedgerInstruction,
swapInstruction
])

instructions.push(...jupiterInstructions)

// --- 3f. Add Cleanup Instructions for Temporary ATAs ---
const atasToClose: PublicKey[] = []

// Add source ATA if it's temporary
if (sourceAtaForJupiter) {
atasToClose.push(sourceAtaForJupiter)
}
// Add output ATA if it's temporary
if (outputAtaForJupiter) {
atasToClose.push(outputAtaForJupiter)
}

// Add close account instructions for all ATAs that need to be closed
for (const ataToClose of atasToClose) {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,7 @@ import { useMemo } from 'react'
import { QuoteResponse, SwapMode } from '@jup-ag/api'
import { useQuery } from '@tanstack/react-query'

import { getJupiterQuoteByMint } from '~/services/Jupiter'
import { getJupiterQuoteByMint, MAX_ALLOWED_ACCOUNTS } from '~/services/Jupiter'

import { QUERY_KEYS } from '../queryKeys'
import { QueryOptions, type QueryKey } from '../types'
Expand DownExpand Up@@ -103,7 +103,8 @@ export const useTokenExchangeRate = (
amountUi: safeInputAmount,
slippageBps: SLIPPAGE_BPS,
swapMode: params.swapMode ?? 'ExactIn',
onlyDirectRoutes: false
onlyDirectRoutes: false,
maxAccounts: MAX_ALLOWED_ACCOUNTS
})

// Calculate exchange rate (how many output tokens per 1 input token)
Expand Down
39 changes: 39 additions & 0 deletions packages/common/src/api/tan-query/jupiter/utils.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,6 +103,45 @@ export async function addAtaToUserBankInstructions({
return userBankAddress
}

/**
* Creates an Associated Token Account (ATA) for Jupiter when shared accounts are not supported.
* This is used as a fallback when Jupiter's shared account system fails for simple AMMs.
*
* @param tokenConfig - The token configuration containing mint address
* @param userPublicKey - The user's public key
* @param feePayer - The fee payer's public key
* @param instructions - Array to push the ATA creation instruction to
* @returns The created ATA public key
*/
export function addJupiterOutputAtaInstruction({
tokenConfig,
userPublicKey,
feePayer,
instructions
}: {
tokenConfig: UserBankManagedTokenInfo
userPublicKey: PublicKey
feePayer: PublicKey
instructions: TransactionInstruction[]
}): PublicKey {
const outputAtaForJupiter = getAssociatedTokenAddressSync(
new PublicKey(tokenConfig.mintAddress),
userPublicKey,
true
)

instructions.push(
createAssociatedTokenAccountIdempotentInstruction(
feePayer,
outputAtaForJupiter,
userPublicKey,
new PublicKey(tokenConfig.mintAddress)
)
)

return outputAtaForJupiter
}

/**
* Get the appropriate error response for a swap error based on the error stage.
*/
Expand Down
65 changes: 63 additions & 2 deletions packages/common/src/services/Jupiter.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,7 +20,8 @@ export const SLIPPAGE_TOLERANCE_EXCEEDED_ERROR = 6001
// Define JupiterTokenSymbol type here since we can't import it directly
export type JupiterTokenSymbol = keyof typeof TOKEN_LISTING_MAP

const MAX_ACCOUNTS = 20
export const DEFAULT_MAX_ACCOUNTS = 20
export const MAX_ALLOWED_ACCOUNTS = 64

let _jup: ReturnType<typeof createJupiterApiClient>

Expand DownExpand Up@@ -99,7 +100,7 @@ export const getJupiterQuoteByMint = async ({
slippageBps,
swapMode = 'ExactIn',
onlyDirectRoutes = false,
maxAccounts = MAX_ACCOUNTS
maxAccounts = DEFAULT_MAX_ACCOUNTS
}: JupiterMintQuoteParams): Promise<JupiterQuoteResult> => {
const amount =
swapMode === 'ExactIn'
Expand DownExpand Up@@ -137,6 +138,66 @@ export const getJupiterQuoteByMint = async ({
}
}

export type JupiterQuoteWithRetryResult = {
maxAccountsValue: number
quoteResult: JupiterQuoteResult
}

/**
* Gets a Jupiter quote with automatic retry logic for maxAccounts
* Starts with DEFAULT_MAX_ACCOUNTS and increments by 10 until MAX_ALLOWED_ACCOUNTS
* Returns the successful quote along with the maxAccounts value that worked
*/
export const getJupiterQuoteByMintWithRetry = async ({
inputMint,
outputMint,
inputDecimals,
outputDecimals,
amountUi,
slippageBps,
swapMode = 'ExactIn',
onlyDirectRoutes = false
}: Omit<
JupiterMintQuoteParams,
'maxAccounts'
>): Promise<JupiterQuoteWithRetryResult> => {
let maxAccounts = DEFAULT_MAX_ACCOUNTS
let lastError
let quoteResult: JupiterQuoteResult | null = null

while (maxAccounts <= MAX_ALLOWED_ACCOUNTS) {
try {
quoteResult = await getJupiterQuoteByMint({
inputMint,
outputMint,
inputDecimals,
outputDecimals,
amountUi,
slippageBps,
swapMode,
onlyDirectRoutes,
maxAccounts
})
break
} catch (err) {
lastError = err
maxAccounts += 10
if (maxAccounts > MAX_ALLOWED_ACCOUNTS) {
throw lastError
}
}
}

if (quoteResult === null) {
throw lastError
}

return {
maxAccountsValue: maxAccounts,
quoteResult
}
}

/**
* Converts an array of Jupiter instructions to Solana TransactionInstructions
* Filters out undefined instructions and handles the conversion
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
# Solana Relay Plugin

Plugin to facilitate sending Audius-related Solana transactions

## Dev

Create a file `dev.env` and add the following

```
audius_discprov_env
audius_discprov_url
audius_db_url
audius_redis_url
audius_solana_endpoint
audius_solana_listen_rpc_url
audius_solana_track_listen_count_address
audius_solana_waudio_mint
audius_solana_usdc_mint
audius_solana_bonk_mint
audius_solana_user_bank_program_address
audius_solana_payment_router_program_address
audius_solana_rewards_manager_program_address
audius_solana_rewards_manager_account
audius_solana_fee_payer_wallets
audius_delegate_private_key
audius_ipdata_api_key
audius_solana_eth_registry_program
audius_solana_listens_valid_signer
audius_solana_signer_private_key
audius_identity_relayer_public_key
audius_solana_listens_ip_hourly_rate_limit
audius_solana_listens_ip_daily_rate_limit
audius_solana_listens_ip_weekly_rate_limit
audius_solana_listens_track_hourly_rate_limit
audius_solana_listens_track_daily_rate_limit
audius_solana_listens_track_weekly_rate_limit
audius_anti_abuse_oracle
```

Then

```
npm run dev
```

If using with the web app, updat the `SOLANA_RELAY_ENDPOINT` value in `packages/web/src/services/env/env.<env>.ts` to point to the localhost port that the relay server is running on (i.e. localhost:6002)
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 53 additions & 17 deletions packages/common/src/api/tan-query/jupiter/useSwapTokens.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,8 +14,9 @@ import {
import { Feature } from '~/models'
import {
convertJupiterInstructions,
getJupiterQuoteByMint,
jupiterInstance
getJupiterQuoteByMintWithRetry,
jupiterInstance,
JupiterQuoteResult
} from '~/services/Jupiter'
import { useTokens } from '~/store/ui/buy-sell'
import { TokenInfo } from '~/store/ui/buy-sell/types'
Expand All@@ -28,7 +29,11 @@ import {
SwapTokensResult,
UserBankManagedTokenInfo
} from './types'
import { addUserBankToAtaInstructions, getSwapErrorResponse } from './utils'
import {
addUserBankToAtaInstructions,
addJupiterOutputAtaInstruction,
getSwapErrorResponse
} from './utils'

const SWAP_LOOKUP_TABLE_ADDRESS = new PublicKey(
'2WB87JxGZieRd7hi3y87wq6HAsPLyb9zrSx8B5z1QEzM'
Expand DownExpand Up@@ -74,7 +79,7 @@ export const useSwapTokens = () => {
const slippageBps = params.slippageBps ?? 50
const wrapUnwrapSol = params.wrapUnwrapSol ?? true

let quoteResult
let quoteResult: JupiterQuoteResult | undefined
let signature: string | undefined
let errorStage = 'UNKNOWN'
const instructions: TransactionInstruction[] = []
Expand DownExpand Up@@ -117,7 +122,7 @@ export const useSwapTokens = () => {

// ---------- 3. Get Quote from Jupiter ----------
errorStage = 'QUOTE_RETRIEVAL'
quoteResult= await getJupiterQuoteByMint({
const { quoteResult: quote } = await getJupiterQuoteByMintWithRetry({
inputMint: inputMintUiAddress,
outputMint: outputMintUiAddress,
inputDecimals: inputToken.decimals,
Expand All@@ -127,6 +132,7 @@ export const useSwapTokens = () => {
swapMode: 'ExactIn',
onlyDirectRoutes: false
})
quoteResult = quote

// ---------- 4. Prepare Transaction Instructions ----------

Expand All@@ -146,6 +152,7 @@ export const useSwapTokens = () => {
feePayer,
instructions
})
let outputAtaForJupiter: PublicKey | undefined

// --- 3b. Determine Jupiter's Destination Token Account ---
errorStage = 'OUTPUT_TOKEN_PREPARATION'
Expand All@@ -160,34 +167,63 @@ export const useSwapTokens = () => {

// --- 3c. Get Jupiter Swap Instructions ---
errorStage = 'SWAP_INSTRUCTION_RETRIEVAL'
let swapInstructionsResult
const swapRequestParams = {
quoteResponse: quoteResult.quote,
userPublicKey: userPublicKey.toBase58(),
destinationTokenAccount: preferredJupiterDestination,
wrapAndUnwrapSol: wrapUnwrapSol
}
try {
swapInstructionsResult = await jupiterInstance.swapInstructionsPost({
swapRequest: {
...swapRequestParams,
useSharedAccounts: true
}
})
} catch (e) {
// In the case of simple AMMs, we will see an error that looks like this:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should probably check for that error case in particular?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought about this, but I'm worried about casing it on the API, which could change. There's no harm in retrying once here, I think

// {
// error: "Simple AMMs are not supported with shared accounts"
// errorCode: "NOT_SUPPORTED"
// }
// We can retry with useSharedAccounts: false and bail out if that still errors
swapInstructionsResult = await jupiterInstance.swapInstructionsPost({
swapRequest: {
...swapRequestParams,
useSharedAccounts: false
}
})
// If getting the instruction succeeded, we need to manually handle
// the intermediary account for the output mint
outputAtaForJupiter = addJupiterOutputAtaInstruction({
tokenConfig: outputTokenConfig,
userPublicKey,
feePayer,
instructions
})
}
const {
tokenLedgerInstruction,
swapInstruction,
addressLookupTableAddresses
} = await jupiterInstance.swapInstructionsPost({
swapRequest: {
quoteResponse: quoteResult.quote,
userPublicKey: userPublicKey.toBase58(),
destinationTokenAccount: preferredJupiterDestination,
wrapAndUnwrapSol: wrapUnwrapSol,
useSharedAccounts: true
}
})

} = swapInstructionsResult
const jupiterInstructions = convertJupiterInstructions([
tokenLedgerInstruction,
swapInstruction
])

instructions.push(...jupiterInstructions)

// --- 3f. Add Cleanup Instructions for Temporary ATAs ---
const atasToClose: PublicKey[] = []

// Add source ATA if it's temporary
if (sourceAtaForJupiter) {
atasToClose.push(sourceAtaForJupiter)
}
// Add output ATA if it's temporary
if (outputAtaForJupiter) {
atasToClose.push(outputAtaForJupiter)
}

// Add close account instructions for all ATAs that need to be closed
for (const ataToClose of atasToClose) {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,7 @@ import { useMemo } from 'react'
import { QuoteResponse, SwapMode } from '@jup-ag/api'
import { useQuery } from '@tanstack/react-query'

import { getJupiterQuoteByMint } from '~/services/Jupiter'
import { getJupiterQuoteByMint, MAX_ALLOWED_ACCOUNTS } from '~/services/Jupiter'

import { QUERY_KEYS } from '../queryKeys'
import { QueryOptions, type QueryKey } from '../types'
Expand DownExpand Up@@ -103,7 +103,8 @@ export const useTokenExchangeRate = (
amountUi: safeInputAmount,
slippageBps: SLIPPAGE_BPS,
swapMode: params.swapMode ?? 'ExactIn',
onlyDirectRoutes: false
onlyDirectRoutes: false,
maxAccounts: MAX_ALLOWED_ACCOUNTS
})

// Calculate exchange rate (how many output tokens per 1 input token)
Expand Down
39 changes: 39 additions & 0 deletions packages/common/src/api/tan-query/jupiter/utils.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,6 +103,45 @@ export async function addAtaToUserBankInstructions({
return userBankAddress
}

/**
* Creates an Associated Token Account (ATA) for Jupiter when shared accounts are not supported.
* This is used as a fallback when Jupiter's shared account system fails for simple AMMs.
*
* @param tokenConfig - The token configuration containing mint address
* @param userPublicKey - The user's public key
* @param feePayer - The fee payer's public key
* @param instructions - Array to push the ATA creation instruction to
* @returns The created ATA public key
*/
export function addJupiterOutputAtaInstruction({
tokenConfig,
userPublicKey,
feePayer,
instructions
}: {
tokenConfig: UserBankManagedTokenInfo
userPublicKey: PublicKey
feePayer: PublicKey
instructions: TransactionInstruction[]
}): PublicKey {
const outputAtaForJupiter = getAssociatedTokenAddressSync(
new PublicKey(tokenConfig.mintAddress),
userPublicKey,
true
)

instructions.push(
createAssociatedTokenAccountIdempotentInstruction(
feePayer,
outputAtaForJupiter,
userPublicKey,
new PublicKey(tokenConfig.mintAddress)
)
)

return outputAtaForJupiter
}

/**
* Get the appropriate error response for a swap error based on the error stage.
*/
Expand Down
65 changes: 63 additions & 2 deletions packages/common/src/services/Jupiter.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,7 +20,8 @@ export const SLIPPAGE_TOLERANCE_EXCEEDED_ERROR = 6001
// Define JupiterTokenSymbol type here since we can't import it directly
export type JupiterTokenSymbol = keyof typeof TOKEN_LISTING_MAP

const MAX_ACCOUNTS = 20
export const DEFAULT_MAX_ACCOUNTS = 20
export const MAX_ALLOWED_ACCOUNTS = 64

let _jup: ReturnType<typeof createJupiterApiClient>

Expand DownExpand Up@@ -99,7 +100,7 @@ export const getJupiterQuoteByMint = async ({
slippageBps,
swapMode = 'ExactIn',
onlyDirectRoutes = false,
maxAccounts = MAX_ACCOUNTS
maxAccounts = DEFAULT_MAX_ACCOUNTS
}: JupiterMintQuoteParams): Promise<JupiterQuoteResult> => {
const amount =
swapMode === 'ExactIn'
Expand DownExpand Up@@ -137,6 +138,66 @@ export const getJupiterQuoteByMint = async ({
}
}

export type JupiterQuoteWithRetryResult = {
maxAccountsValue: number
quoteResult: JupiterQuoteResult
}

/**
* Gets a Jupiter quote with automatic retry logic for maxAccounts
* Starts with DEFAULT_MAX_ACCOUNTS and increments by 10 until MAX_ALLOWED_ACCOUNTS
* Returns the successful quote along with the maxAccounts value that worked
*/
export const getJupiterQuoteByMintWithRetry = async ({
inputMint,
outputMint,
inputDecimals,
outputDecimals,
amountUi,
slippageBps,
swapMode = 'ExactIn',
onlyDirectRoutes = false
}: Omit<
JupiterMintQuoteParams,
'maxAccounts'
>): Promise<JupiterQuoteWithRetryResult> => {
let maxAccounts = DEFAULT_MAX_ACCOUNTS
let lastError
let quoteResult: JupiterQuoteResult | null = null

while (maxAccounts <= MAX_ALLOWED_ACCOUNTS) {
try {
quoteResult = await getJupiterQuoteByMint({
inputMint,
outputMint,
inputDecimals,
outputDecimals,
amountUi,
slippageBps,
swapMode,
onlyDirectRoutes,
maxAccounts
})
break
} catch (err) {
lastError = err
maxAccounts += 10
if (maxAccounts > MAX_ALLOWED_ACCOUNTS) {
throw lastError
}
}
}

if (quoteResult === null) {
throw lastError
}

return {
maxAccountsValue: maxAccounts,
quoteResult
}
}

/**
* Converts an array of Jupiter instructions to Solana TransactionInstructions
* Filters out undefined instructions and handles the conversion
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
# Solana Relay Plugin

Plugin to facilitate sending Audius-related Solana transactions

## Dev

Create a file `dev.env` and add the following

```
audius_discprov_env
audius_discprov_url
audius_db_url
audius_redis_url
audius_solana_endpoint
audius_solana_listen_rpc_url
audius_solana_track_listen_count_address
audius_solana_waudio_mint
audius_solana_usdc_mint
audius_solana_bonk_mint
audius_solana_user_bank_program_address
audius_solana_payment_router_program_address
audius_solana_rewards_manager_program_address
audius_solana_rewards_manager_account
audius_solana_fee_payer_wallets
audius_delegate_private_key
audius_ipdata_api_key
audius_solana_eth_registry_program
audius_solana_listens_valid_signer
audius_solana_signer_private_key
audius_identity_relayer_public_key
audius_solana_listens_ip_hourly_rate_limit
audius_solana_listens_ip_daily_rate_limit
audius_solana_listens_ip_weekly_rate_limit
audius_solana_listens_track_hourly_rate_limit
audius_solana_listens_track_daily_rate_limit
audius_solana_listens_track_weekly_rate_limit
audius_anti_abuse_oracle
```

Then

```
npm run dev
```

If using with the web app, updat the `SOLANA_RELAY_ENDPOINT` value in `packages/web/src/services/env/env.<env>.ts` to point to the localhost port that the relay server is running on (i.e. localhost:6002)
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 53 additions & 17 deletions packages/common/src/api/tan-query/jupiter/useSwapTokens.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,8 +14,9 @@ import {
import { Feature } from '~/models'
import {
convertJupiterInstructions,
getJupiterQuoteByMint,
jupiterInstance
getJupiterQuoteByMintWithRetry,
jupiterInstance,
JupiterQuoteResult
} from '~/services/Jupiter'
import { useTokens } from '~/store/ui/buy-sell'
import { TokenInfo } from '~/store/ui/buy-sell/types'
Expand All@@ -28,7 +29,11 @@ import {
SwapTokensResult,
UserBankManagedTokenInfo
} from './types'
import { addUserBankToAtaInstructions, getSwapErrorResponse } from './utils'
import {
addUserBankToAtaInstructions,
addJupiterOutputAtaInstruction,
getSwapErrorResponse
} from './utils'

const SWAP_LOOKUP_TABLE_ADDRESS = new PublicKey(
'2WB87JxGZieRd7hi3y87wq6HAsPLyb9zrSx8B5z1QEzM'
Expand DownExpand Up@@ -74,7 +79,7 @@ export const useSwapTokens = () => {
const slippageBps = params.slippageBps ?? 50
const wrapUnwrapSol = params.wrapUnwrapSol ?? true

let quoteResult
let quoteResult: JupiterQuoteResult | undefined
let signature: string | undefined
let errorStage = 'UNKNOWN'
const instructions: TransactionInstruction[] = []
Expand DownExpand Up@@ -117,7 +122,7 @@ export const useSwapTokens = () => {

// ---------- 3. Get Quote from Jupiter ----------
errorStage = 'QUOTE_RETRIEVAL'
quoteResult= await getJupiterQuoteByMint({
const { quoteResult: quote } = await getJupiterQuoteByMintWithRetry({
inputMint: inputMintUiAddress,
outputMint: outputMintUiAddress,
inputDecimals: inputToken.decimals,
Expand All@@ -127,6 +132,7 @@ export const useSwapTokens = () => {
swapMode: 'ExactIn',
onlyDirectRoutes: false
})
quoteResult = quote

// ---------- 4. Prepare Transaction Instructions ----------

Expand All@@ -146,6 +152,7 @@ export const useSwapTokens = () => {
feePayer,
instructions
})
let outputAtaForJupiter: PublicKey | undefined

// --- 3b. Determine Jupiter's Destination Token Account ---
errorStage = 'OUTPUT_TOKEN_PREPARATION'
Expand All@@ -160,34 +167,63 @@ export const useSwapTokens = () => {

// --- 3c. Get Jupiter Swap Instructions ---
errorStage = 'SWAP_INSTRUCTION_RETRIEVAL'
let swapInstructionsResult
const swapRequestParams = {
quoteResponse: quoteResult.quote,
userPublicKey: userPublicKey.toBase58(),
destinationTokenAccount: preferredJupiterDestination,
wrapAndUnwrapSol: wrapUnwrapSol
}
try {
swapInstructionsResult = await jupiterInstance.swapInstructionsPost({
swapRequest: {
...swapRequestParams,
useSharedAccounts: true
}
})
} catch (e) {
// In the case of simple AMMs, we will see an error that looks like this:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should probably check for that error case in particular?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought about this, but I'm worried about casing it on the API, which could change. There's no harm in retrying once here, I think

// {
// error: "Simple AMMs are not supported with shared accounts"
// errorCode: "NOT_SUPPORTED"
// }
// We can retry with useSharedAccounts: false and bail out if that still errors
swapInstructionsResult = await jupiterInstance.swapInstructionsPost({
swapRequest: {
...swapRequestParams,
useSharedAccounts: false
}
})
// If getting the instruction succeeded, we need to manually handle
// the intermediary account for the output mint
outputAtaForJupiter = addJupiterOutputAtaInstruction({
tokenConfig: outputTokenConfig,
userPublicKey,
feePayer,
instructions
})
}
const {
tokenLedgerInstruction,
swapInstruction,
addressLookupTableAddresses
} = await jupiterInstance.swapInstructionsPost({
swapRequest: {
quoteResponse: quoteResult.quote,
userPublicKey: userPublicKey.toBase58(),
destinationTokenAccount: preferredJupiterDestination,
wrapAndUnwrapSol: wrapUnwrapSol,
useSharedAccounts: true
}
})

} = swapInstructionsResult
const jupiterInstructions = convertJupiterInstructions([
tokenLedgerInstruction,
swapInstruction
])

instructions.push(...jupiterInstructions)

// --- 3f. Add Cleanup Instructions for Temporary ATAs ---
const atasToClose: PublicKey[] = []

// Add source ATA if it's temporary
if (sourceAtaForJupiter) {
atasToClose.push(sourceAtaForJupiter)
}
// Add output ATA if it's temporary
if (outputAtaForJupiter) {
atasToClose.push(outputAtaForJupiter)
}

// Add close account instructions for all ATAs that need to be closed
for (const ataToClose of atasToClose) {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,7 @@ import { useMemo } from 'react'
import { QuoteResponse, SwapMode } from '@jup-ag/api'
import { useQuery } from '@tanstack/react-query'

import { getJupiterQuoteByMint } from '~/services/Jupiter'
import { getJupiterQuoteByMint, MAX_ALLOWED_ACCOUNTS } from '~/services/Jupiter'

import { QUERY_KEYS } from '../queryKeys'
import { QueryOptions, type QueryKey } from '../types'
Expand DownExpand Up@@ -103,7 +103,8 @@ export const useTokenExchangeRate = (
amountUi: safeInputAmount,
slippageBps: SLIPPAGE_BPS,
swapMode: params.swapMode ?? 'ExactIn',
onlyDirectRoutes: false
onlyDirectRoutes: false,
maxAccounts: MAX_ALLOWED_ACCOUNTS
})

// Calculate exchange rate (how many output tokens per 1 input token)
Expand Down
39 changes: 39 additions & 0 deletions packages/common/src/api/tan-query/jupiter/utils.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,6 +103,45 @@ export async function addAtaToUserBankInstructions({
return userBankAddress
}

/**
* Creates an Associated Token Account (ATA) for Jupiter when shared accounts are not supported.
* This is used as a fallback when Jupiter's shared account system fails for simple AMMs.
*
* @param tokenConfig - The token configuration containing mint address
* @param userPublicKey - The user's public key
* @param feePayer - The fee payer's public key
* @param instructions - Array to push the ATA creation instruction to
* @returns The created ATA public key
*/
export function addJupiterOutputAtaInstruction({
tokenConfig,
userPublicKey,
feePayer,
instructions
}: {
tokenConfig: UserBankManagedTokenInfo
userPublicKey: PublicKey
feePayer: PublicKey
instructions: TransactionInstruction[]
}): PublicKey {
const outputAtaForJupiter = getAssociatedTokenAddressSync(
new PublicKey(tokenConfig.mintAddress),
userPublicKey,
true
)

instructions.push(
createAssociatedTokenAccountIdempotentInstruction(
feePayer,
outputAtaForJupiter,
userPublicKey,
new PublicKey(tokenConfig.mintAddress)
)
)

return outputAtaForJupiter
}

/**
* Get the appropriate error response for a swap error based on the error stage.
*/
Expand Down
65 changes: 63 additions & 2 deletions packages/common/src/services/Jupiter.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,7 +20,8 @@ export const SLIPPAGE_TOLERANCE_EXCEEDED_ERROR = 6001
// Define JupiterTokenSymbol type here since we can't import it directly
export type JupiterTokenSymbol = keyof typeof TOKEN_LISTING_MAP

const MAX_ACCOUNTS = 20
export const DEFAULT_MAX_ACCOUNTS = 20
export const MAX_ALLOWED_ACCOUNTS = 64

let _jup: ReturnType<typeof createJupiterApiClient>

Expand DownExpand Up@@ -99,7 +100,7 @@ export const getJupiterQuoteByMint = async ({
slippageBps,
swapMode = 'ExactIn',
onlyDirectRoutes = false,
maxAccounts = MAX_ACCOUNTS
maxAccounts = DEFAULT_MAX_ACCOUNTS
}: JupiterMintQuoteParams): Promise<JupiterQuoteResult> => {
const amount =
swapMode === 'ExactIn'
Expand DownExpand Up@@ -137,6 +138,66 @@ export const getJupiterQuoteByMint = async ({
}
}

export type JupiterQuoteWithRetryResult = {
maxAccountsValue: number
quoteResult: JupiterQuoteResult
}

/**
* Gets a Jupiter quote with automatic retry logic for maxAccounts
* Starts with DEFAULT_MAX_ACCOUNTS and increments by 10 until MAX_ALLOWED_ACCOUNTS
* Returns the successful quote along with the maxAccounts value that worked
*/
export const getJupiterQuoteByMintWithRetry = async ({
inputMint,
outputMint,
inputDecimals,
outputDecimals,
amountUi,
slippageBps,
swapMode = 'ExactIn',
onlyDirectRoutes = false
}: Omit<
JupiterMintQuoteParams,
'maxAccounts'
>): Promise<JupiterQuoteWithRetryResult> => {
let maxAccounts = DEFAULT_MAX_ACCOUNTS
let lastError
let quoteResult: JupiterQuoteResult | null = null

while (maxAccounts <= MAX_ALLOWED_ACCOUNTS) {
try {
quoteResult = await getJupiterQuoteByMint({
inputMint,
outputMint,
inputDecimals,
outputDecimals,
amountUi,
slippageBps,
swapMode,
onlyDirectRoutes,
maxAccounts
})
break
} catch (err) {
lastError = err
maxAccounts += 10
if (maxAccounts > MAX_ALLOWED_ACCOUNTS) {
throw lastError
}
}
}

if (quoteResult === null) {
throw lastError
}

return {
maxAccountsValue: maxAccounts,
quoteResult
}
}

/**
* Converts an array of Jupiter instructions to Solana TransactionInstructions
* Filters out undefined instructions and handles the conversion
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
# Solana Relay Plugin

Plugin to facilitate sending Audius-related Solana transactions

## Dev

Create a file `dev.env` and add the following

```
audius_discprov_env
audius_discprov_url
audius_db_url
audius_redis_url
audius_solana_endpoint
audius_solana_listen_rpc_url
audius_solana_track_listen_count_address
audius_solana_waudio_mint
audius_solana_usdc_mint
audius_solana_bonk_mint
audius_solana_user_bank_program_address
audius_solana_payment_router_program_address
audius_solana_rewards_manager_program_address
audius_solana_rewards_manager_account
audius_solana_fee_payer_wallets
audius_delegate_private_key
audius_ipdata_api_key
audius_solana_eth_registry_program
audius_solana_listens_valid_signer
audius_solana_signer_private_key
audius_identity_relayer_public_key
audius_solana_listens_ip_hourly_rate_limit
audius_solana_listens_ip_daily_rate_limit
audius_solana_listens_ip_weekly_rate_limit
audius_solana_listens_track_hourly_rate_limit
audius_solana_listens_track_daily_rate_limit
audius_solana_listens_track_weekly_rate_limit
audius_anti_abuse_oracle
```

Then

```
npm run dev
```

If using with the web app, updat the `SOLANA_RELAY_ENDPOINT` value in `packages/web/src/services/env/env.<env>.ts` to point to the localhost port that the relay server is running on (i.e. localhost:6002)
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 53 additions & 17 deletions packages/common/src/api/tan-query/jupiter/useSwapTokens.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,8 +14,9 @@ import {
import { Feature } from '~/models'
import {
convertJupiterInstructions,
getJupiterQuoteByMint,
jupiterInstance
getJupiterQuoteByMintWithRetry,
jupiterInstance,
JupiterQuoteResult
} from '~/services/Jupiter'
import { useTokens } from '~/store/ui/buy-sell'
import { TokenInfo } from '~/store/ui/buy-sell/types'
Expand All@@ -28,7 +29,11 @@ import {
SwapTokensResult,
UserBankManagedTokenInfo
} from './types'
import { addUserBankToAtaInstructions, getSwapErrorResponse } from './utils'
import {
addUserBankToAtaInstructions,
addJupiterOutputAtaInstruction,
getSwapErrorResponse
} from './utils'

const SWAP_LOOKUP_TABLE_ADDRESS = new PublicKey(
'2WB87JxGZieRd7hi3y87wq6HAsPLyb9zrSx8B5z1QEzM'
Expand DownExpand Up@@ -74,7 +79,7 @@ export const useSwapTokens = () => {
const slippageBps = params.slippageBps ?? 50
const wrapUnwrapSol = params.wrapUnwrapSol ?? true

let quoteResult
let quoteResult: JupiterQuoteResult | undefined
let signature: string | undefined
let errorStage = 'UNKNOWN'
const instructions: TransactionInstruction[] = []
Expand DownExpand Up@@ -117,7 +122,7 @@ export const useSwapTokens = () => {

// ---------- 3. Get Quote from Jupiter ----------
errorStage = 'QUOTE_RETRIEVAL'
quoteResult= await getJupiterQuoteByMint({
const { quoteResult: quote } = await getJupiterQuoteByMintWithRetry({
inputMint: inputMintUiAddress,
outputMint: outputMintUiAddress,
inputDecimals: inputToken.decimals,
Expand All@@ -127,6 +132,7 @@ export const useSwapTokens = () => {
swapMode: 'ExactIn',
onlyDirectRoutes: false
})
quoteResult = quote

// ---------- 4. Prepare Transaction Instructions ----------

Expand All@@ -146,6 +152,7 @@ export const useSwapTokens = () => {
feePayer,
instructions
})
let outputAtaForJupiter: PublicKey | undefined

// --- 3b. Determine Jupiter's Destination Token Account ---
errorStage = 'OUTPUT_TOKEN_PREPARATION'
Expand All@@ -160,34 +167,63 @@ export const useSwapTokens = () => {

// --- 3c. Get Jupiter Swap Instructions ---
errorStage = 'SWAP_INSTRUCTION_RETRIEVAL'
let swapInstructionsResult
const swapRequestParams = {
quoteResponse: quoteResult.quote,
userPublicKey: userPublicKey.toBase58(),
destinationTokenAccount: preferredJupiterDestination,
wrapAndUnwrapSol: wrapUnwrapSol
}
try {
swapInstructionsResult = await jupiterInstance.swapInstructionsPost({
swapRequest: {
...swapRequestParams,
useSharedAccounts: true
}
})
} catch (e) {
// In the case of simple AMMs, we will see an error that looks like this:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should probably check for that error case in particular?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought about this, but I'm worried about casing it on the API, which could change. There's no harm in retrying once here, I think

// {
// error: "Simple AMMs are not supported with shared accounts"
// errorCode: "NOT_SUPPORTED"
// }
// We can retry with useSharedAccounts: false and bail out if that still errors
swapInstructionsResult = await jupiterInstance.swapInstructionsPost({
swapRequest: {
...swapRequestParams,
useSharedAccounts: false
}
})
// If getting the instruction succeeded, we need to manually handle
// the intermediary account for the output mint
outputAtaForJupiter = addJupiterOutputAtaInstruction({
tokenConfig: outputTokenConfig,
userPublicKey,
feePayer,
instructions
})
}
const {
tokenLedgerInstruction,
swapInstruction,
addressLookupTableAddresses
} = await jupiterInstance.swapInstructionsPost({
swapRequest: {
quoteResponse: quoteResult.quote,
userPublicKey: userPublicKey.toBase58(),
destinationTokenAccount: preferredJupiterDestination,
wrapAndUnwrapSol: wrapUnwrapSol,
useSharedAccounts: true
}
})

} = swapInstructionsResult
const jupiterInstructions = convertJupiterInstructions([
tokenLedgerInstruction,
swapInstruction
])

instructions.push(...jupiterInstructions)

// --- 3f. Add Cleanup Instructions for Temporary ATAs ---
const atasToClose: PublicKey[] = []

// Add source ATA if it's temporary
if (sourceAtaForJupiter) {
atasToClose.push(sourceAtaForJupiter)
}
// Add output ATA if it's temporary
if (outputAtaForJupiter) {
atasToClose.push(outputAtaForJupiter)
}

// Add close account instructions for all ATAs that need to be closed
for (const ataToClose of atasToClose) {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,7 @@ import { useMemo } from 'react'
import { QuoteResponse, SwapMode } from '@jup-ag/api'
import { useQuery } from '@tanstack/react-query'

import { getJupiterQuoteByMint } from '~/services/Jupiter'
import { getJupiterQuoteByMint, MAX_ALLOWED_ACCOUNTS } from '~/services/Jupiter'

import { QUERY_KEYS } from '../queryKeys'
import { QueryOptions, type QueryKey } from '../types'
Expand DownExpand Up@@ -103,7 +103,8 @@ export const useTokenExchangeRate = (
amountUi: safeInputAmount,
slippageBps: SLIPPAGE_BPS,
swapMode: params.swapMode ?? 'ExactIn',
onlyDirectRoutes: false
onlyDirectRoutes: false,
maxAccounts: MAX_ALLOWED_ACCOUNTS
})

// Calculate exchange rate (how many output tokens per 1 input token)
Expand Down
39 changes: 39 additions & 0 deletions packages/common/src/api/tan-query/jupiter/utils.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,6 +103,45 @@ export async function addAtaToUserBankInstructions({
return userBankAddress
}

/**
* Creates an Associated Token Account (ATA) for Jupiter when shared accounts are not supported.
* This is used as a fallback when Jupiter's shared account system fails for simple AMMs.
*
* @param tokenConfig - The token configuration containing mint address
* @param userPublicKey - The user's public key
* @param feePayer - The fee payer's public key
* @param instructions - Array to push the ATA creation instruction to
* @returns The created ATA public key
*/
export function addJupiterOutputAtaInstruction({
tokenConfig,
userPublicKey,
feePayer,
instructions
}: {
tokenConfig: UserBankManagedTokenInfo
userPublicKey: PublicKey
feePayer: PublicKey
instructions: TransactionInstruction[]
}): PublicKey {
const outputAtaForJupiter = getAssociatedTokenAddressSync(
new PublicKey(tokenConfig.mintAddress),
userPublicKey,
true
)

instructions.push(
createAssociatedTokenAccountIdempotentInstruction(
feePayer,
outputAtaForJupiter,
userPublicKey,
new PublicKey(tokenConfig.mintAddress)
)
)

return outputAtaForJupiter
}

/**
* Get the appropriate error response for a swap error based on the error stage.
*/
Expand Down
65 changes: 63 additions & 2 deletions packages/common/src/services/Jupiter.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,7 +20,8 @@ export const SLIPPAGE_TOLERANCE_EXCEEDED_ERROR = 6001
// Define JupiterTokenSymbol type here since we can't import it directly
export type JupiterTokenSymbol = keyof typeof TOKEN_LISTING_MAP

const MAX_ACCOUNTS = 20
export const DEFAULT_MAX_ACCOUNTS = 20
export const MAX_ALLOWED_ACCOUNTS = 64

let _jup: ReturnType<typeof createJupiterApiClient>

Expand DownExpand Up@@ -99,7 +100,7 @@ export const getJupiterQuoteByMint = async ({
slippageBps,
swapMode = 'ExactIn',
onlyDirectRoutes = false,
maxAccounts = MAX_ACCOUNTS
maxAccounts = DEFAULT_MAX_ACCOUNTS
}: JupiterMintQuoteParams): Promise<JupiterQuoteResult> => {
const amount =
swapMode === 'ExactIn'
Expand DownExpand Up@@ -137,6 +138,66 @@ export const getJupiterQuoteByMint = async ({
}
}

export type JupiterQuoteWithRetryResult = {
maxAccountsValue: number
quoteResult: JupiterQuoteResult
}

/**
* Gets a Jupiter quote with automatic retry logic for maxAccounts
* Starts with DEFAULT_MAX_ACCOUNTS and increments by 10 until MAX_ALLOWED_ACCOUNTS
* Returns the successful quote along with the maxAccounts value that worked
*/
export const getJupiterQuoteByMintWithRetry = async ({
inputMint,
outputMint,
inputDecimals,
outputDecimals,
amountUi,
slippageBps,
swapMode = 'ExactIn',
onlyDirectRoutes = false
}: Omit<
JupiterMintQuoteParams,
'maxAccounts'
>): Promise<JupiterQuoteWithRetryResult> => {
let maxAccounts = DEFAULT_MAX_ACCOUNTS
let lastError
let quoteResult: JupiterQuoteResult | null = null

while (maxAccounts <= MAX_ALLOWED_ACCOUNTS) {
try {
quoteResult = await getJupiterQuoteByMint({
inputMint,
outputMint,
inputDecimals,
outputDecimals,
amountUi,
slippageBps,
swapMode,
onlyDirectRoutes,
maxAccounts
})
break
} catch (err) {
lastError = err
maxAccounts += 10
if (maxAccounts > MAX_ALLOWED_ACCOUNTS) {
throw lastError
}
}
}

if (quoteResult === null) {
throw lastError
}

return {
maxAccountsValue: maxAccounts,
quoteResult
}
}

/**
* Converts an array of Jupiter instructions to Solana TransactionInstructions
* Filters out undefined instructions and handles the conversion
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
# Solana Relay Plugin

Plugin to facilitate sending Audius-related Solana transactions

## Dev

Create a file `dev.env` and add the following

```
audius_discprov_env
audius_discprov_url
audius_db_url
audius_redis_url
audius_solana_endpoint
audius_solana_listen_rpc_url
audius_solana_track_listen_count_address
audius_solana_waudio_mint
audius_solana_usdc_mint
audius_solana_bonk_mint
audius_solana_user_bank_program_address
audius_solana_payment_router_program_address
audius_solana_rewards_manager_program_address
audius_solana_rewards_manager_account
audius_solana_fee_payer_wallets
audius_delegate_private_key
audius_ipdata_api_key
audius_solana_eth_registry_program
audius_solana_listens_valid_signer
audius_solana_signer_private_key
audius_identity_relayer_public_key
audius_solana_listens_ip_hourly_rate_limit
audius_solana_listens_ip_daily_rate_limit
audius_solana_listens_ip_weekly_rate_limit
audius_solana_listens_track_hourly_rate_limit
audius_solana_listens_track_daily_rate_limit
audius_solana_listens_track_weekly_rate_limit
audius_anti_abuse_oracle
```

Then

```
npm run dev
```

If using with the web app, updat the `SOLANA_RELAY_ENDPOINT` value in `packages/web/src/services/env/env.<env>.ts` to point to the localhost port that the relay server is running on (i.e. localhost:6002)
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 53 additions & 17 deletions packages/common/src/api/tan-query/jupiter/useSwapTokens.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,8 +14,9 @@ import {
import { Feature } from '~/models'
import {
convertJupiterInstructions,
getJupiterQuoteByMint,
jupiterInstance
getJupiterQuoteByMintWithRetry,
jupiterInstance,
JupiterQuoteResult
} from '~/services/Jupiter'
import { useTokens } from '~/store/ui/buy-sell'
import { TokenInfo } from '~/store/ui/buy-sell/types'
Expand All@@ -28,7 +29,11 @@ import {
SwapTokensResult,
UserBankManagedTokenInfo
} from './types'
import { addUserBankToAtaInstructions, getSwapErrorResponse } from './utils'
import {
addUserBankToAtaInstructions,
addJupiterOutputAtaInstruction,
getSwapErrorResponse
} from './utils'

const SWAP_LOOKUP_TABLE_ADDRESS = new PublicKey(
'2WB87JxGZieRd7hi3y87wq6HAsPLyb9zrSx8B5z1QEzM'
Expand DownExpand Up@@ -74,7 +79,7 @@ export const useSwapTokens = () => {
const slippageBps = params.slippageBps ?? 50
const wrapUnwrapSol = params.wrapUnwrapSol ?? true

let quoteResult
let quoteResult: JupiterQuoteResult | undefined
let signature: string | undefined
let errorStage = 'UNKNOWN'
const instructions: TransactionInstruction[] = []
Expand DownExpand Up@@ -117,7 +122,7 @@ export const useSwapTokens = () => {

// ---------- 3. Get Quote from Jupiter ----------
errorStage = 'QUOTE_RETRIEVAL'
quoteResult= await getJupiterQuoteByMint({
const { quoteResult: quote } = await getJupiterQuoteByMintWithRetry({
inputMint: inputMintUiAddress,
outputMint: outputMintUiAddress,
inputDecimals: inputToken.decimals,
Expand All@@ -127,6 +132,7 @@ export const useSwapTokens = () => {
swapMode: 'ExactIn',
onlyDirectRoutes: false
})
quoteResult = quote

// ---------- 4. Prepare Transaction Instructions ----------

Expand All@@ -146,6 +152,7 @@ export const useSwapTokens = () => {
feePayer,
instructions
})
let outputAtaForJupiter: PublicKey | undefined

// --- 3b. Determine Jupiter's Destination Token Account ---
errorStage = 'OUTPUT_TOKEN_PREPARATION'
Expand All@@ -160,34 +167,63 @@ export const useSwapTokens = () => {

// --- 3c. Get Jupiter Swap Instructions ---
errorStage = 'SWAP_INSTRUCTION_RETRIEVAL'
let swapInstructionsResult
const swapRequestParams = {
quoteResponse: quoteResult.quote,
userPublicKey: userPublicKey.toBase58(),
destinationTokenAccount: preferredJupiterDestination,
wrapAndUnwrapSol: wrapUnwrapSol
}
try {
swapInstructionsResult = await jupiterInstance.swapInstructionsPost({
swapRequest: {
...swapRequestParams,
useSharedAccounts: true
}
})
} catch (e) {
// In the case of simple AMMs, we will see an error that looks like this:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should probably check for that error case in particular?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought about this, but I'm worried about casing it on the API, which could change. There's no harm in retrying once here, I think

// {
// error: "Simple AMMs are not supported with shared accounts"
// errorCode: "NOT_SUPPORTED"
// }
// We can retry with useSharedAccounts: false and bail out if that still errors
swapInstructionsResult = await jupiterInstance.swapInstructionsPost({
swapRequest: {
...swapRequestParams,
useSharedAccounts: false
}
})
// If getting the instruction succeeded, we need to manually handle
// the intermediary account for the output mint
outputAtaForJupiter = addJupiterOutputAtaInstruction({
tokenConfig: outputTokenConfig,
userPublicKey,
feePayer,
instructions
})
}
const {
tokenLedgerInstruction,
swapInstruction,
addressLookupTableAddresses
} = await jupiterInstance.swapInstructionsPost({
swapRequest: {
quoteResponse: quoteResult.quote,
userPublicKey: userPublicKey.toBase58(),
destinationTokenAccount: preferredJupiterDestination,
wrapAndUnwrapSol: wrapUnwrapSol,
useSharedAccounts: true
}
})

} = swapInstructionsResult
const jupiterInstructions = convertJupiterInstructions([
tokenLedgerInstruction,
swapInstruction
])

instructions.push(...jupiterInstructions)

// --- 3f. Add Cleanup Instructions for Temporary ATAs ---
const atasToClose: PublicKey[] = []

// Add source ATA if it's temporary
if (sourceAtaForJupiter) {
atasToClose.push(sourceAtaForJupiter)
}
// Add output ATA if it's temporary
if (outputAtaForJupiter) {
atasToClose.push(outputAtaForJupiter)
}

// Add close account instructions for all ATAs that need to be closed
for (const ataToClose of atasToClose) {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,7 @@ import { useMemo } from 'react'
import { QuoteResponse, SwapMode } from '@jup-ag/api'
import { useQuery } from '@tanstack/react-query'

import { getJupiterQuoteByMint } from '~/services/Jupiter'
import { getJupiterQuoteByMint, MAX_ALLOWED_ACCOUNTS } from '~/services/Jupiter'

import { QUERY_KEYS } from '../queryKeys'
import { QueryOptions, type QueryKey } from '../types'
Expand DownExpand Up@@ -103,7 +103,8 @@ export const useTokenExchangeRate = (
amountUi: safeInputAmount,
slippageBps: SLIPPAGE_BPS,
swapMode: params.swapMode ?? 'ExactIn',
onlyDirectRoutes: false
onlyDirectRoutes: false,
maxAccounts: MAX_ALLOWED_ACCOUNTS
})

// Calculate exchange rate (how many output tokens per 1 input token)
Expand Down
39 changes: 39 additions & 0 deletions packages/common/src/api/tan-query/jupiter/utils.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,6 +103,45 @@ export async function addAtaToUserBankInstructions({
return userBankAddress
}

/**
* Creates an Associated Token Account (ATA) for Jupiter when shared accounts are not supported.
* This is used as a fallback when Jupiter's shared account system fails for simple AMMs.
*
* @param tokenConfig - The token configuration containing mint address
* @param userPublicKey - The user's public key
* @param feePayer - The fee payer's public key
* @param instructions - Array to push the ATA creation instruction to
* @returns The created ATA public key
*/
export function addJupiterOutputAtaInstruction({
tokenConfig,
userPublicKey,
feePayer,
instructions
}: {
tokenConfig: UserBankManagedTokenInfo
userPublicKey: PublicKey
feePayer: PublicKey
instructions: TransactionInstruction[]
}): PublicKey {
const outputAtaForJupiter = getAssociatedTokenAddressSync(
new PublicKey(tokenConfig.mintAddress),
userPublicKey,
true
)

instructions.push(
createAssociatedTokenAccountIdempotentInstruction(
feePayer,
outputAtaForJupiter,
userPublicKey,
new PublicKey(tokenConfig.mintAddress)
)
)

return outputAtaForJupiter
}

/**
* Get the appropriate error response for a swap error based on the error stage.
*/
Expand Down
65 changes: 63 additions & 2 deletions packages/common/src/services/Jupiter.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,7 +20,8 @@ export const SLIPPAGE_TOLERANCE_EXCEEDED_ERROR = 6001
// Define JupiterTokenSymbol type here since we can't import it directly
export type JupiterTokenSymbol = keyof typeof TOKEN_LISTING_MAP

const MAX_ACCOUNTS = 20
export const DEFAULT_MAX_ACCOUNTS = 20
export const MAX_ALLOWED_ACCOUNTS = 64

let _jup: ReturnType<typeof createJupiterApiClient>

Expand DownExpand Up@@ -99,7 +100,7 @@ export const getJupiterQuoteByMint = async ({
slippageBps,
swapMode = 'ExactIn',
onlyDirectRoutes = false,
maxAccounts = MAX_ACCOUNTS
maxAccounts = DEFAULT_MAX_ACCOUNTS
}: JupiterMintQuoteParams): Promise<JupiterQuoteResult> => {
const amount =
swapMode === 'ExactIn'
Expand DownExpand Up@@ -137,6 +138,66 @@ export const getJupiterQuoteByMint = async ({
}
}

export type JupiterQuoteWithRetryResult = {
maxAccountsValue: number
quoteResult: JupiterQuoteResult
}

/**
* Gets a Jupiter quote with automatic retry logic for maxAccounts
* Starts with DEFAULT_MAX_ACCOUNTS and increments by 10 until MAX_ALLOWED_ACCOUNTS
* Returns the successful quote along with the maxAccounts value that worked
*/
export const getJupiterQuoteByMintWithRetry = async ({
inputMint,
outputMint,
inputDecimals,
outputDecimals,
amountUi,
slippageBps,
swapMode = 'ExactIn',
onlyDirectRoutes = false
}: Omit<
JupiterMintQuoteParams,
'maxAccounts'
>): Promise<JupiterQuoteWithRetryResult> => {
let maxAccounts = DEFAULT_MAX_ACCOUNTS
let lastError
let quoteResult: JupiterQuoteResult | null = null

while (maxAccounts <= MAX_ALLOWED_ACCOUNTS) {
try {
quoteResult = await getJupiterQuoteByMint({
inputMint,
outputMint,
inputDecimals,
outputDecimals,
amountUi,
slippageBps,
swapMode,
onlyDirectRoutes,
maxAccounts
})
break
} catch (err) {
lastError = err
maxAccounts += 10
if (maxAccounts > MAX_ALLOWED_ACCOUNTS) {
throw lastError
}
}
}

if (quoteResult === null) {
throw lastError
}

return {
maxAccountsValue: maxAccounts,
quoteResult
}
}

/**
* Converts an array of Jupiter instructions to Solana TransactionInstructions
* Filters out undefined instructions and handles the conversion
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
# Solana Relay Plugin

Plugin to facilitate sending Audius-related Solana transactions

## Dev

Create a file `dev.env` and add the following

```
audius_discprov_env
audius_discprov_url
audius_db_url
audius_redis_url
audius_solana_endpoint
audius_solana_listen_rpc_url
audius_solana_track_listen_count_address
audius_solana_waudio_mint
audius_solana_usdc_mint
audius_solana_bonk_mint
audius_solana_user_bank_program_address
audius_solana_payment_router_program_address
audius_solana_rewards_manager_program_address
audius_solana_rewards_manager_account
audius_solana_fee_payer_wallets
audius_delegate_private_key
audius_ipdata_api_key
audius_solana_eth_registry_program
audius_solana_listens_valid_signer
audius_solana_signer_private_key
audius_identity_relayer_public_key
audius_solana_listens_ip_hourly_rate_limit
audius_solana_listens_ip_daily_rate_limit
audius_solana_listens_ip_weekly_rate_limit
audius_solana_listens_track_hourly_rate_limit
audius_solana_listens_track_daily_rate_limit
audius_solana_listens_track_weekly_rate_limit
audius_anti_abuse_oracle
```

Then

```
npm run dev
```

If using with the web app, updat the `SOLANA_RELAY_ENDPOINT` value in `packages/web/src/services/env/env.<env>.ts` to point to the localhost port that the relay server is running on (i.e. localhost:6002)
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 53 additions & 17 deletions packages/common/src/api/tan-query/jupiter/useSwapTokens.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,8 +14,9 @@ import {
import { Feature } from '~/models'
import {
convertJupiterInstructions,
getJupiterQuoteByMint,
jupiterInstance
getJupiterQuoteByMintWithRetry,
jupiterInstance,
JupiterQuoteResult
} from '~/services/Jupiter'
import { useTokens } from '~/store/ui/buy-sell'
import { TokenInfo } from '~/store/ui/buy-sell/types'
Expand All@@ -28,7 +29,11 @@ import {
SwapTokensResult,
UserBankManagedTokenInfo
} from './types'
import { addUserBankToAtaInstructions, getSwapErrorResponse } from './utils'
import {
addUserBankToAtaInstructions,
addJupiterOutputAtaInstruction,
getSwapErrorResponse
} from './utils'

const SWAP_LOOKUP_TABLE_ADDRESS = new PublicKey(
'2WB87JxGZieRd7hi3y87wq6HAsPLyb9zrSx8B5z1QEzM'
Expand DownExpand Up@@ -74,7 +79,7 @@ export const useSwapTokens = () => {
const slippageBps = params.slippageBps ?? 50
const wrapUnwrapSol = params.wrapUnwrapSol ?? true

let quoteResult
let quoteResult: JupiterQuoteResult | undefined
let signature: string | undefined
let errorStage = 'UNKNOWN'
const instructions: TransactionInstruction[] = []
Expand DownExpand Up@@ -117,7 +122,7 @@ export const useSwapTokens = () => {

// ---------- 3. Get Quote from Jupiter ----------
errorStage = 'QUOTE_RETRIEVAL'
quoteResult= await getJupiterQuoteByMint({
const { quoteResult: quote } = await getJupiterQuoteByMintWithRetry({
inputMint: inputMintUiAddress,
outputMint: outputMintUiAddress,
inputDecimals: inputToken.decimals,
Expand All@@ -127,6 +132,7 @@ export const useSwapTokens = () => {
swapMode: 'ExactIn',
onlyDirectRoutes: false
})
quoteResult = quote

// ---------- 4. Prepare Transaction Instructions ----------

Expand All@@ -146,6 +152,7 @@ export const useSwapTokens = () => {
feePayer,
instructions
})
let outputAtaForJupiter: PublicKey | undefined

// --- 3b. Determine Jupiter's Destination Token Account ---
errorStage = 'OUTPUT_TOKEN_PREPARATION'
Expand All@@ -160,34 +167,63 @@ export const useSwapTokens = () => {

// --- 3c. Get Jupiter Swap Instructions ---
errorStage = 'SWAP_INSTRUCTION_RETRIEVAL'
let swapInstructionsResult
const swapRequestParams = {
quoteResponse: quoteResult.quote,
userPublicKey: userPublicKey.toBase58(),
destinationTokenAccount: preferredJupiterDestination,
wrapAndUnwrapSol: wrapUnwrapSol
}
try {
swapInstructionsResult = await jupiterInstance.swapInstructionsPost({
swapRequest: {
...swapRequestParams,
useSharedAccounts: true
}
})
} catch (e) {
// In the case of simple AMMs, we will see an error that looks like this:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should probably check for that error case in particular?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought about this, but I'm worried about casing it on the API, which could change. There's no harm in retrying once here, I think

// {
// error: "Simple AMMs are not supported with shared accounts"
// errorCode: "NOT_SUPPORTED"
// }
// We can retry with useSharedAccounts: false and bail out if that still errors
swapInstructionsResult = await jupiterInstance.swapInstructionsPost({
swapRequest: {
...swapRequestParams,
useSharedAccounts: false
}
})
// If getting the instruction succeeded, we need to manually handle
// the intermediary account for the output mint
outputAtaForJupiter = addJupiterOutputAtaInstruction({
tokenConfig: outputTokenConfig,
userPublicKey,
feePayer,
instructions
})
}
const {
tokenLedgerInstruction,
swapInstruction,
addressLookupTableAddresses
} = await jupiterInstance.swapInstructionsPost({
swapRequest: {
quoteResponse: quoteResult.quote,
userPublicKey: userPublicKey.toBase58(),
destinationTokenAccount: preferredJupiterDestination,
wrapAndUnwrapSol: wrapUnwrapSol,
useSharedAccounts: true
}
})

} = swapInstructionsResult
const jupiterInstructions = convertJupiterInstructions([
tokenLedgerInstruction,
swapInstruction
])

instructions.push(...jupiterInstructions)

// --- 3f. Add Cleanup Instructions for Temporary ATAs ---
const atasToClose: PublicKey[] = []

// Add source ATA if it's temporary
if (sourceAtaForJupiter) {
atasToClose.push(sourceAtaForJupiter)
}
// Add output ATA if it's temporary
if (outputAtaForJupiter) {
atasToClose.push(outputAtaForJupiter)
}

// Add close account instructions for all ATAs that need to be closed
for (const ataToClose of atasToClose) {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,7 @@ import { useMemo } from 'react'
import { QuoteResponse, SwapMode } from '@jup-ag/api'
import { useQuery } from '@tanstack/react-query'

import { getJupiterQuoteByMint } from '~/services/Jupiter'
import { getJupiterQuoteByMint, MAX_ALLOWED_ACCOUNTS } from '~/services/Jupiter'

import { QUERY_KEYS } from '../queryKeys'
import { QueryOptions, type QueryKey } from '../types'
Expand DownExpand Up@@ -103,7 +103,8 @@ export const useTokenExchangeRate = (
amountUi: safeInputAmount,
slippageBps: SLIPPAGE_BPS,
swapMode: params.swapMode ?? 'ExactIn',
onlyDirectRoutes: false
onlyDirectRoutes: false,
maxAccounts: MAX_ALLOWED_ACCOUNTS
})

// Calculate exchange rate (how many output tokens per 1 input token)
Expand Down
39 changes: 39 additions & 0 deletions packages/common/src/api/tan-query/jupiter/utils.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,6 +103,45 @@ export async function addAtaToUserBankInstructions({
return userBankAddress
}

/**
* Creates an Associated Token Account (ATA) for Jupiter when shared accounts are not supported.
* This is used as a fallback when Jupiter's shared account system fails for simple AMMs.
*
* @param tokenConfig - The token configuration containing mint address
* @param userPublicKey - The user's public key
* @param feePayer - The fee payer's public key
* @param instructions - Array to push the ATA creation instruction to
* @returns The created ATA public key
*/
export function addJupiterOutputAtaInstruction({
tokenConfig,
userPublicKey,
feePayer,
instructions
}: {
tokenConfig: UserBankManagedTokenInfo
userPublicKey: PublicKey
feePayer: PublicKey
instructions: TransactionInstruction[]
}): PublicKey {
const outputAtaForJupiter = getAssociatedTokenAddressSync(
new PublicKey(tokenConfig.mintAddress),
userPublicKey,
true
)

instructions.push(
createAssociatedTokenAccountIdempotentInstruction(
feePayer,
outputAtaForJupiter,
userPublicKey,
new PublicKey(tokenConfig.mintAddress)
)
)

return outputAtaForJupiter
}

/**
* Get the appropriate error response for a swap error based on the error stage.
*/
Expand Down
65 changes: 63 additions & 2 deletions packages/common/src/services/Jupiter.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,7 +20,8 @@ export const SLIPPAGE_TOLERANCE_EXCEEDED_ERROR = 6001
// Define JupiterTokenSymbol type here since we can't import it directly
export type JupiterTokenSymbol = keyof typeof TOKEN_LISTING_MAP

const MAX_ACCOUNTS = 20
export const DEFAULT_MAX_ACCOUNTS = 20
export const MAX_ALLOWED_ACCOUNTS = 64

let _jup: ReturnType<typeof createJupiterApiClient>

Expand DownExpand Up@@ -99,7 +100,7 @@ export const getJupiterQuoteByMint = async ({
slippageBps,
swapMode = 'ExactIn',
onlyDirectRoutes = false,
maxAccounts = MAX_ACCOUNTS
maxAccounts = DEFAULT_MAX_ACCOUNTS
}: JupiterMintQuoteParams): Promise<JupiterQuoteResult> => {
const amount =
swapMode === 'ExactIn'
Expand DownExpand Up@@ -137,6 +138,66 @@ export const getJupiterQuoteByMint = async ({
}
}

export type JupiterQuoteWithRetryResult = {
maxAccountsValue: number
quoteResult: JupiterQuoteResult
}

/**
* Gets a Jupiter quote with automatic retry logic for maxAccounts
* Starts with DEFAULT_MAX_ACCOUNTS and increments by 10 until MAX_ALLOWED_ACCOUNTS
* Returns the successful quote along with the maxAccounts value that worked
*/
export const getJupiterQuoteByMintWithRetry = async ({
inputMint,
outputMint,
inputDecimals,
outputDecimals,
amountUi,
slippageBps,
swapMode = 'ExactIn',
onlyDirectRoutes = false
}: Omit<
JupiterMintQuoteParams,
'maxAccounts'
>): Promise<JupiterQuoteWithRetryResult> => {
let maxAccounts = DEFAULT_MAX_ACCOUNTS
let lastError
let quoteResult: JupiterQuoteResult | null = null

while (maxAccounts <= MAX_ALLOWED_ACCOUNTS) {
try {
quoteResult = await getJupiterQuoteByMint({
inputMint,
outputMint,
inputDecimals,
outputDecimals,
amountUi,
slippageBps,
swapMode,
onlyDirectRoutes,
maxAccounts
})
break
} catch (err) {
lastError = err
maxAccounts += 10
if (maxAccounts > MAX_ALLOWED_ACCOUNTS) {
throw lastError
}
}
}

if (quoteResult === null) {
throw lastError
}

return {
maxAccountsValue: maxAccounts,
quoteResult
}
}

/**
* Converts an array of Jupiter instructions to Solana TransactionInstructions
* Filters out undefined instructions and handles the conversion
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
# Solana Relay Plugin

Plugin to facilitate sending Audius-related Solana transactions

## Dev

Create a file `dev.env` and add the following

```
audius_discprov_env
audius_discprov_url
audius_db_url
audius_redis_url
audius_solana_endpoint
audius_solana_listen_rpc_url
audius_solana_track_listen_count_address
audius_solana_waudio_mint
audius_solana_usdc_mint
audius_solana_bonk_mint
audius_solana_user_bank_program_address
audius_solana_payment_router_program_address
audius_solana_rewards_manager_program_address
audius_solana_rewards_manager_account
audius_solana_fee_payer_wallets
audius_delegate_private_key
audius_ipdata_api_key
audius_solana_eth_registry_program
audius_solana_listens_valid_signer
audius_solana_signer_private_key
audius_identity_relayer_public_key
audius_solana_listens_ip_hourly_rate_limit
audius_solana_listens_ip_daily_rate_limit
audius_solana_listens_ip_weekly_rate_limit
audius_solana_listens_track_hourly_rate_limit
audius_solana_listens_track_daily_rate_limit
audius_solana_listens_track_weekly_rate_limit
audius_anti_abuse_oracle
```

Then

```
npm run dev
```

If using with the web app, updat the `SOLANA_RELAY_ENDPOINT` value in `packages/web/src/services/env/env.<env>.ts` to point to the localhost port that the relay server is running on (i.e. localhost:6002)
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 53 additions & 17 deletions packages/common/src/api/tan-query/jupiter/useSwapTokens.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,8 +14,9 @@ import {
import { Feature } from '~/models'
import {
convertJupiterInstructions,
getJupiterQuoteByMint,
jupiterInstance
getJupiterQuoteByMintWithRetry,
jupiterInstance,
JupiterQuoteResult
} from '~/services/Jupiter'
import { useTokens } from '~/store/ui/buy-sell'
import { TokenInfo } from '~/store/ui/buy-sell/types'
Expand All@@ -28,7 +29,11 @@ import {
SwapTokensResult,
UserBankManagedTokenInfo
} from './types'
import { addUserBankToAtaInstructions, getSwapErrorResponse } from './utils'
import {
addUserBankToAtaInstructions,
addJupiterOutputAtaInstruction,
getSwapErrorResponse
} from './utils'

const SWAP_LOOKUP_TABLE_ADDRESS = new PublicKey(
'2WB87JxGZieRd7hi3y87wq6HAsPLyb9zrSx8B5z1QEzM'
Expand DownExpand Up@@ -74,7 +79,7 @@ export const useSwapTokens = () => {
const slippageBps = params.slippageBps ?? 50
const wrapUnwrapSol = params.wrapUnwrapSol ?? true

let quoteResult
let quoteResult: JupiterQuoteResult | undefined
let signature: string | undefined
let errorStage = 'UNKNOWN'
const instructions: TransactionInstruction[] = []
Expand DownExpand Up@@ -117,7 +122,7 @@ export const useSwapTokens = () => {

// ---------- 3. Get Quote from Jupiter ----------
errorStage = 'QUOTE_RETRIEVAL'
quoteResult= await getJupiterQuoteByMint({
const { quoteResult: quote } = await getJupiterQuoteByMintWithRetry({
inputMint: inputMintUiAddress,
outputMint: outputMintUiAddress,
inputDecimals: inputToken.decimals,
Expand All@@ -127,6 +132,7 @@ export const useSwapTokens = () => {
swapMode: 'ExactIn',
onlyDirectRoutes: false
})
quoteResult = quote

// ---------- 4. Prepare Transaction Instructions ----------

Expand All@@ -146,6 +152,7 @@ export const useSwapTokens = () => {
feePayer,
instructions
})
let outputAtaForJupiter: PublicKey | undefined

// --- 3b. Determine Jupiter's Destination Token Account ---
errorStage = 'OUTPUT_TOKEN_PREPARATION'
Expand All@@ -160,34 +167,63 @@ export const useSwapTokens = () => {

// --- 3c. Get Jupiter Swap Instructions ---
errorStage = 'SWAP_INSTRUCTION_RETRIEVAL'
let swapInstructionsResult
const swapRequestParams = {
quoteResponse: quoteResult.quote,
userPublicKey: userPublicKey.toBase58(),
destinationTokenAccount: preferredJupiterDestination,
wrapAndUnwrapSol: wrapUnwrapSol
}
try {
swapInstructionsResult = await jupiterInstance.swapInstructionsPost({
swapRequest: {
...swapRequestParams,
useSharedAccounts: true
}
})
} catch (e) {
// In the case of simple AMMs, we will see an error that looks like this:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should probably check for that error case in particular?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought about this, but I'm worried about casing it on the API, which could change. There's no harm in retrying once here, I think

// {
// error: "Simple AMMs are not supported with shared accounts"
// errorCode: "NOT_SUPPORTED"
// }
// We can retry with useSharedAccounts: false and bail out if that still errors
swapInstructionsResult = await jupiterInstance.swapInstructionsPost({
swapRequest: {
...swapRequestParams,
useSharedAccounts: false
}
})
// If getting the instruction succeeded, we need to manually handle
// the intermediary account for the output mint
outputAtaForJupiter = addJupiterOutputAtaInstruction({
tokenConfig: outputTokenConfig,
userPublicKey,
feePayer,
instructions
})
}
const {
tokenLedgerInstruction,
swapInstruction,
addressLookupTableAddresses
} = await jupiterInstance.swapInstructionsPost({
swapRequest: {
quoteResponse: quoteResult.quote,
userPublicKey: userPublicKey.toBase58(),
destinationTokenAccount: preferredJupiterDestination,
wrapAndUnwrapSol: wrapUnwrapSol,
useSharedAccounts: true
}
})

} = swapInstructionsResult
const jupiterInstructions = convertJupiterInstructions([
tokenLedgerInstruction,
swapInstruction
])

instructions.push(...jupiterInstructions)

// --- 3f. Add Cleanup Instructions for Temporary ATAs ---
const atasToClose: PublicKey[] = []

// Add source ATA if it's temporary
if (sourceAtaForJupiter) {
atasToClose.push(sourceAtaForJupiter)
}
// Add output ATA if it's temporary
if (outputAtaForJupiter) {
atasToClose.push(outputAtaForJupiter)
}

// Add close account instructions for all ATAs that need to be closed
for (const ataToClose of atasToClose) {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,7 @@ import { useMemo } from 'react'
import { QuoteResponse, SwapMode } from '@jup-ag/api'
import { useQuery } from '@tanstack/react-query'

import { getJupiterQuoteByMint } from '~/services/Jupiter'
import { getJupiterQuoteByMint, MAX_ALLOWED_ACCOUNTS } from '~/services/Jupiter'

import { QUERY_KEYS } from '../queryKeys'
import { QueryOptions, type QueryKey } from '../types'
Expand DownExpand Up@@ -103,7 +103,8 @@ export const useTokenExchangeRate = (
amountUi: safeInputAmount,
slippageBps: SLIPPAGE_BPS,
swapMode: params.swapMode ?? 'ExactIn',
onlyDirectRoutes: false
onlyDirectRoutes: false,
maxAccounts: MAX_ALLOWED_ACCOUNTS
})

// Calculate exchange rate (how many output tokens per 1 input token)
Expand Down
39 changes: 39 additions & 0 deletions packages/common/src/api/tan-query/jupiter/utils.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,6 +103,45 @@ export async function addAtaToUserBankInstructions({
return userBankAddress
}

/**
* Creates an Associated Token Account (ATA) for Jupiter when shared accounts are not supported.
* This is used as a fallback when Jupiter's shared account system fails for simple AMMs.
*
* @param tokenConfig - The token configuration containing mint address
* @param userPublicKey - The user's public key
* @param feePayer - The fee payer's public key
* @param instructions - Array to push the ATA creation instruction to
* @returns The created ATA public key
*/
export function addJupiterOutputAtaInstruction({
tokenConfig,
userPublicKey,
feePayer,
instructions
}: {
tokenConfig: UserBankManagedTokenInfo
userPublicKey: PublicKey
feePayer: PublicKey
instructions: TransactionInstruction[]
}): PublicKey {
const outputAtaForJupiter = getAssociatedTokenAddressSync(
new PublicKey(tokenConfig.mintAddress),
userPublicKey,
true
)

instructions.push(
createAssociatedTokenAccountIdempotentInstruction(
feePayer,
outputAtaForJupiter,
userPublicKey,
new PublicKey(tokenConfig.mintAddress)
)
)

return outputAtaForJupiter
}

/**
* Get the appropriate error response for a swap error based on the error stage.
*/
Expand Down
65 changes: 63 additions & 2 deletions packages/common/src/services/Jupiter.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,7 +20,8 @@ export const SLIPPAGE_TOLERANCE_EXCEEDED_ERROR = 6001
// Define JupiterTokenSymbol type here since we can't import it directly
export type JupiterTokenSymbol = keyof typeof TOKEN_LISTING_MAP

const MAX_ACCOUNTS = 20
export const DEFAULT_MAX_ACCOUNTS = 20
export const MAX_ALLOWED_ACCOUNTS = 64

let _jup: ReturnType<typeof createJupiterApiClient>

Expand DownExpand Up@@ -99,7 +100,7 @@ export const getJupiterQuoteByMint = async ({
slippageBps,
swapMode = 'ExactIn',
onlyDirectRoutes = false,
maxAccounts = MAX_ACCOUNTS
maxAccounts = DEFAULT_MAX_ACCOUNTS
}: JupiterMintQuoteParams): Promise<JupiterQuoteResult> => {
const amount =
swapMode === 'ExactIn'
Expand DownExpand Up@@ -137,6 +138,66 @@ export const getJupiterQuoteByMint = async ({
}
}

export type JupiterQuoteWithRetryResult = {
maxAccountsValue: number
quoteResult: JupiterQuoteResult
}

/**
* Gets a Jupiter quote with automatic retry logic for maxAccounts
* Starts with DEFAULT_MAX_ACCOUNTS and increments by 10 until MAX_ALLOWED_ACCOUNTS
* Returns the successful quote along with the maxAccounts value that worked
*/
export const getJupiterQuoteByMintWithRetry = async ({
inputMint,
outputMint,
inputDecimals,
outputDecimals,
amountUi,
slippageBps,
swapMode = 'ExactIn',
onlyDirectRoutes = false
}: Omit<
JupiterMintQuoteParams,
'maxAccounts'
>): Promise<JupiterQuoteWithRetryResult> => {
let maxAccounts = DEFAULT_MAX_ACCOUNTS
let lastError
let quoteResult: JupiterQuoteResult | null = null

while (maxAccounts <= MAX_ALLOWED_ACCOUNTS) {
try {
quoteResult = await getJupiterQuoteByMint({
inputMint,
outputMint,
inputDecimals,
outputDecimals,
amountUi,
slippageBps,
swapMode,
onlyDirectRoutes,
maxAccounts
})
break
} catch (err) {
lastError = err
maxAccounts += 10
if (maxAccounts > MAX_ALLOWED_ACCOUNTS) {
throw lastError
}
}
}

if (quoteResult === null) {
throw lastError
}

return {
maxAccountsValue: maxAccounts,
quoteResult
}
}

/**
* Converts an array of Jupiter instructions to Solana TransactionInstructions
* Filters out undefined instructions and handles the conversion
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
# Solana Relay Plugin

Plugin to facilitate sending Audius-related Solana transactions

## Dev

Create a file `dev.env` and add the following

```
audius_discprov_env
audius_discprov_url
audius_db_url
audius_redis_url
audius_solana_endpoint
audius_solana_listen_rpc_url
audius_solana_track_listen_count_address
audius_solana_waudio_mint
audius_solana_usdc_mint
audius_solana_bonk_mint
audius_solana_user_bank_program_address
audius_solana_payment_router_program_address
audius_solana_rewards_manager_program_address
audius_solana_rewards_manager_account
audius_solana_fee_payer_wallets
audius_delegate_private_key
audius_ipdata_api_key
audius_solana_eth_registry_program
audius_solana_listens_valid_signer
audius_solana_signer_private_key
audius_identity_relayer_public_key
audius_solana_listens_ip_hourly_rate_limit
audius_solana_listens_ip_daily_rate_limit
audius_solana_listens_ip_weekly_rate_limit
audius_solana_listens_track_hourly_rate_limit
audius_solana_listens_track_daily_rate_limit
audius_solana_listens_track_weekly_rate_limit
audius_anti_abuse_oracle
```

Then

```
npm run dev
```

If using with the web app, updat the `SOLANA_RELAY_ENDPOINT` value in `packages/web/src/services/env/env.<env>.ts` to point to the localhost port that the relay server is running on (i.e. localhost:6002)
Loading