From a8ddc72bebf0a4597ee06ebcd26232e3dd64d9cb Mon Sep 17 00:00:00 2001 From: Raymond Jacobson Date: Fri, 6 Sep 2024 19:33:14 -0700 Subject: [PATCH 1/2] [PAY-3386] Implement mobile pay with anything --- .../src/store/purchase-content/sagas.ts | 78 ++++++++++++++++--- packages/common/src/store/storeContext.ts | 11 +++ .../NavigationContainer.tsx | 8 ++ .../payment-method/PaymentMethod.tsx | 51 ++++++++---- .../components/payment-method/TokenPicker.tsx | 17 ++-- .../PremiumContentPurchaseDrawer.tsx | 8 +- .../src/screens/app-screen/AppTabsScreen.tsx | 3 + .../wallet-connect/ConnectNewWalletButton.tsx | 2 - .../WalletConnectModalScreen.tsx | 2 + .../src/screens/wallet-connect/types.ts | 9 +++ .../wallet-connect/usePhantomConnect.ts | 23 +++++- packages/mobile/src/store/storeContext.ts | 8 ++ .../src/store/utils/phantomWalletConnect.ts | 57 ++++++++++++++ .../sagas/connectNewWalletSaga.ts | 51 ++++++++++-- .../src/store/wallet-connect/sagas/index.ts | 4 +- .../mobile/src/store/wallet-connect/slice.ts | 3 + .../mobile/src/store/wallet-connect/types.ts | 10 +++ 17 files changed, 295 insertions(+), 50 deletions(-) create mode 100644 packages/mobile/src/store/utils/phantomWalletConnect.ts diff --git a/packages/common/src/store/purchase-content/sagas.ts b/packages/common/src/store/purchase-content/sagas.ts index 449d7c3801f..21f8447eeb0 100644 --- a/packages/common/src/store/purchase-content/sagas.ts +++ b/packages/common/src/store/purchase-content/sagas.ts @@ -9,8 +9,10 @@ import { TransactionMessage } from '@solana/web3.js' import BN from 'bn.js' +import bs58 from 'bs58' import { sumBy } from 'lodash' import { takeLatest } from 'redux-saga/effects' +import nacl, { BoxKeyPair } from 'tweetnacl' import { call, put, race, select, take } from 'typed-redux-saga' import { userTrackMetadataFromSDK } from '~/adapters' @@ -66,6 +68,7 @@ import { CoinflowPurchaseMetadata, coinflowOnrampModalActions } from '~/store/ui/modals/coinflow-onramp-modal' +import { waitForValue } from '~/utils' import { encodeHashId } from '~/utils/hashIds' import { BN_USDC_CENT_WEI } from '~/utils/wallet' @@ -111,6 +114,23 @@ type GetPurchaseConfigArgs = { contentType: PurchaseableContentType } +const serializeKeyPair = (value: BoxKeyPair) => { + const { publicKey, secretKey } = value + const encodedKeyPair = { + publicKey: bs58.encode(publicKey), + secretKey: bs58.encode(secretKey) + } + return JSON.stringify(encodedKeyPair) +} + +const deserializeKeyPair = (value: string): BoxKeyPair => { + const { publicKey, secretKey } = JSON.parse(value) + return { + publicKey: bs58.decode(publicKey), + secretKey: bs58.decode(secretKey) + } +} + function* getContentInfo({ contentId, contentType }: GetPurchaseConfigArgs) { const metadata = contentType === PurchaseableContentType.ALBUM @@ -911,12 +931,30 @@ function* purchaseWithAnything({ throw new Error('Failed to fetch USDC user bank token account info') } - // Get the solana wallet provider - const provider = window.solana - if (!provider) { - throw new Error('No solana provider / wallet found') + let sourceWallet: PublicKey + + const isNativeMobile = yield* getContext('isNativeMobile') + const mobileWalletActions = yield* getContext('mobileWalletActions') + if (isNativeMobile && mobileWalletActions) { + const { connect } = mobileWalletActions + const getWalletConnectPublicKey = (state: any) => + state.walletConnect.publicKey + const dappKeyPair = nacl.box.keyPair() + yield* put({ + type: 'walletConnect/setDappKeyPair', + payload: { dappKeyPair: serializeKeyPair(dappKeyPair) } + }) + yield* call(connect, dappKeyPair) + yield* call(waitForValue, getWalletConnectPublicKey) + sourceWallet = new PublicKey(yield* select(getWalletConnectPublicKey)) + } else { + // Get the solana wallet provider + const provider = window.solana + if (!provider) { + throw new Error('No solana provider / wallet found') + } + sourceWallet = (yield* call(provider.connect)).publicKey } - const sourceWallet = yield* call(provider.connect) let transaction: VersionedTransaction if (inputMint === TOKEN_LISTING_MAP.USDC.address) { @@ -926,7 +964,7 @@ function* purchaseWithAnything({ sdk.services.paymentRouterClient.createTransferInstruction ], { - sourceWallet: sourceWallet.publicKey, + sourceWallet, total: totalAmountWithDecimals, mint: 'USDC' } @@ -934,7 +972,7 @@ function* purchaseWithAnything({ transaction = yield* call( [sdk.services.solanaClient, sdk.services.solanaClient.buildTransaction], { - feePayer: sourceWallet.publicKey, + feePayer: sourceWallet, instructions: [instruction] } ) @@ -950,7 +988,7 @@ function* purchaseWithAnything({ ) const externalTokenAccountPublicKey = getAssociatedTokenAddressSync( new PublicKey(inputMint), - sourceWallet.publicKey + sourceWallet ) console.info('Calling jupiter API to get a quote') @@ -984,7 +1022,7 @@ function* purchaseWithAnything({ if (inputMint === TOKEN_LISTING_MAP.SOL.address) { const amount = yield* call( [connection, connection.getBalance], - sourceWallet.publicKey + sourceWallet ) hasEnoughTokens = amount >= BigInt(quote.inAmount) } @@ -1000,7 +1038,7 @@ function* purchaseWithAnything({ const { swapTransaction } = yield* call([jup, jup.swapPost], { swapRequest: { quoteResponse: quote, - userPublicKey: sourceWallet.publicKey.toString(), + userPublicKey: sourceWallet.toString(), destinationTokenAccount: paymentRouterTokenAccount.address.toString() } }) @@ -1076,7 +1114,25 @@ function* purchaseWithAnything({ transaction.message = message.compileToV0Message(addressLookupTableAccounts) // Execute the swap by signing and sending the transaction - return yield* call([provider, provider.signAndSendTransaction], transaction) + if (isNativeMobile && mobileWalletActions) { + const { signAndSendTransaction } = mobileWalletActions + const getWalletConnectState = (state: any) => state.walletConnect + const { dappKeyPair, sharedSecret, session } = yield* select( + getWalletConnectState + ) + return yield* call(signAndSendTransaction, { + transaction, + dappKeyPair, + sharedSecret: new Uint8Array(bs58.decode(sharedSecret)), + session + }) + } else { + const provider = window.solana + return yield* call( + [provider, provider.signAndSendTransaction], + transaction + ) + } } catch (e) { console.error(`handlePayWithAnything | Error: ${e}`) throw e diff --git a/packages/common/src/store/storeContext.ts b/packages/common/src/store/storeContext.ts index 9b2d5e9d5e8..499e5e82cb1 100644 --- a/packages/common/src/store/storeContext.ts +++ b/packages/common/src/store/storeContext.ts @@ -1,7 +1,9 @@ import { FetchNFTClient } from '@audius/fetch-nft' import type { AudiusSdk } from '@audius/sdk' +import { VersionedTransaction } from '@solana/web3.js' import { Location } from 'history' import { Dispatch } from 'redux' +import nacl from 'tweetnacl' import { AllTrackingEvents, @@ -81,4 +83,13 @@ export type CommonStoreContext = { } isMobile: boolean dispatch: Dispatch + mobileWalletActions?: { + connect: (dappKeyPair: nacl.BoxKeyPair) => void + signAndSendTransaction: (params: { + transaction: VersionedTransaction + session: string + sharedSecret: Uint8Array + dappKeyPair: nacl.BoxKeyPair + }) => void + } } diff --git a/packages/mobile/src/components/navigation-container/NavigationContainer.tsx b/packages/mobile/src/components/navigation-container/NavigationContainer.tsx index e76880c69f0..c716a133450 100644 --- a/packages/mobile/src/components/navigation-container/NavigationContainer.tsx +++ b/packages/mobile/src/components/navigation-container/NavigationContainer.tsx @@ -223,6 +223,14 @@ const NavigationContainer = (props: NavigationContainerProps) => { path = path.replace('#embed', '') + const connectPath = /^\/(connect)/ + if (path.match(connectPath)) { + path = `${path.replace( + connectPath, + routeNameRef.current ?? '/feed' + )}&path=connect` + } + const walletConnectPath = /^\/(wallet-connect)/ if (path.match(walletConnectPath)) { path = `${path.replace( diff --git a/packages/mobile/src/components/payment-method/PaymentMethod.tsx b/packages/mobile/src/components/payment-method/PaymentMethod.tsx index da7fb6af000..b00b71f8075 100644 --- a/packages/mobile/src/components/payment-method/PaymentMethod.tsx +++ b/packages/mobile/src/components/payment-method/PaymentMethod.tsx @@ -18,9 +18,11 @@ import { Text, Flex, IconQrCode, - IconPhantomPlain + IconPhantomPlain, + IconCaretRight } from '@audius/harmony-native' import { Divider, RadioButton } from 'app/components/core' +import { useNavigation } from 'app/hooks/useNavigation' import { getPurchaseVendor } from 'app/store/purchase-vendor/selectors' import { setPurchaseVendor } from 'app/store/purchase-vendor/slice' import { flexRowCentered, makeStyles } from 'app/styles' @@ -39,7 +41,7 @@ const messages = { withCard: 'Credit/Debit Card', withCrypto: 'USDC Transfer', withAnything: 'Pay with Anything', - requiresPhantom: 'Phantom wallet required', + withAnyToken: 'Pay with any Solana token', showAdvanced: 'Show advanced options', hideAdvanced: 'Hide advanced options' } @@ -51,6 +53,7 @@ const useStyles = makeStyles(({ spacing }) => ({ }, rowTitle: { ...flexRowCentered(), + alignItems: 'flex-start', gap: spacing(3) }, rowTitleText: { @@ -177,6 +180,9 @@ export const PaymentMethod = ({ icon: IconQrCode } ] + + const navigation = useNavigation() + if ( isPayWithAnythingEnabled && selectedPurchaseMethodMintAddress && @@ -186,20 +192,37 @@ export const PaymentMethod = ({ id: PurchaseMethod.WALLET, value: PurchaseMethod.WALLET, label: ( - + - - {messages.withAnything} - - + {messages.withAnything} - - {messages.requiresPhantom} - + {selectedMethod === PurchaseMethod.WALLET ? ( + { + navigation.navigate('TokenPicker') + handleOpenTokenPicker() + }} + hitSlop={spacing(6)} + > + + + {messages.withAnyToken} + + + + + + + + ) : null} ), icon: IconPhantomPlain diff --git a/packages/mobile/src/components/payment-method/TokenPicker.tsx b/packages/mobile/src/components/payment-method/TokenPicker.tsx index c28e6f6cd37..2950cf07cff 100644 --- a/packages/mobile/src/components/payment-method/TokenPicker.tsx +++ b/packages/mobile/src/components/payment-method/TokenPicker.tsx @@ -31,7 +31,9 @@ export const TokenPicker = ({ onChange: (address: string) => void onOpen: () => void }) => { - const [selectedAddress, setSelectedAddress] = useState(selectedTokenAddress) + const [selectedAddress, setSelectedAddress] = useState( + selectedTokenAddress + ) const assets = useAsync(async () => { const res = await fetch(TOKEN_LIST_URL) @@ -40,7 +42,9 @@ export const TokenPicker = ({ }, []) const handleSubmit = useCallback(() => { - onChange(selectedAddress) + if (selectedAddress) { + onChange(selectedAddress) + } }, [onChange, selectedAddress]) const navigation = useNavigation() @@ -69,8 +73,8 @@ export const TokenPicker = ({ ) const selectedOption = useMemo( - () => options.find((option) => option.value === selectedTokenAddress), - [selectedTokenAddress, options] + () => options.find((option) => option.value === selectedAddress), + [selectedAddress, options] ) if (assets.loading || assets.error) { @@ -107,9 +111,10 @@ export const TokenPicker = ({ /> { const asset = optionsMap[item.value] return ( @@ -132,7 +137,7 @@ export const TokenPicker = ({ numberOfLines={1} ellipsizeMode='tail' > - {asset.name} + {`(${asset.name})`} ) diff --git a/packages/mobile/src/components/premium-content-purchase-drawer/PremiumContentPurchaseDrawer.tsx b/packages/mobile/src/components/premium-content-purchase-drawer/PremiumContentPurchaseDrawer.tsx index f63240fad7c..e142ff85b09 100644 --- a/packages/mobile/src/components/premium-content-purchase-drawer/PremiumContentPurchaseDrawer.tsx +++ b/packages/mobile/src/components/premium-content-purchase-drawer/PremiumContentPurchaseDrawer.tsx @@ -248,11 +248,9 @@ const RenderForm = ({ const { isEnabled: isIOSUSDCPurchaseEnabled } = useFeatureFlag( FeatureFlags.IOS_USDC_PURCHASE_ENABLED ) - // TODO Re-enable pay with anything (mobile) when fully working - // const { isEnabled: isPayWithAnythingEnabledFlag } = useFeatureFlag( - // FeatureFlags.PAY_WITH_ANYTHING_ENABLED - // ) - const isPayWithAnythingEnabledFlag = false + const { isEnabled: isPayWithAnythingEnabledFlag } = useFeatureFlag( + FeatureFlags.PAY_WITH_ANYTHING_ENABLED + ) const isIOSDisabled = Platform.OS === 'ios' && !isIOSUSDCPurchaseEnabled const { submitForm, resetForm } = useFormikContext() diff --git a/packages/mobile/src/screens/app-screen/AppTabsScreen.tsx b/packages/mobile/src/screens/app-screen/AppTabsScreen.tsx index a9f34d505dd..2b5e71c2878 100644 --- a/packages/mobile/src/screens/app-screen/AppTabsScreen.tsx +++ b/packages/mobile/src/screens/app-screen/AppTabsScreen.tsx @@ -7,6 +7,8 @@ import { createBottomTabNavigator } from '@react-navigation/bottom-tabs' import type { NavigatorScreenParams } from '@react-navigation/native' import { useDispatch } from 'react-redux' +import { usePhantomConnect } from '../wallet-connect/usePhantomConnect' + import { AppTabBar } from './AppTabBar' import type { ExploreTabScreenParamList } from './ExploreTabScreen' import { ExploreTabScreen } from './ExploreTabScreen' @@ -36,6 +38,7 @@ const tabBar = (props: BottomTabBarProps) => export const AppTabsScreen = () => { const dispatch = useDispatch() const appState = useAppState() + usePhantomConnect() useEffect(() => { if (appState === 'active') { diff --git a/packages/mobile/src/screens/wallet-connect/ConnectNewWalletButton.tsx b/packages/mobile/src/screens/wallet-connect/ConnectNewWalletButton.tsx index ebfbf9e6a5f..e545659e592 100644 --- a/packages/mobile/src/screens/wallet-connect/ConnectNewWalletButton.tsx +++ b/packages/mobile/src/screens/wallet-connect/ConnectNewWalletButton.tsx @@ -8,7 +8,6 @@ import { Button } from '@audius/harmony-native' import { makeStyles } from 'app/styles' import { useCanConnectNewWallet } from './useCanConnectNewWallet' -import { usePhantomConnect } from './usePhantomConnect' import { useWalletConnect } from './useWalletConnect' const { getConfirmingWalletStatus, getRemoveWallet } = @@ -29,7 +28,6 @@ const useStyles = makeStyles(({ spacing }) => ({ export const ConnectNewWalletButton = () => { const styles = useStyles() const { connector } = useWalletConnect() - usePhantomConnect() const newWalletStatus = useSelector(getConfirmingWalletStatus) const { status: removeWalletStatus } = useSelector(getRemoveWallet) diff --git a/packages/mobile/src/screens/wallet-connect/WalletConnectModalScreen.tsx b/packages/mobile/src/screens/wallet-connect/WalletConnectModalScreen.tsx index db7dd495181..579be548cd7 100644 --- a/packages/mobile/src/screens/wallet-connect/WalletConnectModalScreen.tsx +++ b/packages/mobile/src/screens/wallet-connect/WalletConnectModalScreen.tsx @@ -6,6 +6,7 @@ import { useAppScreenOptions } from '../app-screen/useAppScreenOptions' import { WalletConnectScreen } from './WalletConnectScreen' import { ConfirmRemoveWalletDrawer, WalletsDrawer } from './components' +import { usePhantomConnect } from './usePhantomConnect' const Stack = createNativeStackNavigator() @@ -13,6 +14,7 @@ const screenOptionOverrides = { headerRight: () => null } export const WalletConnectModalScreen = () => { const screenOptions = useAppScreenOptions(screenOptionOverrides) + usePhantomConnect() return ( diff --git a/packages/mobile/src/screens/wallet-connect/types.ts b/packages/mobile/src/screens/wallet-connect/types.ts index 16ff6ed2e39..e9258e4d948 100644 --- a/packages/mobile/src/screens/wallet-connect/types.ts +++ b/packages/mobile/src/screens/wallet-connect/types.ts @@ -1,5 +1,12 @@ import type { RouteProp } from '@react-navigation/native' +export type ConnectParams = { + path: 'connect' + phantom_encryption_public_key: string + data: string + nonce: string +} + export type ConnectNewWalletParams = { path: 'wallet-connect' phantom_encryption_public_key: string @@ -20,6 +27,7 @@ export type SignedMessageParams = { export type WalletConnectParamList = { Wallets: + | ConnectParams | ConnectNewWalletParams | CancelPhantomConnectParams | SignedMessageParams @@ -32,6 +40,7 @@ export type WalletConnectParamList = { export type WalletConnectParams = | undefined + | ConnectParams | ConnectNewWalletParams | SignedMessageParams diff --git a/packages/mobile/src/screens/wallet-connect/usePhantomConnect.ts b/packages/mobile/src/screens/wallet-connect/usePhantomConnect.ts index ac650f54ec0..b25d2511450 100644 --- a/packages/mobile/src/screens/wallet-connect/usePhantomConnect.ts +++ b/packages/mobile/src/screens/wallet-connect/usePhantomConnect.ts @@ -4,7 +4,11 @@ import { tokenDashboardPageActions } from '@audius/common/store' import { useRoute } from '@react-navigation/native' import { useDispatch } from 'react-redux' -import { connectNewWallet, signMessage } from 'app/store/wallet-connect/slice' +import { + connect, + connectNewWallet, + signMessage +} from 'app/store/wallet-connect/slice' import type { WalletConnectRoute } from './types' @@ -12,7 +16,11 @@ const { updateWalletError } = tokenDashboardPageActions export const usePhantomConnect = () => { const dispatch = useDispatch() - const { params } = useRoute>() + const route = useRoute>() + // I have no idea what's going on here. I don't think this works currently, + // somehow the params we care about is inside the other params ??? + // @ts-ignore + const params = route?.params?.params?.params useEffect(() => { if (!params) return @@ -20,9 +28,18 @@ export const usePhantomConnect = () => { dispatch(updateWalletError({ errorMessage: params.errorMessage })) return } - if (params.path === 'wallet-connect') { + + // The following set of cases are hooks that handle deep links from + // Phantom back to Audius. + if (params.path === 'connect') { + dispatch(connect({ ...params, connectionType: 'phantom' })) + // Connect creates a session between Audius and phantom + } else if (params.path === 'wallet-connect') { + // Wallet-connect creates a session between Audius and phantom + // and then associates the wallet to the user's profile dispatch(connectNewWallet({ ...params, connectionType: 'phantom' })) } else if (params.path === 'wallet-sign-message') { + // Wallet-sign-message receives signs a string message dispatch(signMessage({ ...params, connectionType: 'phantom' })) } }, [params?.path, params, dispatch]) diff --git a/packages/mobile/src/store/storeContext.ts b/packages/mobile/src/store/storeContext.ts index 7321649065c..9942c2decc4 100644 --- a/packages/mobile/src/store/storeContext.ts +++ b/packages/mobile/src/store/storeContext.ts @@ -18,6 +18,10 @@ import { import { audiusSdk } from 'app/services/sdk/audius-sdk' import { trackDownload } from 'app/services/track-download' import { walletClient } from 'app/services/wallet-client' +import { + connect, + signAndSendTransaction +} from 'app/store/utils/phantomWalletConnect' import { generatePlaylistArtwork } from 'app/utils/generatePlaylistArtwork' import { reportToSentry } from 'app/utils/reportToSentry' import share from 'app/utils/share' @@ -63,6 +67,10 @@ export const storeContext: CommonStoreContext = { generatePlaylistArtwork }, isMobile: true, + mobileWalletActions: { + connect, + signAndSendTransaction + }, // @ts-ignore dispatch will be populated in store.ts dispatch: undefined } diff --git a/packages/mobile/src/store/utils/phantomWalletConnect.ts b/packages/mobile/src/store/utils/phantomWalletConnect.ts new file mode 100644 index 00000000000..b7ea352d4f0 --- /dev/null +++ b/packages/mobile/src/store/utils/phantomWalletConnect.ts @@ -0,0 +1,57 @@ +// package to assist with phantom wallet connect + +import type { VersionedTransaction } from '@solana/web3.js' +import bs58 from 'bs58' +import { Linking } from 'react-native' + +import { + buildUrl, + deserializeKeyPair, + encryptPayload +} from 'app/store/wallet-connect/utils' + +export const connect = async (dappKeyPair: nacl.BoxKeyPair) => { + const params = new URLSearchParams({ + dapp_encryption_public_key: bs58.encode(dappKeyPair.publicKey), + cluster: 'mainnet-beta', + app_url: 'https://phantom.app', + redirect_link: 'audius://connect' + }) + + const url = buildUrl('connect', params) + Linking.openURL(url) +} + +export type SignAndSendTransactionProps = { + transaction: VersionedTransaction + session: string + sharedSecret: Uint8Array + dappKeyPair: string +} + +export const signAndSendTransaction = async ({ + transaction, + session, + sharedSecret, + dappKeyPair +}: SignAndSendTransactionProps) => { + const serializedTransaction = transaction.serialize() + + const payload = { + session, + transaction: bs58.encode(serializedTransaction) + } + const [nonce, encryptedPayload] = encryptPayload(payload, sharedSecret) + + const params = new URLSearchParams({ + dapp_encryption_public_key: bs58.encode( + deserializeKeyPair(dappKeyPair).publicKey + ), + nonce: bs58.encode(nonce), + redirect_link: 'audius://', + payload: bs58.encode(encryptedPayload) + }) + + const url = buildUrl('signAndSendTransaction', params) + Linking.openURL(url) +} diff --git a/packages/mobile/src/store/wallet-connect/sagas/connectNewWalletSaga.ts b/packages/mobile/src/store/wallet-connect/sagas/connectNewWalletSaga.ts index 213670be787..abaf924d436 100644 --- a/packages/mobile/src/store/wallet-connect/sagas/connectNewWalletSaga.ts +++ b/packages/mobile/src/store/wallet-connect/sagas/connectNewWalletSaga.ts @@ -18,13 +18,14 @@ import type { JsonMap } from 'app/types/analytics' import { getDappKeyPair } from '../selectors' import { + connect, connectNewWallet, setConnectionStatus, setPublicKey, setSession, setSharedSecret } from '../slice' -import type { ConnectNewWalletAction } from '../types' +import type { ConnectAction, ConnectNewWalletAction } from '../types' import { buildUrl, decryptPayload, encryptPayload } from '../utils' const { setIsConnectingWallet, connectNewWallet: baseConnectNewWallet } = tokenDashboardPageActions @@ -37,6 +38,7 @@ export function* convertToChecksumAddress(address: WalletAddress) { return ethWeb3.utils.toChecksumAddress(address) } +// Connection a new wallet to an Audius account function* connectNewWalletAsync(action: ConnectNewWalletAction) { const accountUserId = yield* select(getUserId) if (!accountUserId) return @@ -64,6 +66,11 @@ function* connectNewWalletAsync(action: ConnectNewWalletAction) { ) const connectData = decryptPayload(data, nonce, sharedSecretDapp) const { session, public_key } = connectData + yield* put( + setSharedSecret({ sharedSecret: bs58.encode(sharedSecretDapp) }) + ) + yield* put(setSession({ session })) + yield* put(setPublicKey({ publicKey: public_key })) const isNewWallet = yield* checkIsNewWallet(public_key, Chain.Sol) if (!isNewWallet) return @@ -72,12 +79,6 @@ function* connectNewWalletAsync(action: ConnectNewWalletAction) { public_key, Chain.Sol ) - - yield* put( - setSharedSecret({ sharedSecret: bs58.encode(sharedSecretDapp) }) - ) - yield* put(setSession({ session })) - yield* put(setPublicKey({ publicKey: public_key })) yield* put( setIsConnectingWallet({ wallet: public_key, @@ -183,6 +184,42 @@ function* connectNewWalletAsync(action: ConnectNewWalletAction) { } } +// Connect a wallet to establish a session, but don't connect +// to an Audius account +function* connectAsync(action: ConnectAction) { + const accountUserId = yield* select(getUserId) + if (!accountUserId) return + switch (action.payload.connectionType) { + case null: + console.error('No connection type set') + break + case 'phantom': { + const { phantom_encryption_public_key, data, nonce } = action.payload + const dappKeyPair = yield* select(getDappKeyPair) + + if (!dappKeyPair) return + + const sharedSecretDapp = nacl.box.before( + bs58.decode(phantom_encryption_public_key), + dappKeyPair.secretKey + ) + const connectData = decryptPayload(data, nonce, sharedSecretDapp) + const { session, public_key } = connectData + yield* put( + setSharedSecret({ sharedSecret: bs58.encode(sharedSecretDapp) }) + ) + yield* put(setSession({ session })) + yield* put(setPublicKey({ publicKey: public_key })) + } + } +} + +export function* watchConnect() { + yield* takeEvery(connect.type, function* (action: ConnectAction) { + yield* call(connectAsync, action) + }) +} + export function* watchConnectNewWallet() { yield* takeEvery( connectNewWallet.type, diff --git a/packages/mobile/src/store/wallet-connect/sagas/index.ts b/packages/mobile/src/store/wallet-connect/sagas/index.ts index ed1077dff7a..693ae60012c 100644 --- a/packages/mobile/src/store/wallet-connect/sagas/index.ts +++ b/packages/mobile/src/store/wallet-connect/sagas/index.ts @@ -1,6 +1,6 @@ -import { watchConnectNewWallet } from './connectNewWalletSaga' +import { watchConnect, watchConnectNewWallet } from './connectNewWalletSaga' import { watchSignMessage } from './signMessageSaga' export default function sagas() { - return [watchConnectNewWallet, watchSignMessage] + return [watchConnect, watchConnectNewWallet, watchSignMessage] } diff --git a/packages/mobile/src/store/wallet-connect/slice.ts b/packages/mobile/src/store/wallet-connect/slice.ts index 876f28e1e1f..bc34e16d93d 100644 --- a/packages/mobile/src/store/wallet-connect/slice.ts +++ b/packages/mobile/src/store/wallet-connect/slice.ts @@ -3,6 +3,7 @@ import type { PayloadAction } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit' import type { + ConnectAction, ConnectionType, ConnectNewWalletAction, SignMessageAction @@ -32,6 +33,7 @@ const walletConnectSlice = createSlice({ name: 'walletConnect', initialState, reducers: { + connect: (_state, _action: ConnectAction) => {}, connectNewWallet: (_state, _action: ConnectNewWalletAction) => {}, signMessage: (_state, _action: SignMessageAction) => {}, setDappKeyPair: (state, action: PayloadAction<{ dappKeyPair: string }>) => { @@ -62,6 +64,7 @@ const walletConnectSlice = createSlice({ }) export const { + connect, connectNewWallet, signMessage, setDappKeyPair, diff --git a/packages/mobile/src/store/wallet-connect/types.ts b/packages/mobile/src/store/wallet-connect/types.ts index 444b4e9eefd..42f847ad1c5 100644 --- a/packages/mobile/src/store/wallet-connect/types.ts +++ b/packages/mobile/src/store/wallet-connect/types.ts @@ -5,6 +5,14 @@ export type ConnectionType = | 'solana-phone-wallet-adapter' | 'wallet-connect' +type Connect = { + connectionType: 'phantom' + path: 'connect' + phantom_encryption_public_key: string + data: string + nonce: string +} + type ConnectNewPhantomWallet = { connectionType: 'phantom' path: 'wallet-connect' @@ -27,6 +35,8 @@ export type ConnectNewWalletAction = PayloadAction< ConnectNewPhantomWallet | ConnectNewEthWallet | ConnectSolanaPhoneWallet > +export type ConnectAction = PayloadAction + type SignPhantomWallet = { connectionType: 'phantom' path: 'wallet-sign-message' From 1c2b45e7567e1d5607d423462e0ca17288c69935 Mon Sep 17 00:00:00 2001 From: Raymond Jacobson Date: Sat, 7 Sep 2024 11:38:22 -0700 Subject: [PATCH 2/2] Fix types --- packages/common/src/store/purchase-content/sagas.ts | 2 +- .../mobile/src/store/utils/phantomWalletConnect.ts | 13 ++++--------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/common/src/store/purchase-content/sagas.ts b/packages/common/src/store/purchase-content/sagas.ts index 21f8447eeb0..1f925872298 100644 --- a/packages/common/src/store/purchase-content/sagas.ts +++ b/packages/common/src/store/purchase-content/sagas.ts @@ -1122,7 +1122,7 @@ function* purchaseWithAnything({ ) return yield* call(signAndSendTransaction, { transaction, - dappKeyPair, + dappKeyPair: deserializeKeyPair(dappKeyPair), sharedSecret: new Uint8Array(bs58.decode(sharedSecret)), session }) diff --git a/packages/mobile/src/store/utils/phantomWalletConnect.ts b/packages/mobile/src/store/utils/phantomWalletConnect.ts index b7ea352d4f0..c6de3ed7962 100644 --- a/packages/mobile/src/store/utils/phantomWalletConnect.ts +++ b/packages/mobile/src/store/utils/phantomWalletConnect.ts @@ -3,12 +3,9 @@ import type { VersionedTransaction } from '@solana/web3.js' import bs58 from 'bs58' import { Linking } from 'react-native' +import type nacl from 'tweetnacl' -import { - buildUrl, - deserializeKeyPair, - encryptPayload -} from 'app/store/wallet-connect/utils' +import { buildUrl, encryptPayload } from 'app/store/wallet-connect/utils' export const connect = async (dappKeyPair: nacl.BoxKeyPair) => { const params = new URLSearchParams({ @@ -26,7 +23,7 @@ export type SignAndSendTransactionProps = { transaction: VersionedTransaction session: string sharedSecret: Uint8Array - dappKeyPair: string + dappKeyPair: nacl.BoxKeyPair } export const signAndSendTransaction = async ({ @@ -44,9 +41,7 @@ export const signAndSendTransaction = async ({ const [nonce, encryptedPayload] = encryptPayload(payload, sharedSecret) const params = new URLSearchParams({ - dapp_encryption_public_key: bs58.encode( - deserializeKeyPair(dappKeyPair).publicKey - ), + dapp_encryption_public_key: bs58.encode(dappKeyPair.publicKey), nonce: bs58.encode(nonce), redirect_link: 'audius://', payload: bs58.encode(encryptedPayload)