Skip to content

Commit

Permalink
feat: implement jupiter as a swapper (#8120)
Browse files Browse the repository at this point in the history
  • Loading branch information
NeOMakinG authored Dec 2, 2024
1 parent e09ebd6 commit 4ab323c
Show file tree
Hide file tree
Showing 31 changed files with 989 additions and 52 deletions.
5 changes: 3 additions & 2 deletions .env.base
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,14 @@ REACT_APP_FEATURE_READ_ONLY_ASSETS=true
REACT_APP_FEATURE_SWAPPER_SOLANA=false

# Swapper feature flags - other .env files will override these
REACT_APP_FEATURE_CHAINFLIP=false
REACT_APP_FEATURE_CHAINFLIP_DCA=false
REACT_APP_FEATURE_CHAINFLIP_SWAP=false
REACT_APP_FEATURE_CHAINFLIP_SWAP_DCA=false
REACT_APP_FEATURE_COWSWAP=true
REACT_APP_FEATURE_LIFI_SWAP=true
REACT_APP_FEATURE_THOR_SWAP=true
REACT_APP_FEATURE_THOR_SWAP_STREAMING_SWAPS=true
REACT_APP_FEATURE_ZRX_SWAP=true
REACT_APP_FEATURE_JUPITER_SWAP=false

# chat woot
REACT_APP_CHATWOOT_TOKEN=jmoXp9BPMSPEYHeJX5YKT15Q
Expand Down
5 changes: 3 additions & 2 deletions .env.dev
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
REACT_APP_FEATURE_SWAPPER_SOLANA=true

# Swapper feature flags
REACT_APP_FEATURE_CHAINFLIP=true
REACT_APP_FEATURE_CHAINFLIP_DCA=false
REACT_APP_FEATURE_CHAINFLIP_SWAP=true
REACT_APP_FEATURE_CHAINFLIP_SWAP_DCA=false
REACT_APP_FEATURE_PUBLIC_TRADE_ROUTE=true
REACT_APP_FEATURE_LIMIT_ORDERS=true
REACT_APP_FEATURE_JUPITER_SWAP=true

# logging
REACT_APP_REDUX_WINDOW=false
Expand Down
4 changes: 2 additions & 2 deletions .env.develop
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ REACT_APP_FEATURE_SWAPPER_SOLANA=true

# Swapper feature flags
REACT_APP_FEATURE_LIMIT_ORDERS=true
REACT_APP_FEATURE_CHAINFLIP=true
REACT_APP_FEATURE_CHAINFLIP_DCA=false
REACT_APP_FEATURE_CHAINFLIP_SWAP=true
REACT_APP_FEATURE_CHAINFLIP_SWAP_DCA=false

# mixpanel
REACT_APP_MIXPANEL_TOKEN=1c1369f6ea23a6404bac41b42817cc4b
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
"@formatjs/intl-numberformat": "^8.10.3",
"@formatjs/intl-pluralrules": "^5.2.14",
"@json-rpc-tools/utils": "^1.7.6",
"@jup-ag/api": "^6.0.30",
"@keepkey/hdwallet-keepkey-rest": "1.40.42",
"@keepkey/keepkey-sdk": "0.2.57",
"@ledgerhq/hw-transport-webusb": "^6.29.2",
Expand Down
2 changes: 2 additions & 0 deletions packages/caip/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export const arbitrumAssetId: AssetId = 'eip155:42161/slip44:60'
export const arbitrumNovaAssetId: AssetId = 'eip155:42170/slip44:60'
export const baseAssetId: AssetId = 'eip155:8453/slip44:60'
export const solAssetId: AssetId = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501'
export const wrappedSolAssetId: AssetId =
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:So11111111111111111111111111111111111111112'

export const foxatarAssetId: AssetId =
'eip155:137/erc721:0x2e727c425a11ce6b8819b3004db332c12d2af2a2'
Expand Down
157 changes: 134 additions & 23 deletions packages/chain-adapters/src/solana/SolanaChainAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from '@shapeshiftoss/caip'
import type {
HDWallet,
SolanaAddressLookupTableAccountInfo,
SolanaSignTx,
SolanaTxInstruction,
SolanaWallet,
Expand All @@ -22,18 +23,22 @@ import {
createTransferInstruction,
getAccount,
getAssociatedTokenAddressSync,
TOKEN_2022_PROGRAM_ID,
TOKEN_PROGRAM_ID,
TokenAccountNotFoundError,
TokenInvalidAccountOwnerError,
} from '@solana/spl-token'
import type { TransactionInstruction } from '@solana/web3.js'
import type { AccountInfo, TransactionInstruction } from '@solana/web3.js'
import {
AddressLookupTableAccount,
ComputeBudgetProgram,
Connection,
PublicKey,
SystemProgram,
TransactionMessage,
VersionedTransaction,
} from '@solana/web3.js'
import { isUndefined } from 'lodash'
import PQueue from 'p-queue'

import type { ChainAdapter as IChainAdapter } from '../api'
Expand Down Expand Up @@ -62,6 +67,8 @@ import { toAddressNList, toRootDerivationPath } from '../utils'
import { assertAddressNotSanctioned } from '../utils/validateAddress'
import { microLamportsToLamports } from './utils'

export const svmChainIds = [KnownChainIds.SolanaMainnet] as const

// Maximum compute units allowed for a single solana transaction
const MAX_COMPUTE_UNITS = 1400000

Expand Down Expand Up @@ -251,6 +258,10 @@ export class ChainAdapter implements IChainAdapter<KnownChainIds.SolanaMainnet>
)
}

const addressLookupTableAccountInfos = await this.getAddressLookupTableAccounts(
chainSpecific.addressLookupTableAccounts ?? [],
)

const txToSign: SignTx<KnownChainIds.SolanaMainnet> = {
addressNList: toAddressNList(this.getBIP44Params({ accountNumber })),
blockHash: blockhash,
Expand All @@ -259,6 +270,7 @@ export class ChainAdapter implements IChainAdapter<KnownChainIds.SolanaMainnet>
instructions,
to: tokenId ? '' : to,
value: tokenId ? '' : value,
addressLookupTableAccountInfos,
}

return { txToSign }
Expand Down Expand Up @@ -449,8 +461,14 @@ export class ChainAdapter implements IChainAdapter<KnownChainIds.SolanaMainnet>
const { to, chainSpecific } = input
const { from, tokenId, instructions = [] } = chainSpecific

if (!to) throw new Error('to is required')
if (!input.value) throw new Error('value is required')
const estimationInstructions = [...instructions]

const addressLookupTableAccounts = await this.getSolanaAddressLookupTableAccountsInfo(
chainSpecific.addressLookupTableAccounts ?? [],
)

if (isUndefined(input.to)) throw new Error(`${this.getName()}ChainAdapter: to is required`)
if (!input.value) throw new Error(`${this.getName()}ChainAdapter: value is required`)

const value = Number(input.value)

Expand All @@ -463,9 +481,9 @@ export class ChainAdapter implements IChainAdapter<KnownChainIds.SolanaMainnet>
value: input.value,
})

instructions.push(...tokenTransferInstructions)
estimationInstructions.push(...tokenTransferInstructions)
} else {
instructions.push(
estimationInstructions.push(
SystemProgram.transfer({
fromPubkey: new PublicKey(from),
toPubkey: new PublicKey(to),
Expand All @@ -477,17 +495,19 @@ export class ChainAdapter implements IChainAdapter<KnownChainIds.SolanaMainnet>

// Set compute unit limit to the maximum compute units for the purposes of estimating the compute unit cost of a transaction,
// ensuring the transaction does not exceed the maximum compute units alotted for a single transaction.
instructions.push(ComputeBudgetProgram.setComputeUnitLimit({ units: MAX_COMPUTE_UNITS }))
estimationInstructions.push(
ComputeBudgetProgram.setComputeUnitLimit({ units: MAX_COMPUTE_UNITS }),
)

// placeholder compute unit price instruction for the purposes of estimating the compute unit cost of a transaction
instructions.push(ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 0 }))
estimationInstructions.push(ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 0 }))

const message = new TransactionMessage({
payerKey: new PublicKey(input.chainSpecific.from),
instructions,
instructions: estimationInstructions,
// static block hash as fee estimation replaces the block hash with latest to save us a client side call
recentBlockhash: '4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZAMdL4VZHirAn',
}).compileToV0Message()
}).compileToV0Message(addressLookupTableAccounts)

const transaction = new VersionedTransaction(message)

Expand All @@ -507,10 +527,49 @@ export class ChainAdapter implements IChainAdapter<KnownChainIds.SolanaMainnet>
}): Promise<TransactionInstruction[]> {
const instructions: TransactionInstruction[] = []

const { instruction, destinationTokenAccount } =
await this.createAssociatedTokenAccountInstruction({ from, to, tokenId })

if (instruction) {
instructions.push(instruction)
}

instructions.push(
createTransferInstruction(
getAssociatedTokenAddressSync(new PublicKey(tokenId), new PublicKey(from), true),
destinationTokenAccount,
new PublicKey(from),
Number(value),
),
)

return instructions
}

public async createAssociatedTokenAccountInstruction({
from,
to,
tokenId,
}: {
from: string
to: string
tokenId: string
}): Promise<{
instruction?: TransactionInstruction
destinationTokenAccount: PublicKey
}> {
const accountInfo = await this.connection.getAccountInfo(new PublicKey(tokenId))

const TOKEN_PROGRAM =
accountInfo?.owner.toString() === TOKEN_2022_PROGRAM_ID.toString()
? TOKEN_2022_PROGRAM_ID
: TOKEN_PROGRAM_ID

const destinationTokenAccount = getAssociatedTokenAddressSync(
new PublicKey(tokenId),
new PublicKey(to),
true,
TOKEN_PROGRAM,
)

// check if destination token account exists and add creation instruction if it doesn't
Expand All @@ -521,31 +580,27 @@ export class ChainAdapter implements IChainAdapter<KnownChainIds.SolanaMainnet>
err instanceof TokenAccountNotFoundError ||
err instanceof TokenInvalidAccountOwnerError
) {
instructions.push(
createAssociatedTokenAccountInstruction(
return {
instruction: createAssociatedTokenAccountInstruction(
// sender pays for creation of the token account
new PublicKey(from),
destinationTokenAccount,
new PublicKey(to),
new PublicKey(tokenId),
TOKEN_PROGRAM,
),
)
destinationTokenAccount,
}
}
}

instructions.push(
createTransferInstruction(
getAssociatedTokenAddressSync(new PublicKey(tokenId), new PublicKey(from), true),
destinationTokenAccount,
new PublicKey(from),
Number(value),
),
)

return instructions
return {
instruction: undefined,
destinationTokenAccount,
}
}

private convertInstruction(instruction: TransactionInstruction): SolanaTxInstruction {
public convertInstruction(instruction: TransactionInstruction): SolanaTxInstruction {
return {
keys: instruction.keys.map(key => ({
pubkey: key.pubkey.toString(),
Expand All @@ -557,6 +612,12 @@ export class ChainAdapter implements IChainAdapter<KnownChainIds.SolanaMainnet>
}
}

public async getTxStatus(tx: unchained.solana.Tx, pubkey: string): Promise<unchained.TxStatus> {
const parsedTx = await this.parseTx(tx, pubkey)

return parsedTx.status
}

private async parseTx(tx: unchained.solana.Tx, pubkey: string): Promise<Transaction> {
const { address: _, ...parsedTx } = await this.parser.parse(tx, pubkey)

Expand All @@ -572,4 +633,54 @@ export class ChainAdapter implements IChainAdapter<KnownChainIds.SolanaMainnet>
})),
}
}

get httpProvider(): unchained.solana.Api {
return this.providers.http
}

private async getAddressLookupTableAccountsInfo(
addresses: string[],
): Promise<(AccountInfo<Buffer> | null)[]> {
return await this.connection.getMultipleAccountsInfo(addresses.map(key => new PublicKey(key)))
}

private async getSolanaAddressLookupTableAccountsInfo(
addresses: string[],
): Promise<AddressLookupTableAccount[]> {
const addressLookupTableAccountInfos = await this.getAddressLookupTableAccountsInfo(addresses)

return addressLookupTableAccountInfos.reduce((acc, accountInfo, index) => {
const addressLookupTableAddress = addresses[index]
if (accountInfo) {
const addressLookupTableAccount = new AddressLookupTableAccount({
key: new PublicKey(addressLookupTableAddress),
state: AddressLookupTableAccount.deserialize(
new Uint8Array(Buffer.from(accountInfo.data)),
),
})
acc.push(addressLookupTableAccount)
}

return acc
}, new Array<AddressLookupTableAccount>())
}

private async getAddressLookupTableAccounts(
addresses: string[],
): Promise<SolanaAddressLookupTableAccountInfo[]> {
const addressLookupTableAccountInfos = await this.getAddressLookupTableAccountsInfo(addresses)

return addressLookupTableAccountInfos.reduce((acc, accountInfo, index) => {
const addressLookupTableAddress = addresses[index]
if (accountInfo) {
const addressLookupTableAccount = {
key: addressLookupTableAddress,
data: Buffer.from(accountInfo.data),
}
acc.push(addressLookupTableAccount)
}

return acc
}, new Array<SolanaAddressLookupTableAccountInfo>())
}
}
2 changes: 2 additions & 0 deletions packages/chain-adapters/src/solana/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@ export type BuildTxInput = {
computeUnitPrice?: string
tokenId?: string
instructions?: SolanaTxInstruction[]
addressLookupTableAccounts?: string[]
}

export type GetFeeDataInput = {
from: string
tokenId?: string
instructions?: TransactionInstruction[]
addressLookupTableAccounts?: string[]
}

export type FeeData = {
Expand Down
12 changes: 12 additions & 0 deletions packages/swapper/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import { chainflipApi } from './swappers/ChainflipSwapper/endpoints'
import { cowSwapper } from './swappers/CowSwapper/CowSwapper'
import { cowApi } from './swappers/CowSwapper/endpoints'
import { COW_SWAP_SUPPORTED_CHAIN_IDS } from './swappers/CowSwapper/utils/constants'
import { jupiterApi } from './swappers/JupiterSwapper/endpoints'
import { jupiterSwapper } from './swappers/JupiterSwapper/JupiterSwapper'
import { JUPITER_SUPPORTED_CHAIN_IDS } from './swappers/JupiterSwapper/utils/constants'
import { lifiApi } from './swappers/LifiSwapper/endpoints'
import {
LIFI_GET_TRADE_QUOTE_POLLING_INTERVAL,
Expand Down Expand Up @@ -85,6 +88,12 @@ export const swappers: Record<
supportedChainIds: CHAINFLIP_SUPPORTED_CHAIN_IDS,
pollingInterval: DEFAULT_GET_TRADE_QUOTE_POLLING_INTERVAL,
},
[SwapperName.Jupiter]: {
...jupiterSwapper,
...jupiterApi,
supportedChainIds: JUPITER_SUPPORTED_CHAIN_IDS,
pollingInterval: DEFAULT_GET_TRADE_QUOTE_POLLING_INTERVAL,
},
[SwapperName.Test]: undefined,
}

Expand All @@ -96,6 +105,7 @@ const DEFAULT_LIFI_SLIPPAGE_DECIMAL_PERCENTAGE = '0.005' // .5%
const DEFAULT_THOR_SLIPPAGE_DECIMAL_PERCENTAGE = '0.01' // 1%
const DEFAULT_ARBITRUM_BRIDGE_SLIPPAGE_DECIMAL_PERCENTAGE = '0' // no slippage for Arbitrum Bridge, so no slippage tolerance
const DEFAULT_CHAINFLIP_SLIPPAGE_DECIMAL_PERCENTAGE = '0.02' // 2%
const DEFAULT_JUPITER_SLIPPAGE_DECIMAL_PERCENTAGE = '0.01' // 1%

export const getDefaultSlippageDecimalPercentageForSwapper = (
swapperName?: SwapperName,
Expand All @@ -117,6 +127,8 @@ export const getDefaultSlippageDecimalPercentageForSwapper = (
return DEFAULT_ARBITRUM_BRIDGE_SLIPPAGE_DECIMAL_PERCENTAGE
case SwapperName.Chainflip:
return DEFAULT_CHAINFLIP_SLIPPAGE_DECIMAL_PERCENTAGE
case SwapperName.Jupiter:
return DEFAULT_JUPITER_SLIPPAGE_DECIMAL_PERCENTAGE
default:
assertUnreachable(swapperName)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ export const _getTradeQuote = async (
const isStreaming = singleQuoteResponse.type === CHAINFLIP_DCA_QUOTE
const feeData = await getFeeData()

if (isStreaming && !deps.config.REACT_APP_FEATURE_CHAINFLIP_DCA) {
if (isStreaming && !deps.config.REACT_APP_FEATURE_CHAINFLIP_SWAP_DCA) {
// DCA currently disabled - Streaming swap logic is very much tied to THOR currently and will deserve its own PR to generalize
// Even if we manage to get DCA swaps to execute, we wouldn't manage to properly poll with current web THOR-centric arch
continue
Expand Down
Loading

0 comments on commit 4ab323c

Please sign in to comment.