diff --git a/packages/common/src/hooks/purchaseContent/usePurchaseContentErrorMessage.ts b/packages/common/src/hooks/purchaseContent/usePurchaseContentErrorMessage.ts index d037e28a82d..2dc40ecf2b6 100644 --- a/packages/common/src/hooks/purchaseContent/usePurchaseContentErrorMessage.ts +++ b/packages/common/src/hooks/purchaseContent/usePurchaseContentErrorMessage.ts @@ -1,4 +1,3 @@ -import { BuyCryptoErrorCode } from '~/store/buy-crypto/types' import { BuyUSDCErrorCode } from '~/store/index' import { PurchaseContentErrorCode, @@ -35,25 +34,10 @@ export const usePurchaseContentErrorMessage = ( return messages.minimumPurchase(minUSDCPurchaseAmountCents) case BuyUSDCErrorCode.MaxAmountExceeded: return messages.maximumPurchase(maxUSDCPurchaseAmountCents) - case BuyCryptoErrorCode.COUNTRY_NOT_SUPPORTED: - case BuyUSDCErrorCode.CountryNotSupported: - return messages.countryNotSupported - case BuyCryptoErrorCode.BAD_AMOUNT: - return messages.badAmount( - minUSDCPurchaseAmountCents, - maxUSDCPurchaseAmountCents - ) case PurchaseErrorCode.InsufficientExternalTokenBalance: return messages.insufficientExternalTokenBalance case PurchaseErrorCode.NoQuote: return messages.noQuote - case BuyCryptoErrorCode.BAD_FEE_PAYER: - case BuyCryptoErrorCode.BAD_PROVIDER: - case BuyCryptoErrorCode.BAD_TOKEN: - case BuyCryptoErrorCode.ON_RAMP_ERROR: - case BuyCryptoErrorCode.SWAP_ERROR: - case BuyCryptoErrorCode.UNKNOWN: - case BuyCryptoErrorCode.INSUFFICIENT_FUNDS_ERROR: case BuyUSDCErrorCode.OnrampError: case PurchaseErrorCode.Canceled: case PurchaseErrorCode.InsufficientBalance: diff --git a/packages/common/src/hooks/useCoinflowAdapter.ts b/packages/common/src/hooks/useCoinflowAdapter.ts index 9083ed023f9..b5ca926c950 100644 --- a/packages/common/src/hooks/useCoinflowAdapter.ts +++ b/packages/common/src/hooks/useCoinflowAdapter.ts @@ -22,7 +22,6 @@ import { purchaseContentActions } from '~/store' import { getWalletAddresses } from '~/store/account/selectors' -import { BuyCryptoError } from '~/store/buy-crypto/types' import { getFeePayer } from '~/store/solana/selectors' type CoinflowAdapter = { @@ -203,9 +202,7 @@ export const useCoinflowAdapter = ({ } catch (e) { console.error('Caught error in sendTransaction', e) const error = - e instanceof PurchaseContentError || - e instanceof BuyUSDCError || - e instanceof BuyCryptoError + e instanceof PurchaseContentError || e instanceof BuyUSDCError ? e : new PurchaseContentError(PurchaseErrorCode.Unknown, `${e}`) dispatch( diff --git a/packages/common/src/models/Analytics.ts b/packages/common/src/models/Analytics.ts index 398803a99d1..96a3b65837f 100644 --- a/packages/common/src/models/Analytics.ts +++ b/packages/common/src/models/Analytics.ts @@ -9,8 +9,7 @@ import { StringAudio, WalletAddress } from '~/models/Wallet' -import { MintName } from '~/services/index' -import { Nullable, Prettify } from '~/utils/typeUtils' +import { Nullable } from '~/utils/typeUtils' import { Chain } from './Chain' import { PlaylistLibraryKind } from './PlaylistLibrary' @@ -415,21 +414,6 @@ export enum Name { BUY_USDC_RECOVERY_FAILURE = 'Buy USDC: Recovery Failure', BUY_USDC_ADD_FUNDS_MANUALLY = 'Buy USDC: Add Funds Manually', - // Buy Crypto - BUY_CRYPTO_STARTED = 'Buy Crypto: Started', - BUY_CRYPTO_ON_RAMP_OPENED = 'Buy Crypto: On Ramp Opened', - BUY_CRYPTO_ON_RAMP_SUCCESS = 'Buy Crypto: On Ramp Success', - BUY_CRYPTO_ON_RAMP_FAILURE = 'Buy Crypto: On Ramp Failure', - BUY_CRYPTO_ON_RAMP_CANCELED = 'Buy Crypto: On Ramp Canceled', - BUY_CRYPTO_ON_RAMP_CONFIRMED = 'Buy Crypto: On Ramp Confirmed', - BUY_CRYPTO_SUCCESS = 'Buy Crypto: Success', - BUY_CRYPTO_FAILURE = 'Buy Crypto: Failure', - - // Buy Crypto Recovery - BUY_CRYPTO_RECOVERY_STARTED = 'Buy Crypto: Recovery Started', - BUY_CRYPTO_RECOVERY_SUCCESS = 'Buy Crypto: Recovery Success', - BUY_CRYPTO_RECOVERY_FAILURE = 'Buy Crypto: Recovery Failure', - // Withdraw USDC WITHDRAW_USDC_MODAL_OPENED = 'Withdraw USDC: Modal Opened', WITHDRAW_USDC_ADDRESS_PASTED = 'Withdraw USDC: Address Pasted', @@ -2083,35 +2067,6 @@ type BuyUSDCAddFundsManually = { eventName: Name.BUY_USDC_ADD_FUNDS_MANUALLY } -// Buy Crypto - -type BuyCryptoEvent = { - eventName: - | Name.BUY_CRYPTO_STARTED - | Name.BUY_CRYPTO_ON_RAMP_OPENED - | Name.BUY_CRYPTO_ON_RAMP_SUCCESS - | Name.BUY_CRYPTO_ON_RAMP_FAILURE - | Name.BUY_CRYPTO_ON_RAMP_CANCELED - | Name.BUY_CRYPTO_ON_RAMP_CONFIRMED - | Name.BUY_CRYPTO_SUCCESS - | Name.BUY_CRYPTO_FAILURE - - provider: string - requestedAmount: number - mint: MintName - error?: string -} - -type BuyCryptoRecoveryEvent = Prettify< - { - eventName: - | Name.BUY_CRYPTO_RECOVERY_STARTED - | Name.BUY_CRYPTO_RECOVERY_FAILURE - | Name.BUY_CRYPTO_RECOVERY_SUCCESS - intendedSOL: number - } & Omit -> - // Withdraw USDC export type WithdrawUSDCEventFields = { @@ -2972,8 +2927,6 @@ export type AllTrackingEvents = | BuyUSDCRecoverySuccess | BuyUSDCRecoveryFailure | BuyUSDCAddFundsManually - | BuyCryptoEvent - | BuyCryptoRecoveryEvent | WithdrawUSDCModalOpened | WithdrawUSDCAddressPasted | WithdrawUSDCFormError diff --git a/packages/common/src/services/audius-backend/solana.ts b/packages/common/src/services/audius-backend/solana.ts index f1c684d9bc2..d1fa17519da 100644 --- a/packages/common/src/services/audius-backend/solana.ts +++ b/packages/common/src/services/audius-backend/solana.ts @@ -11,16 +11,13 @@ import { getAssociatedTokenAddressSync } from '@solana/spl-token' import { - AddressLookupTableAccount, Commitment, ComputeBudgetProgram, Connection, Keypair, PublicKey, Transaction, - TransactionInstruction, - TransactionMessage, - VersionedTransaction + TransactionInstruction } from '@solana/web3.js' import BN from 'bn.js' @@ -37,7 +34,6 @@ import { AudiusBackend } from './AudiusBackend' const DEFAULT_RETRY_DELAY = 1000 const DEFAULT_MAX_RETRY_COUNT = 120 -const PLACEHOLDER_SIGNATURE = new Array(64).fill(0) export const RECOVERY_MEMO_STRING = 'Recover Withdrawal' export const WITHDRAWAL_MEMO_STRING = 'Withdrawal' export const PREPARE_WITHDRAWAL_MEMO_STRING = 'Prepare Withdrawal' @@ -293,58 +289,6 @@ export const pollForTokenBalanceChange = async ( throw new Error(`${debugTokenName} balance polling exceeded maximum retries`) } -/** - * Polls the given wallet until its SOL balance is different from initial balance or a timeoout. - * @throws an error if the balance doesn't change within the timeout. - */ -export const pollForBalanceChange = async ( - audiusBackendInstance: AudiusBackend, - { - wallet, - initialBalance, - retryDelayMs = DEFAULT_RETRY_DELAY, - maxRetryCount = DEFAULT_MAX_RETRY_COUNT, - sdk - }: { - wallet: PublicKey - initialBalance?: bigint - retryDelayMs?: number - maxRetryCount?: number - sdk: AudiusSdk - } -) => { - console.info(`Polling SOL balance for ${wallet.toBase58()} ...`) - let balanceBN = await audiusBackendInstance.getAddressSolBalance({ - address: wallet.toBase58(), - sdk - }) - let balance = BigInt(balanceBN.toString()) - if (initialBalance === undefined) { - initialBalance = balance - } - let retries = 0 - while (balance === initialBalance && retries++ < maxRetryCount) { - console.debug( - `Polling SOL balance (${initialBalance} === ${balance}) [${retries}/${maxRetryCount}]` - ) - await delay(retryDelayMs) - balanceBN = await audiusBackendInstance.getAddressSolBalance({ - address: wallet.toBase58(), - sdk - }) - balance = BigInt(balanceBN.toString()) - } - if (balance !== initialBalance) { - console.debug( - `SOL balance changed by ${ - balance - initialBalance - } (${initialBalance} => ${balance})` - ) - return balance - } - throw new Error('SOL balance polling exceeded maximum retries') -} - export const findAssociatedTokenAddress = async ( audiusBackendInstance: AudiusBackend, { solanaAddress, mint }: { solanaAddress: string; mint: MintName } @@ -595,102 +539,6 @@ export const relayTransaction = async ( }) } -/** - * Relays the given versioned transaction using the libs transaction handler - */ -export const relayVersionedTransaction = async ( - audiusBackendInstance: AudiusBackend, - { - transaction, - addressLookupTableAccounts, - skipPreflight - }: { - transaction: VersionedTransaction - addressLookupTableAccounts: AddressLookupTableAccount[] - skipPreflight?: boolean - } -) => { - const placeholderSignature = Buffer.from(PLACEHOLDER_SIGNATURE) - const libs = await audiusBackendInstance.getAudiusLibsTyped() - const decompiledMessage = TransactionMessage.decompile(transaction.message, { - addressLookupTableAccounts - }) - const signatures = transaction.message.staticAccountKeys - .slice(0, transaction.message.header.numRequiredSignatures) - .map((publicKey, index) => ({ - publicKey: publicKey.toBase58(), - signature: Buffer.from(transaction.signatures[index]) - })) - .filter((meta) => !meta.signature.equals(placeholderSignature)) - return await libs.solanaWeb3Manager!.transactionHandler.handleTransaction({ - instructions: decompiledMessage.instructions, - recentBlockhash: decompiledMessage.recentBlockhash, - signatures, - feePayerOverride: decompiledMessage.payerKey, - lookupTableAddresses: addressLookupTableAccounts.map((lut) => - lut.key.toBase58() - ), - skipPreflight - }) -} - -/** - * Helper that gets the lookup table accounts (that is, the account holding the lookup table, - * not the accounts _in_ the lookup table) from their addresses. - */ -export const getLookupTableAccounts = async ( - audiusBackendInstance: AudiusBackend, - { lookupTableAddresses }: { lookupTableAddresses: string[] } -) => { - const libs = await audiusBackendInstance.getAudiusLibsTyped() - const connection = libs.solanaWeb3Manager!.getConnection() - return await Promise.all( - lookupTableAddresses.map(async (address) => { - const account = await connection.getAddressLookupTable( - new PublicKey(address) - ) - if (account.value == null) { - throw new Error(`Couldn't find lookup table ${address}`) - } - return account.value - }) - ) -} - -/** - * Helper to create a versioned transaction with lookup tables - */ -export const createVersionedTransaction = async ( - audiusBackendInstance: AudiusBackend, - { - instructions, - lookupTableAddresses, - feePayer, - sdk - }: { - instructions: TransactionInstruction[] - lookupTableAddresses: string[] - feePayer: PublicKey - sdk: AudiusSdk - } -) => { - const addressLookupTableAccounts = await getLookupTableAccounts( - audiusBackendInstance, - { lookupTableAddresses } - ) - const recentBlockhash = await getRecentBlockhash({ sdk }) - - const message = new TransactionMessage({ - payerKey: feePayer, - recentBlockhash, - instructions - }).compileToV0Message(addressLookupTableAccounts) - return { - transaction: new VersionedTransaction(message), - addressLookupTableAccounts - } -} - // NOTE: The above all need to be updated to use SDK. The below is fresh. /** diff --git a/packages/common/src/store/buy-crypto/index.ts b/packages/common/src/store/buy-crypto/index.ts deleted file mode 100644 index 17b38c896c9..00000000000 --- a/packages/common/src/store/buy-crypto/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { - default as buyCryptoReducer, - actions as buyCryptoActions -} from './slice' -export { default as buyCryptoSagas } from './sagas' diff --git a/packages/common/src/store/buy-crypto/sagas.ts b/packages/common/src/store/buy-crypto/sagas.ts deleted file mode 100644 index 5348fa8470d..00000000000 --- a/packages/common/src/store/buy-crypto/sagas.ts +++ /dev/null @@ -1,929 +0,0 @@ -import { - NATIVE_MINT, - createAssociatedTokenAccountIdempotentInstruction, - createCloseAccountInstruction, - createSyncNativeInstruction, - getAssociatedTokenAddressSync -} from '@solana/spl-token' -import { - Keypair, - LAMPORTS_PER_SOL, - PublicKey, - SystemProgram, - TransactionInstruction -} from '@solana/web3.js' -import { - call, - delay, - put, - race, - select, - take, - takeLatest -} from 'typed-redux-saga' - -import { Name } from '~/models/Analytics' -import { ErrorLevel } from '~/models/ErrorReporting' -import { - SLIPPAGE_TOLERANCE_EXCEEDED_ERROR, - jupiterInstance, - parseJupiterInstruction, - quoteWithAnalytics -} from '~/services/Jupiter' -import { - MEMO_PROGRAM_ID, - MintName, - createUserBankIfNeeded, - createVersionedTransaction, - getTokenAccountInfo, - pollForBalanceChange, - pollForTokenBalanceChange, - relayVersionedTransaction -} from '~/services/audius-backend/solana' -import { FeatureFlags } from '~/services/index' -import { IntKeys } from '~/services/remote-config/types' -import { - onrampCanceled, - onrampFailed, - onrampSucceeded, - buyCryptoViaSol, - buyCryptoCanceled, - buyCryptoFailed, - buyCryptoSucceeded, - buyCryptoRecoverySucceeded, - buyCryptoRecoveryFailed -} from '~/store/buy-crypto/slice' -import { getBuyUSDCRemoteConfig } from '~/store/buy-usdc' -import { getContext } from '~/store/commonStore' -import { getFeePayer } from '~/store/solana/selectors' -import { TOKEN_LISTING_MAP } from '~/store/ui/buy-audio/constants' -import { OnRampProvider } from '~/store/ui/buy-audio/types' -import { setVisibility } from '~/store/ui/modals/parentSlice' -import { initializeStripeModal } from '~/store/ui/stripe-modal/slice' -import { waitForAccount, waitForValue } from '~/utils/sagaHelpers' - -import { getWalletAddresses } from '../account/selectors' -import { getSDK } from '../sdkUtils' - -import { - BuyCryptoConfig, - BuyCryptoError, - BuyCryptoErrorCode, - BuyCryptoViaSolLocalStorageState -} from './types' - -const BUY_CRYPTO_VIA_SOL_STATE_KEY = 'buy_crypto_via_sol' -const LOCAL_STORAGE_STATE_TTL_MS = 1000 * 60 * 60 * 2 // 2 hours (arbitrary) - -function* getBuyAudioRemoteConfig() { - // Default slippage tolerance, in percentage basis points - const DEFAULT_SLIPPAGE_BPS = 30 - const DEFAULT_MIN_AUDIO_PURCHASE_AMOUNT = 5 - const DEFAULT_MAX_AUDIO_PURCHASE_AMOUNT = 999 - const remoteConfigInstance = yield* getContext('remoteConfigInstance') - yield* call([remoteConfigInstance, remoteConfigInstance.waitForRemoteConfig]) - const minAudioAmount = - remoteConfigInstance.getRemoteVar(IntKeys.MIN_AUDIO_PURCHASE_AMOUNT) ?? - DEFAULT_MIN_AUDIO_PURCHASE_AMOUNT - const maxAudioAmount = - remoteConfigInstance.getRemoteVar(IntKeys.MAX_AUDIO_PURCHASE_AMOUNT) ?? - DEFAULT_MAX_AUDIO_PURCHASE_AMOUNT - const slippage = - remoteConfigInstance.getRemoteVar(IntKeys.BUY_AUDIO_SLIPPAGE) ?? - DEFAULT_SLIPPAGE_BPS - const retryDelayMs = - remoteConfigInstance.getRemoteVar(IntKeys.BUY_TOKEN_WALLET_POLL_DELAY_MS) ?? - undefined - const maxRetryCount = - remoteConfigInstance.getRemoteVar( - IntKeys.BUY_TOKEN_WALLET_POLL_MAX_RETRIES - ) ?? undefined - return { - minAudioAmount, - maxAudioAmount, - slippage, - maxRetryCount, - retryDelayMs - } -} - -function* getBuyCryptoRemoteConfig(mint: MintName) { - if (mint === 'USDC') { - const config = yield* call(getBuyUSDCRemoteConfig) - return { - maxAmount: config.maxUSDCPurchaseAmountCents / 100.0, - minAmount: config.minUSDCPurchaseAmountCents / 100.0, - slippageBps: config.slippage, - retryDelayMs: config.retryDelayMs, - maxRetryCount: config.maxRetryCount - } as BuyCryptoConfig - } else { - const config = yield* call(getBuyAudioRemoteConfig) - return { - maxAmount: config.maxAudioAmount, - minAmount: config.minAudioAmount, - slippageBps: config.slippage, - retryDelayMs: config.retryDelayMs, - maxRetryCount: config.maxRetryCount - } as BuyCryptoConfig - } -} - -function* swapSolForCrypto({ - wallet, - mint, - feePayer, - quoteResponse, - userbank -}: { - wallet: Keypair - mint: MintName - feePayer: PublicKey - userbank: PublicKey - quoteResponse: Awaited> -}) { - const audiusBackendInstance = yield* getContext('audiusBackendInstance') - // Create a memo - // See: https://github.com/solana-labs/solana-program-library/blob/d6297495ea4dcc1bd48f3efdd6e3bbdaef25a495/memo/js/src/index.ts#L27 - const memoInstruction = new TransactionInstruction({ - keys: [ - { - pubkey: wallet.publicKey, - isSigner: true, - isWritable: true - } - ], - programId: MEMO_PROGRAM_ID, - data: Buffer.from(`In-App $${mint.toUpperCase()} Purchase: Link by Stripe`) - }) - - // Create a temp wSOL account - const walletSolTokenAccount = getAssociatedTokenAddressSync( - NATIVE_MINT, - wallet.publicKey - ) - const createWSOLInstruction = - createAssociatedTokenAccountIdempotentInstruction( - feePayer, // fee payer - walletSolTokenAccount, // account to create - wallet.publicKey, // owner - NATIVE_MINT // mint - ) - - // Transfer the SOL to the wSOL account - const transferWSOLInstruction = SystemProgram.transfer({ - fromPubkey: wallet.publicKey, - toPubkey: walletSolTokenAccount, - lamports: - quoteResponse.swapMode === 'ExactIn' - ? BigInt(quoteResponse.inAmount) - : BigInt(quoteResponse.otherAmountThreshold) - }) - const syncNativeInstruction = createSyncNativeInstruction( - walletSolTokenAccount - ) - - // Swap the new SOL amount into the desired token amount - const { swapInstruction, addressLookupTableAddresses } = yield* call( - [jupiterInstance, jupiterInstance.swapInstructionsPost], - { - swapRequest: { - quoteResponse, - userPublicKey: wallet.publicKey.toBase58(), - destinationTokenAccount: userbank.toBase58(), - useSharedAccounts: true - } - } - ) - - // Close the temporary wSOL account - const closeWSOLInstruction = createCloseAccountInstruction( - walletSolTokenAccount, // account to close - feePayer, // fee destination - wallet.publicKey // owner - ) - - const instructions = [ - memoInstruction, - createWSOLInstruction, - transferWSOLInstruction, - syncNativeInstruction, - parseJupiterInstruction(swapInstruction), - closeWSOLInstruction - ] - const sdk = yield* getSDK() - const { transaction, addressLookupTableAccounts } = yield* call( - createVersionedTransaction, - audiusBackendInstance, - { - instructions, - lookupTableAddresses: addressLookupTableAddresses, - feePayer, - sdk - } - ) - transaction.sign([wallet]) - - return yield* call(relayVersionedTransaction, audiusBackendInstance, { - transaction, - addressLookupTableAccounts, - skipPreflight: true - }) -} - -function* assertRecoverySuccess({ - res, - swapError, - recoveryError, - mint -}: { - res: string | null - swapError: string | null - recoveryError: string | null - mint: MintName -}) { - if (recoveryError) { - throw new BuyCryptoError( - BuyCryptoErrorCode.SWAP_ERROR, - `Failed to recover ${mint.toUpperCase()} from SOL: ${recoveryError}. Initial Swap Error: ${swapError}` - ) - } else if (!res) { - throw new BuyCryptoError( - BuyCryptoErrorCode.UNKNOWN, - `Unknown error during recovery swap. Initial Swap Error: ${swapError}` - ) - } -} - -function* doBuyCryptoViaSol({ - payload: { amount, mint, provider } -}: ReturnType) { - // Pull from context - const audiusBackendInstance = yield* getContext('audiusBackendInstance') - const solanaWalletService = yield* getContext('solanaWalletService') - const { track, make } = yield* getContext('analytics') - const reportToSentry = yield* getContext('reportToSentry') - const audiusLocalStorage = yield* getContext('localStorage') - const sdk = yield* getSDK() - - yield* call( - track, - make({ - eventName: Name.BUY_CRYPTO_STARTED, - mint, - provider, - requestedAmount: amount - }) - ) - - // Get config - const wallet = yield* call([ - solanaWalletService, - solanaWalletService.getKeypair - ]) - const feePayerAddress = yield* select(getFeePayer) - const config = yield* call(getBuyCryptoRemoteConfig, mint) - const outputToken = TOKEN_LISTING_MAP[mint.toUpperCase()] - let userbank: PublicKey | null = null - try { - if (!wallet) { - throw new Error('Missing Solana root wallet') - } - // Validate inputs - // TODO: Re-add Coinbase support when rewriting BuyAudio - if (provider !== OnRampProvider.STRIPE) { - throw new BuyCryptoError( - BuyCryptoErrorCode.BAD_PROVIDER, - 'Only Link by Stripe is supported' - ) - } - - if (amount < config.minAmount) { - throw new BuyCryptoError( - BuyCryptoErrorCode.BAD_AMOUNT, - `Amount is below app-configured minimum of ${ - config.minAmount - } $${mint.toUpperCase()}` - ) - } - - if (amount > config.maxAmount) { - throw new BuyCryptoError( - BuyCryptoErrorCode.BAD_AMOUNT, - `Amount is above app-configured maximum of ${config.maxAmount}` - ) - } - - if (!outputToken) { - throw new BuyCryptoError( - BuyCryptoErrorCode.BAD_TOKEN, - `Unrecognized token: ${mint}` - ) - } - - if (!feePayerAddress) { - throw new BuyCryptoError( - BuyCryptoErrorCode.BAD_FEE_PAYER, - 'Fee Payer is missing' - ) - } - - // Set up computed vars - const outputTokenLamports = 10 ** outputToken.decimals - const feePayer = new PublicKey(feePayerAddress) - const { currentUser } = yield* select(getWalletAddresses) - if (!currentUser) { - throw new Error('Failed to get current user wallet address') - } - userbank = yield* call(createUserBankIfNeeded, sdk, { - mint, - recordAnalytics: track, - ethAddress: currentUser - }) - - // Get required SOL purchase amount via ExactOut + minRent. - const quoteResponse = yield* call(quoteWithAnalytics, { - quoteArgs: { - inputMint: TOKEN_LISTING_MAP.SOL.address, - outputMint: outputToken.address, - amount: Math.ceil(amount * outputTokenLamports), - swapMode: 'ExactOut', - slippageBps: config.slippageBps - }, - track, - make - }) - - const connection = sdk.services.solanaClient.connection - const minRent = yield* call( - [connection, connection.getMinimumBalanceForRentExemption], - 0 - ) - // otherAmountThreshold is the max input amount for the given slippage. - // Note: ignores any existing SOL as it should be recovered and swapped into USDC - const requiredAmount = Number(quoteResponse.otherAmountThreshold) + minRent - - // Get min stripe purchase amount using USDC as quote for $1 - const minQuote = yield* call(quoteWithAnalytics, { - quoteArgs: { - inputMint: TOKEN_LISTING_MAP.SOL.address, - outputMint: TOKEN_LISTING_MAP.USDC.address, - amount: 1 * 10 ** TOKEN_LISTING_MAP.USDC.decimals, - swapMode: 'ExactOut', - slippageBps: config.slippageBps - }, - track, - make - }) - - const minAmount = Number(minQuote.otherAmountThreshold) - if (requiredAmount < minAmount) { - console.warn( - `Stripe requires minimum purchase of $1 (${minAmount} lamports). Required lamports: ${requiredAmount}` - ) - } - const lamportsToPurchase = Math.max(requiredAmount, minAmount) - - // Open Stripe Modal - // TODO: Support coinbase similarly? - yield* put( - initializeStripeModal({ - amount: (lamportsToPurchase / LAMPORTS_PER_SOL).toString(), - destinationCurrency: 'sol', - destinationWallet: wallet.publicKey.toBase58(), - onrampCanceled: onrampCanceled(), - onrampFailed: onrampFailed({}), - onrampSucceeded: onrampSucceeded() - }) - ) - yield* put(setVisibility({ modal: 'StripeOnRamp', visible: true })) - - yield* call( - track, - make({ - eventName: Name.BUY_CRYPTO_ON_RAMP_OPENED, - mint, - provider, - requestedAmount: amount - }) - ) - - // Get initial balance - const existingBalance = yield* call( - [connection, connection.getBalance], - wallet.publicKey - ) - const initialBalance = BigInt(existingBalance) - - // Wait for on ramp finish - const result = yield* race({ - failure: take(onrampFailed), - success: take(onrampSucceeded), - canceled: take(onrampCanceled) - }) - - // If the user didn't complete the on ramp flow or it failed, return early - if (result.canceled) { - yield* call( - track, - make({ - eventName: Name.BUY_CRYPTO_ON_RAMP_CANCELED, - mint, - provider, - requestedAmount: amount - }) - ) - yield* put(buyCryptoCanceled()) - return - } else if (result.failure) { - const errorString = result.failure.payload?.error - ? result.failure.payload.error.message - : 'Unknown error' - - yield* call( - track, - make({ - eventName: Name.BUY_CRYPTO_ON_RAMP_FAILURE, - mint, - provider, - requestedAmount: amount, - error: errorString - }) - ) - if ( - result.failure.payload?.error?.code === - 'crypto_onramp_unsupported_country' - ) { - throw new BuyCryptoError( - BuyCryptoErrorCode.COUNTRY_NOT_SUPPORTED, - errorString - ) - } - throw new BuyCryptoError(BuyCryptoErrorCode.ON_RAMP_ERROR, errorString) - } - - // Update local storage - const localStorageState: BuyCryptoViaSolLocalStorageState = { - amount, - mint, - provider, - createdAt: Date.now(), - intendedLamports: Number(quoteResponse.otherAmountThreshold) - } - yield* call( - [audiusLocalStorage, audiusLocalStorage.setJSONValue], - BUY_CRYPTO_VIA_SOL_STATE_KEY, - localStorageState - ) - - // Record analytics - yield* call( - track, - make({ - eventName: Name.BUY_CRYPTO_ON_RAMP_SUCCESS, - mint, - provider, - requestedAmount: amount - }) - ) - - // Wait for the funds to come through - const newBalance = yield* call( - pollForBalanceChange, - audiusBackendInstance, - { - wallet: wallet.publicKey, - sdk, - initialBalance, - retryDelayMs: config.retryDelayMs, - maxRetryCount: config.maxRetryCount - } - ) - - // Check that we got the requested amount of SOL - const purchasedAmount = newBalance - initialBalance - if (purchasedAmount < BigInt(lamportsToPurchase)) { - console.warn( - `Warning: Purchased SOL amount differs from expected. Actual: ${ - newBalance - initialBalance - }. Expected: ${lamportsToPurchase}.` - ) - } - - // Record analytics - yield* call( - track, - make({ - eventName: Name.BUY_CRYPTO_ON_RAMP_CONFIRMED, - mint, - provider, - requestedAmount: amount - }) - ) - - // Save pre swap token balance - const account = yield* call(getTokenAccountInfo, sdk, { - tokenAccount: userbank - }) - const preSwapTokenBalance = account?.amount ?? BigInt(0) - - // Try the swap a few times in hopes the price comes back if it slipped - let swapError: string | null = null - let swapTransactionSignature: string | null = null - let retryCount = 0 - // TODO: Put these into optimizely? - const maxRetryCount = 3 - const retryDelayMs = 3000 - do { - const { res, error } = yield* call(swapSolForCrypto, { - feePayer, - mint, - wallet, - userbank, - quoteResponse - }) - swapError = error - swapTransactionSignature = res - if ( - swapError && - retryCount < maxRetryCount && - swapError.includes(`${SLIPPAGE_TOLERANCE_EXCEEDED_ERROR}`) - ) { - console.warn( - `Failed to swap: ${swapError}. Retrying ${ - retryCount + 1 - } of ${maxRetryCount}...` - ) - yield* delay(retryDelayMs) - } - } while (!!swapError && retryCount++ < maxRetryCount) - - // If the swap fails to get the desired ExactOut amount, fall back to - // swapping the amount of SOL the user bought into the target token. - if (swapError) { - console.error( - `Failed to swap for requested ${amount} ${mint.toUpperCase()}. Attempting to salvage all ${mint.toUpperCase()} possible...`, - swapError - ) - // Get "amount" in terms of wei - const expectedAmount = BigInt(quoteResponse.outAmount) - // Get the amount to swap using the new balance less the min required for rent - // Note: This disregards the existing SOL balance, but there shouldn't be any - const newBalance = yield* call( - [connection, connection.getBalance], - wallet.publicKey - ) - const salvageInputAmount = newBalance - minRent - console.info( - `Attempting to salvage ${salvageInputAmount} SOL into ${mint.toUpperCase()}` - ) - - // Get a quote for swapping the entire balance - const exactInQuote = yield* call(quoteWithAnalytics, { - quoteArgs: { - inputMint: TOKEN_LISTING_MAP.SOL.address, - outputMint: TOKEN_LISTING_MAP[mint.toUpperCase()].address, - amount: salvageInputAmount, - swapMode: 'ExactIn', - slippageBps: config.slippageBps - }, - track, - make - }) - - // Do the swap. Just do it once, slippage shouldn't be a - // concern since the quote is fresh and the tolerance is high. - const { res, error: recoveryError } = yield* call(swapSolForCrypto, { - quoteResponse: exactInQuote, - mint, - wallet, - userbank, - feePayer - }) - yield* call(assertRecoverySuccess, { - res, - swapError, - recoveryError, - mint - }) - const outputTokenChange = yield* call(pollForTokenBalanceChange, sdk, { - initialBalance: preSwapTokenBalance, - tokenAccount: userbank, - mint, - retryDelayMs: config.retryDelayMs, - maxRetryCount: config.maxRetryCount - }) - console.info( - `Salvaged ${ - exactInQuote.inAmount - } SOL into ${outputTokenChange} ${mint.toUpperCase()}: ${res}` - ) - if ( - outputTokenChange === undefined || - outputTokenChange < expectedAmount - ) { - // Clear local storage - the SOL is gone, but we just didn't get enough - // of the output token - yield* call( - [audiusLocalStorage, audiusLocalStorage.removeItem], - BUY_CRYPTO_VIA_SOL_STATE_KEY - ) - throw new BuyCryptoError( - BuyCryptoErrorCode.INSUFFICIENT_FUNDS_ERROR, - `Failed to swap SOL to ${expectedAmount} ${mint.toUpperCase()}. Initial Swap Error: ${swapError}.` - ) - } - } else if (!swapTransactionSignature) { - throw new BuyCryptoError( - BuyCryptoErrorCode.UNKNOWN, - 'Unknown error during initial swap' - ) - } - - // Record success - yield* call( - track, - make({ - eventName: Name.BUY_CRYPTO_ON_RAMP_SUCCESS, - mint, - provider, - requestedAmount: amount - }) - ) - yield* put(buyCryptoSucceeded()) - - // Clear local storage - yield* call( - [audiusLocalStorage, audiusLocalStorage.removeItem], - BUY_CRYPTO_VIA_SOL_STATE_KEY - ) - } catch (e) { - const error = - e instanceof BuyCryptoError - ? e - : new BuyCryptoError(BuyCryptoErrorCode.UNKNOWN, `${e}`) - yield* put(buyCryptoFailed({ error })) - yield* call(reportToSentry, { - level: ErrorLevel.Error, - error, - additionalInfo: { - wallet: wallet ? wallet.publicKey.toBase58() : '', - userbank: userbank?.toBase58() - } - }) - yield* call( - track, - make({ - eventName: Name.BUY_CRYPTO_ON_RAMP_FAILURE, - mint, - provider, - requestedAmount: amount, - error: error.message - }) - ) - } -} - -/** - * Failures happen when the on ramp purchase could not be confirmed, or the - * subsequent swap attempts all fail. Get the SOL balance of the wallet, and - * if it has a balance greater than min rent, attempt to swap all of it to the - * output token. If we get enough of the output token after the swap, it's a - * successful recovery. - */ -function* recoverBuyCryptoViaSolIfNecessary() { - yield* call(waitForAccount) - // Pull from context - const solanaWalletService = yield* getContext('solanaWalletService') - const getFeatureEnabled = yield* getContext('getFeatureEnabled') - const { track, make } = yield* getContext('analytics') - const reportToSentry = yield* getContext('reportToSentry') - - // Return early if feature not enabled - if (!getFeatureEnabled(FeatureFlags.BUY_USDC_VIA_SOL)) { - return - } - - // Check for local storage - const audiusLocalStorage = yield* getContext('localStorage') - const localStorageState = yield* call( - [ - audiusLocalStorage, - audiusLocalStorage.getJSONValue - ], - BUY_CRYPTO_VIA_SOL_STATE_KEY - ) - - // Do nothing if there's no local storage state - if (!localStorageState) { - return - } - - const { mint, amount, provider, createdAt, intendedLamports } = - localStorageState - yield* call( - track, - make({ - eventName: Name.BUY_CRYPTO_RECOVERY_STARTED, - mint, - provider, - requestedAmount: amount, - intendedSOL: intendedLamports / LAMPORTS_PER_SOL - }) - ) - - // Get config - const wallet = yield* call([ - solanaWalletService, - solanaWalletService.getKeypair - ]) - const sdk = yield* getSDK() - const connection = sdk.services.solanaClient.connection - const feePayerAddress = yield* waitForValue(getFeePayer) - const config = yield* call(getBuyCryptoRemoteConfig, mint) - const outputToken = TOKEN_LISTING_MAP[mint.toUpperCase()] - let userbank: PublicKey | null = null - - // Pre-emptively clear while working - yield* call( - [audiusLocalStorage, audiusLocalStorage.removeItem], - BUY_CRYPTO_VIA_SOL_STATE_KEY - ) - - try { - if (!wallet) { - throw new Error('Missing Solana root wallet') - } - - if (!feePayerAddress) { - throw new BuyCryptoError( - BuyCryptoErrorCode.BAD_FEE_PAYER, - 'Fee Payer is missing' - ) - } - - const outputTokenLamports = 10 ** outputToken.decimals - const expectedAmount = Math.ceil(amount * outputTokenLamports) - const feePayer = new PublicKey(feePayerAddress) - const { currentUser } = yield* select(getWalletAddresses) - if (!currentUser) { - throw new Error('Failed to get current user wallet address') - } - userbank = yield* call(createUserBankIfNeeded, sdk, { - mint: localStorageState.mint, - recordAnalytics: track, - ethAddress: currentUser - }) - - // Get swappable salvage amount - const minRent = yield* call( - [connection, connection.getMinimumBalanceForRentExemption], - 0 - ) - const balance = yield* call( - [connection, connection.getBalance], - wallet.publicKey - ) - // Cap the swappable salvage amount by the intended lamports, in case - // there's additional SOL for a different reason (eg. old BuyAudio recovery) - const salvageInputAmount = intendedLamports - ? Math.min(balance - minRent, intendedLamports) - : balance - minRent - - // Don't do anything if we don't have any SOL. - // Don't clear local storage either - maybe the SOL hasn't gotten to us yet? - if (salvageInputAmount <= 0) { - throw new BuyCryptoError( - BuyCryptoErrorCode.BAD_AMOUNT, - `Buy Crypto via SOL Recovery flow initiated, but no SOL in root wallet: ${wallet.publicKey.toBase58()}` - ) - } - - // Get pre swap token balance - const account = yield* call(getTokenAccountInfo, sdk, { - tokenAccount: userbank - }) - - // Get a quote for swapping the entire balance - const exactInQuote = yield* call(quoteWithAnalytics, { - quoteArgs: { - inputMint: TOKEN_LISTING_MAP.SOL.address, - outputMint: TOKEN_LISTING_MAP.USDC.address, - amount: salvageInputAmount, - swapMode: 'ExactIn', - slippageBps: config.slippageBps - }, - track, - make - }) - - // Do the swap. Just do it once, slippage shouldn't be a - // concern since the quote is fresh and the tolerance is high. - const { res, error: recoveryError } = yield* call(swapSolForCrypto, { - quoteResponse: exactInQuote, - mint, - wallet, - userbank, - feePayer - }) - - // Check response - yield* call(assertRecoverySuccess, { - res, - swapError: null, - recoveryError, - mint - }) - - // Get the token difference - const initialBalance = account?.amount ?? BigInt(0) - const newBalance = yield* call(pollForTokenBalanceChange, sdk, { - initialBalance, - tokenAccount: userbank, - mint, - retryDelayMs: config.retryDelayMs, - maxRetryCount: config.maxRetryCount - }) - const balanceChange = newBalance - initialBalance - - // Report to the console what we got - console.info( - `Salvaged ${ - exactInQuote.inAmount - } SOL into ${balanceChange} ${mint.toUpperCase()} (expected: ${expectedAmount} ${mint.toUpperCase()}): ${res}` - ) - - // Check if it's enough - if (balanceChange === undefined || balanceChange < expectedAmount) { - throw new BuyCryptoError( - BuyCryptoErrorCode.INSUFFICIENT_FUNDS_ERROR, - `Failed to swap SOL to ${expectedAmount} ${mint.toUpperCase()}. Initial Swap Error: Unknown.` - ) - } - - // Record success - yield* put(buyCryptoRecoverySucceeded()) - yield* call( - track, - make({ - eventName: Name.BUY_CRYPTO_RECOVERY_SUCCESS, - mint, - provider, - intendedSOL: intendedLamports / LAMPORTS_PER_SOL, - requestedAmount: amount - }) - ) - } catch (e) { - const error = - e instanceof BuyCryptoError - ? e - : new BuyCryptoError(BuyCryptoErrorCode.UNKNOWN, `${e}`) - - // Replace the local storage key so we can try again later - // Don't retry if the SOL was swapped away already - if (error.code !== BuyCryptoErrorCode.INSUFFICIENT_FUNDS_ERROR) { - const isExpired = Date.now() - createdAt > LOCAL_STORAGE_STATE_TTL_MS - // Only continue to retry if within 2 hours from initial attempt - // This makes sure we don't wait for SOL to arrive forever - if (!isExpired) { - yield* call( - [audiusLocalStorage, audiusLocalStorage.setJSONValue], - BUY_CRYPTO_VIA_SOL_STATE_KEY, - localStorageState - ) - } else { - console.warn('BuyCrypto recovery expired. Will not reattempt.') - } - } - - yield* call( - track, - make({ - eventName: Name.BUY_CRYPTO_RECOVERY_FAILURE, - mint, - provider, - requestedAmount: amount, - intendedSOL: intendedLamports / LAMPORTS_PER_SOL, - error: error.message - }) - ) - yield* put(buyCryptoRecoveryFailed({ error })) - yield* call(reportToSentry, { - level: ErrorLevel.Error, - error, - additionalInfo: { - wallet: wallet ? wallet.publicKey.toBase58() : '', - userbank: userbank?.toBase58() - } - }) - } -} - -// Only one purchase at a time -function* watchBuyCryptoViaSol() { - yield* takeLatest(buyCryptoViaSol, doBuyCryptoViaSol) -} - -export default function sagas() { - return [watchBuyCryptoViaSol, recoverBuyCryptoViaSolIfNecessary] -} diff --git a/packages/common/src/store/buy-crypto/slice.ts b/packages/common/src/store/buy-crypto/slice.ts deleted file mode 100644 index dbb72cbc22c..00000000000 --- a/packages/common/src/store/buy-crypto/slice.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit' - -import { MintName } from '~/services/index' -import { OnRampProvider } from '~/store/ui/buy-audio/types' -import { StripeSessionCreationError } from '~/store/ui/stripe-modal/types' - -import { BuyCryptoError } from './types' - -type BuyCryptoPayload = { - /** - * The amount the user is requesting, in user friendly decimal denomination - */ - amount: number - /** - * The mint name of the token the user wants to purchase - */ - mint: MintName - /** - * The service used to purchase the SOL necessary - */ - provider: OnRampProvider -} - -type BuyCryptoState = {} - -const initialState: BuyCryptoState = {} - -const slice = createSlice({ - name: 'buy-crypto', - initialState, - reducers: { - buyCryptoViaSol: (_state, _action: PayloadAction) => { - // Triggers saga - }, - /** - * @internal used for tracking onramp state in saga - */ - onrampSucceeded: () => { - // handled by saga - }, - /** - * @internal used for tracking onramp state in saga - */ - onrampCanceled: () => { - // handled by saga - }, - /** - * @internal used for tracking onramp state in saga - */ - onrampFailed: ( - _state, - _action: PayloadAction<{ error?: StripeSessionCreationError }> - ) => { - // handled by saga - }, - /** - * Fired when the purchase was exited by the user - */ - buyCryptoCanceled: () => { - // handled by saga - }, - /** - * Fired when an error was thrown in the saga - */ - buyCryptoFailed: ( - _state, - _action: PayloadAction<{ error: BuyCryptoError }> - ) => { - // handled by saga - }, - /** - * Fired when the purchase succeeds - */ - buyCryptoSucceeded: () => { - // handled by saga - }, - /** - * Fired when the recovery succeeds - */ - buyCryptoRecoverySucceeded: () => { - // handled by saga - }, - /** - * Fired when the recovery fails - */ - buyCryptoRecoveryFailed: ( - _state, - _action: PayloadAction<{ error: BuyCryptoError }> - ) => { - // handled by saga - } - } -}) - -export const { - buyCryptoViaSol, - onrampSucceeded, - onrampCanceled, - onrampFailed, - buyCryptoSucceeded, - buyCryptoFailed, - buyCryptoCanceled, - buyCryptoRecoverySucceeded, - buyCryptoRecoveryFailed -} = slice.actions - -export default slice.reducer -export const actions = slice.actions diff --git a/packages/common/src/store/buy-crypto/types.ts b/packages/common/src/store/buy-crypto/types.ts deleted file mode 100644 index b5248afa673..00000000000 --- a/packages/common/src/store/buy-crypto/types.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { MintName } from '~/services/index' -import { OnRampProvider } from '~/store/ui/buy-audio/types' - -export type BuyCryptoConfig = { - /** - * The maximum amount of the token allowed to be purchased - * in user friendly decimal denomination - */ - maxAmount: number - /** - * The minimum amount of the token allowed to be purchased - * in user friendly decimal denomination - */ - minAmount: number - /** - * The maximum slippage tolerance for the swap, in percentage basis points - * (1 bps = 0.01%) - */ - slippageBps: number - /** - * The time to wait between balance change polls, in milliseconds - */ - retryDelayMs?: number - /** - * The number of times to poll for balance changes - */ - maxRetryCount?: number -} - -export enum BuyCryptoErrorCode { - BAD_AMOUNT = 'BadAmount', - BAD_TOKEN = 'BadToken', - BAD_PROVIDER = 'BadProvider', - BAD_FEE_PAYER = 'BadFeePayer', - SWAP_ERROR = 'SwapError', - INSUFFICIENT_FUNDS_ERROR = 'InsufficientFunds', - ON_RAMP_ERROR = 'OnRampError', - COUNTRY_NOT_SUPPORTED = 'CountryNotSupported', - UNKNOWN = 'UnknownError' -} - -export class BuyCryptoError extends Error { - name = 'BuyCryptoError' - constructor(public code: BuyCryptoErrorCode, message: string) { - super(`${code}: ${message}`) - } -} - -export type BuyCryptoViaSolLocalStorageState = { - amount: number - mint: MintName - provider: OnRampProvider - createdAt: number - intendedLamports: number -} diff --git a/packages/common/src/store/buy-usdc/utils.ts b/packages/common/src/store/buy-usdc/utils.ts index 717e11075ab..8dbf3127301 100644 --- a/packages/common/src/store/buy-usdc/utils.ts +++ b/packages/common/src/store/buy-usdc/utils.ts @@ -7,8 +7,7 @@ import { MAX_CONTENT_PRICE_CENTS, MAX_USDC_PURCHASE_AMOUNT_CENTS, MIN_CONTENT_PRICE_CENTS, - MIN_USDC_PURCHASE_AMOUNT_CENTS, - BUY_TOKEN_VIA_SOL_SLIPPAGE_BPS + MIN_USDC_PURCHASE_AMOUNT_CENTS } from '~/services/remote-config/defaults' import { getAccountUser } from '../account/selectors' @@ -100,18 +99,12 @@ export function* getBuyUSDCRemoteConfig() { IntKeys.BUY_TOKEN_WALLET_POLL_MAX_RETRIES ) ?? undefined - // Only used in the BuyCryptoViaSol flow - const slippage = - remoteConfigInstance.getRemoteVar(IntKeys.BUY_TOKEN_VIA_SOL_SLIPPAGE_BPS) ?? - BUY_TOKEN_VIA_SOL_SLIPPAGE_BPS - return { minContentPriceCents, maxContentPriceCents, minUSDCPurchaseAmountCents, maxUSDCPurchaseAmountCents, maxRetryCount, - retryDelayMs, - slippage + retryDelayMs } } diff --git a/packages/common/src/store/index.ts b/packages/common/src/store/index.ts index b00b8035a80..c49124f18a2 100644 --- a/packages/common/src/store/index.ts +++ b/packages/common/src/store/index.ts @@ -1,7 +1,6 @@ /* eslint-disable import/export */ export * from './account' export * from './average-color' -export * from './buy-crypto' export * from './buy-usdc' export * from './cache' export * from './cast' diff --git a/packages/common/src/store/purchase-content/sagas.ts b/packages/common/src/store/purchase-content/sagas.ts index a4309a5f822..051a0792e48 100644 --- a/packages/common/src/store/purchase-content/sagas.ts +++ b/packages/common/src/store/purchase-content/sagas.ts @@ -32,13 +32,6 @@ import { User } from '~/models/User' import { BNUSDC } from '~/models/Wallet' import { FeatureFlags } from '~/services/remote-config/feature-flags' import { accountSelectors } from '~/store/account' -import { - buyCryptoCanceled, - buyCryptoFailed, - buyCryptoSucceeded, - buyCryptoViaSol -} from '~/store/buy-crypto/slice' -import { BuyCryptoError } from '~/store/buy-crypto/types' import { buyUSDCFlowFailed, buyUSDCFlowSucceeded, @@ -58,7 +51,6 @@ import { getContext } from '~/store/effects' import { getPreviewing, getTrackId } from '~/store/player/selectors' import { stop } from '~/store/player/slice' import { saveTrack } from '~/store/social/tracks/actions' -import { OnRampProvider } from '~/store/ui/buy-audio/types' import { transactionCanceled, transactionFailed, @@ -98,18 +90,6 @@ import { getBalanceNeeded } from './utils' const { getUserId, getAccountUser, getWalletAddresses } = accountSelectors -type RaceStatusResult = { - succeeded?: - | ReturnType - | ReturnType - failed?: - | ReturnType - | ReturnType - canceled?: - | ReturnType - | ReturnType -} - type GetPurchaseConfigArgs = { contentId: ID contentType: PurchaseableContentType @@ -567,44 +547,24 @@ type PurchaseUSDCWithStripeArgs = { } function* purchaseUSDCWithStripe({ amount }: PurchaseUSDCWithStripeArgs) { yield* put(buyUSDC()) - const getFeatureEnabled = yield* getContext('getFeatureEnabled') - const isBuyUSDCViaSolEnabled = yield* call( - getFeatureEnabled, - FeatureFlags.BUY_USDC_VIA_SOL - ) + const cents = Math.ceil(amount * 100) - let result: RaceStatusResult | null = null - if (isBuyUSDCViaSolEnabled) { - yield* put( - buyCryptoViaSol({ - // expects "friendly" amount, so dollars - amount: cents / 100.0, - mint: 'USDC', - provider: OnRampProvider.STRIPE - }) - ) - result = yield* race({ - succeeded: take(buyCryptoSucceeded), - failed: take(buyCryptoFailed), - canceled: take(buyCryptoCanceled) + yield* put( + onrampOpened({ + vendor: PurchaseVendor.STRIPE, + purchaseInfo: { + desiredAmount: cents + } }) - } else { - yield* put( - onrampOpened({ - vendor: PurchaseVendor.STRIPE, - purchaseInfo: { - desiredAmount: cents - } - }) - ) + ) + + const result = yield* race({ + succeeded: take(buyUSDCFlowSucceeded), + canceled: take(onrampCanceled), + failed: take(buyUSDCFlowFailed) + }) - result = yield* race({ - succeeded: take(buyUSDCFlowSucceeded), - canceled: take(onrampCanceled), - failed: take(buyUSDCFlowFailed) - }) - } // Return early for cancellation if (result.canceled) { throw new PurchaseContentError( @@ -922,9 +882,7 @@ function* doStartPurchaseContentFlow({ // If we get a known error, pipe it through directly. Otherwise make sure we // have a properly contstructed error to put into the slice. const error = - e instanceof PurchaseContentError || - e instanceof BuyUSDCError || - e instanceof BuyCryptoError + e instanceof PurchaseContentError || e instanceof BuyUSDCError ? e : new PurchaseContentError(PurchaseErrorCode.Unknown, `${e}`) diff --git a/packages/common/src/store/purchase-content/types.ts b/packages/common/src/store/purchase-content/types.ts index aa62a99aa2e..fd04df17689 100644 --- a/packages/common/src/store/purchase-content/types.ts +++ b/packages/common/src/store/purchase-content/types.ts @@ -1,5 +1,3 @@ -import { BuyCryptoErrorCode } from '~/store/buy-crypto/types' - import { BuyUSDCErrorCode } from '../buy-usdc' export enum PurchaseableContentType { @@ -32,10 +30,7 @@ export enum PurchaseErrorCode { Unknown = 'Unknown' } -export type PurchaseContentErrorCode = - | BuyUSDCErrorCode - | BuyCryptoErrorCode - | PurchaseErrorCode +export type PurchaseContentErrorCode = BuyUSDCErrorCode | PurchaseErrorCode export class PurchaseContentError extends Error { constructor(public code: PurchaseContentErrorCode, message: string) { diff --git a/packages/common/src/store/reducers.ts b/packages/common/src/store/reducers.ts index e01b6d47e37..688dc29aec1 100644 --- a/packages/common/src/store/reducers.ts +++ b/packages/common/src/store/reducers.ts @@ -7,7 +7,6 @@ import { Kind } from '../models' import account from './account/slice' import averageColorReducer from './average-color/slice' -import { buyCryptoReducer } from './buy-crypto' import { buyUSDCReducer } from './buy-usdc' import collectionsReducer from './cache/collections/reducer' import { CollectionsCacheState } from './cache/collections/types' @@ -258,7 +257,6 @@ export const reducers = (storage: Storage, history?: History) => ({ // Gated content buyUSDC: buyUSDCReducer, - buyCrypto: buyCryptoReducer, gatedContent, purchaseContent: purchaseContentReducer, withdrawUSDC: withdrawUSDCReducer, @@ -381,7 +379,6 @@ export type CommonState = { // USDC buyUSDC: ReturnType - buyCrypto: ReturnType // Tipping tipping: ReturnType diff --git a/packages/common/src/store/sagas.ts b/packages/common/src/store/sagas.ts index da432b7c244..09a5bbab492 100644 --- a/packages/common/src/store/sagas.ts +++ b/packages/common/src/store/sagas.ts @@ -26,7 +26,6 @@ import { modalsSagas } from '~/store/ui' -import { buyCryptoSagas } from './buy-crypto' import { playlistUpdatesSagas } from './playlist-updates' import { CommonStoreContext } from './storeContext' @@ -44,7 +43,6 @@ export const sagas = (_ctx: CommonStoreContext) => ({ // tracks: tracksSagas, // users: usersSagas, account: accountSagas, - buyCrypto: buyCryptoSagas, buyUSDC: buyUSDCSagas, remoteConfig: remoteConfigSagas, cast: castSagas, diff --git a/packages/mobile/src/store/sagas.ts b/packages/mobile/src/store/sagas.ts index a584a1837c3..d9d50279a10 100644 --- a/packages/mobile/src/store/sagas.ts +++ b/packages/mobile/src/store/sagas.ts @@ -1,5 +1,4 @@ import { - buyCryptoSagas, buyUSDCSagas, castSagas, chatSagas, @@ -131,7 +130,6 @@ export default function* rootSaga() { // Premium content ...gatedContentSagas(), ...purchaseContentSagas(), - ...buyCryptoSagas(), ...buyUSDCSagas(), ...stripeModalUISagas(), diff --git a/packages/web/src/store/sagas.ts b/packages/web/src/store/sagas.ts index d6f4ebaa38c..fe4fbf9efc9 100644 --- a/packages/web/src/store/sagas.ts +++ b/packages/web/src/store/sagas.ts @@ -1,5 +1,4 @@ import { - buyCryptoSagas, buyUSDCSagas, castSagas, chatSagas, @@ -221,7 +220,6 @@ export default function* rootSaga() { // Gated content gatedContentSagas(), - buyCryptoSagas(), buyUSDCSagas(), purchaseContentSagas(), withdrawUSDCSagas(),