Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Make SDK compatible with external wallets #10392

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion packages/common/src/services/auth/authService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { LocalStorage } from '../local-storage'

import { HedgehogConfig, createHedgehog } from './hedgehog'
import type { IdentityService } from './identity'
import { createHedgehogAuthAdapter } from './sdkAuthAdapter'

export type AuthServiceConfig = {
identityService: IdentityService
Expand Down Expand Up @@ -30,6 +31,7 @@ export type AuthService = {
) => Promise<SignInResponse>
signOut: () => Promise<void>
getWalletAddresses: () => Promise<GetWalletAddressesResult>
sdkAuthAdapter: ReturnType<typeof createHedgehogAuthAdapter>
}

export const createAuthService = ({
Expand All @@ -44,6 +46,10 @@ export const createAuthService = ({
createKey
})

const sdkAuthAdapter = createHedgehogAuthAdapter({
hedgehogInstance
})

const signIn = async (
email: string,
password: string,
Expand Down Expand Up @@ -76,5 +82,11 @@ export const createAuthService = ({
}
}

return { hedgehogInstance, signIn, signOut, getWalletAddresses }
return {
hedgehogInstance,
signIn,
signOut,
getWalletAddresses,
sdkAuthAdapter
}
}
56 changes: 56 additions & 0 deletions packages/common/src/services/auth/sdkAuthAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { SignTypedDataVersion } from '@metamask/eth-sig-util'
import { keccak_256 } from '@noble/hashes/sha3'
import * as secp from '@noble/secp256k1'

import { HedgehogInstance } from './hedgehog'

export const createHedgehogAuthAdapter = ({
hedgehogInstance
}: {
hedgehogInstance: HedgehogInstance
}) => {
return {
sign: async (data: string | Uint8Array) => {
await hedgehogInstance.waitUntilReady()
return await secp.sign(
keccak_256(data),
// @ts-ignore private key is private
hedgehogInstance.getWallet()?.privateKey,
{
recovered: true,
der: false
}
)
},
signTransaction: async (data: any) => {
const { signTypedData } = await import('@metamask/eth-sig-util')
await hedgehogInstance.waitUntilReady()

return signTypedData({
privateKey: Buffer.from(
// @ts-ignore private key is private
hedgehogInstance.getWallet()?.privateKey,
'hex'
),
data: data as any,
version: SignTypedDataVersion.V3
})
},
getSharedSecret: async (publicKey: string | Uint8Array) => {
await hedgehogInstance.waitUntilReady()
return secp.getSharedSecret(
// @ts-ignore private key is private
hedgehogInstance.getWallet()?.privateKey,
publicKey,
true
)
},
getAddress: async () => {
await hedgehogInstance.waitUntilReady()
return hedgehogInstance.wallet?.getAddressString() ?? ''
},
hashAndSign: async (_data: string) => {
return 'Not implemented'
}
}
}
1 change: 1 addition & 0 deletions packages/common/src/store/storeContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export type CommonStoreContext = {
instagramRedirectUrl?: string
share: (url: string, message?: string) => Promise<void> | void
audiusSdk: () => Promise<AudiusSdk>
initSdk: () => Promise<AudiusSdk>
authService: AuthService
imageUtils: {
generatePlaylistArtwork: (
Expand Down
2 changes: 1 addition & 1 deletion packages/mobile/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2407,4 +2407,4 @@ SPEC CHECKSUMS:

PODFILE CHECKSUM: 5a7fd542b0e975255433e61e18b2041151e970bd

COCOAPODS: 1.13.0
COCOAPODS: 1.15.2
6 changes: 3 additions & 3 deletions packages/mobile/src/services/sdk/audius-sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ import { Configuration, SolanaRelay, sdk } from '@audius/sdk'

import { env } from 'app/env'

import { auth } from './auth'
import { authService } from './auth'
import { discoveryNodeSelectorService } from './discoveryNodeSelector'

let inProgress = false
const SDK_LOADED_EVENT_NAME = 'AUDIUS_SDK_LOADED'
const sdkEventEmitter = new EventEmitter()
let sdkInstance: AudiusSdk

const initSdk = async () => {
export const initSdk = async () => {
inProgress = true

// For now, the only solana relay we want to use is on DN 1, so hardcode
Expand Down Expand Up @@ -46,7 +46,7 @@ const initSdk = async () => {
services: {
discoveryNodeSelector,
solanaRelay,
auth
auth: authService.sdkAuthAdapter
}
})
sdkInstance = audiusSdk
Expand Down
44 changes: 0 additions & 44 deletions packages/mobile/src/services/sdk/auth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { createAuthService } from '@audius/common/services'
import { keccak_256 } from '@noble/hashes/sha3'
import * as secp from '@noble/secp256k1'

import { createPrivateKey } from '../createPrivateKey'
import { localStorage } from '../local-storage'
Expand All @@ -12,45 +10,3 @@ export const authService = createAuthService({
identityService: identityServiceInstance,
createKey: createPrivateKey
})
const { hedgehogInstance } = authService

export const auth = {
sign: async (data: string | Uint8Array) => {
await hedgehogInstance.waitUntilReady()
return await secp.sign(
keccak_256(data),
hedgehogInstance.getWallet()?.getPrivateKey() as any,
{
recovered: true,
der: false
}
)
},
signTransaction: async (data: any) => {
const { signTypedData, SignTypedDataVersion } = await import(
'@metamask/eth-sig-util'
)
await hedgehogInstance.waitUntilReady()

return signTypedData({
privateKey: hedgehogInstance.getWallet()!.getPrivateKey(),
data,
version: SignTypedDataVersion.V3
})
},
getSharedSecret: async (publicKey: string | Uint8Array) => {
await hedgehogInstance.waitUntilReady()
return secp.getSharedSecret(
hedgehogInstance.getWallet()?.getPrivateKey() as any,
publicKey,
true
)
},
getAddress: async () => {
await hedgehogInstance.waitUntilReady()
return hedgehogInstance.wallet?.getAddressString() ?? ''
},
hashAndSign: async (_data: string) => {
return 'Not implemented'
}
}
3 changes: 2 additions & 1 deletion packages/mobile/src/store/storeContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
getFeatureEnabled,
remoteConfigInstance
} from 'app/services/remote-config'
import { audiusSdk } from 'app/services/sdk/audius-sdk'
import { audiusSdk, initSdk } from 'app/services/sdk/audius-sdk'
import { authService } from 'app/services/sdk/auth'
import { trackDownload } from 'app/services/track-download'
import { walletClient } from 'app/services/wallet-client'
Expand Down Expand Up @@ -64,6 +64,7 @@ export const storeContext: CommonStoreContext = {
instagramRedirectUrl: env.INSTAGRAM_REDIRECT_URL,
share: (url: string, message?: string) => share({ url, message }),
audiusSdk,
initSdk,
authService,
imageUtils: {
generatePlaylistArtwork
Expand Down
13 changes: 13 additions & 0 deletions packages/sdk/src/sdk/config/getConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { SdkConfig } from '../types'

import { developmentConfig } from './development'
import { productionConfig } from './production'
import { stagingConfig } from './staging'

export const getConfig = (environment: SdkConfig['environment']) => {
return environment === 'development'
? developmentConfig
: environment === 'staging'
? stagingConfig
: productionConfig
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
} from '../api/generated/default'
import { ServicesContainer } from '../types'

const SIGNATURE_EXPIRY_MS = 60 * 1000
const SIGNATURE_EXPIRY_MS = 86400 * 1000
const MESSAGE_HEADER = 'Encoded-Data-Message'
const SIGNATURE_HEADER = 'Encoded-Data-Signature'

Expand Down
11 changes: 2 additions & 9 deletions packages/sdk/src/sdk/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@ import { GrantsApi } from './api/grants/GrantsApi'
import { PlaylistsApi } from './api/playlists/PlaylistsApi'
import { TracksApi } from './api/tracks/TracksApi'
import { UsersApi } from './api/users/UsersApi'
import { developmentConfig } from './config/development'
import { productionConfig } from './config/production'
import { stagingConfig } from './config/staging'
import { getConfig } from './config/getConfig'
import {
addAppInfoMiddleware,
addRequestSignatureMiddleware
Expand Down Expand Up @@ -128,12 +126,7 @@ export const sdk = (config: SdkConfig) => {
}

const initializeServices = (config: SdkConfig) => {
const servicesConfig =
config.environment === 'development'
? developmentConfig
: config.environment === 'staging'
? stagingConfig
: productionConfig
const servicesConfig = getConfig(config.environment)

const defaultLogger = new Logger({
logLevel: config.environment !== 'production' ? 'debug' : undefined
Expand Down
119 changes: 119 additions & 0 deletions packages/sdk/src/sdk/services/Auth/ExternalWalletAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { EIP712TypedData, MessageData } from 'eth-sig-util'
import { Account, createWalletClient, custom, defineChain } from 'viem'

import { getConfig } from '../../config/getConfig'
import { SdkConfig } from '../../types'
import { DiscoveryNodeSelector } from '../DiscoveryNodeSelector'

import type { AuthService } from './types'

// TODO: Move this?
// Add type declaration for window.ethereum
interface EthereumWindow extends Window {
ethereum?: any
}

declare const window: EthereumWindow

export type ExternalWalletAuthConfig = {
environment: SdkConfig['environment']
discoveryNodeSelector: DiscoveryNodeSelector
}

const createClient = (account: Account, chainId: number, rpcUrl: string) => {
const audiusChain = defineChain({
id: chainId,
name: 'Audius',
network: 'Audius',
nativeCurrency: {
decimals: 18,
name: '-',
symbol: '-'
},
rpcUrls: {
default: {
http: [rpcUrl]
},
public: {
http: [rpcUrl]
}
}
})
return createWalletClient({
account,
chain: audiusChain,
transport: custom(window.ethereum!)
})
}

/**
* Uses external (browser) wallet as signer for transactions. Specifically targeting
* Metamask for the moment.
*/
export class ExternalWalletAuth implements AuthService {
private chainId: number
private discoveryNodeSelector: DiscoveryNodeSelector
private client?: ReturnType<typeof createClient>

constructor({
environment,
discoveryNodeSelector
}: ExternalWalletAuthConfig) {
const {
acdc: { chainId }
} = getConfig(environment)
this.chainId = chainId
this.discoveryNodeSelector = discoveryNodeSelector
}

async getClient() {
if (this.client) {
return this.client
}
const rpcUrl = `${await this.discoveryNodeSelector.getSelectedEndpoint()}/chain`

if (!window.ethereum) throw new Error('No window.ethereum found')

const [account]: Account[] = await window.ethereum.request({
method: 'eth_requestAccounts'
})
if (!account) throw new Error('No account returned from Wallet')
this.client = createClient(account, this.chainId, rpcUrl)
return this.client
}

getSharedSecret: (publicKey: string | Uint8Array) => Promise<Uint8Array> =
async (_publicKey) => {
throw new Error('ExternalWalletAuth does not support getSharedSecret()')
}

sign: (data: string | Uint8Array) => Promise<[Uint8Array, number]> = async (
data
) => {
const client = await this.getClient()
const message = typeof data === 'string' ? data : { raw: data }
const signed = await client.signMessage({ message })
// Convert to buffer since calling code expects that...
const buffer = new Uint8Array(Buffer.from(signed, 'utf-8'))
return [buffer, buffer.length]
}

hashAndSign: (data: string) => Promise<string> = () => {
throw new Error('hashAndSign not supported')
}

signTransaction: (
data: MessageData<EIP712TypedData>['data']
) => Promise<string> = async (data) => {
const client = await this.getClient()
// TODO: Type of chainId is messing this up
// @ts-ignore
return client.signTypedData(data)
}

getAddress: () => Promise<string> = async () => {
const client = await this.getClient()
const [address] = await client.getAddresses()
return address || ''
}
}
1 change: 1 addition & 0 deletions packages/sdk/src/sdk/services/Auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './types'
export * from './UserAuth'
export * from './AppAuth'
export * from './DefaultAuth'
export * from './ExternalWalletAuth'
11 changes: 8 additions & 3 deletions packages/web/src/common/store/account/sagas.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,14 @@ export function* fetchAccountAsync({ isSignUp = false }) {
return
}

const accountData = yield call(userApiFetchSaga.getUserAccount, {
wallet
})
let accountData
try {
accountData = yield call(userApiFetchSaga.getUserAccount, {
wallet
})
} catch (e) {
console.error('Failed to fetch account data')
}

if (!accountData || !accountData.user) {
yield put(
Expand Down
Loading