Skip to content

Commit

Permalink
Implement ApeX StarkKey recovery (#415)
Browse files Browse the repository at this point in the history
* implement apex starkkey recovery

Co-authored-by: Piotr Szlachciak <[email protected]>

* implement apex testnet starkkey recovery

* pass isMainnet in context

* change isMainnet flag to chainId

* improve user experience regarding metamask

* fix linter errors

* fix failing test

---------

Co-authored-by: Piotr Szlachciak <[email protected]>
  • Loading branch information
torztomasz and sz-piotr authored Jul 10, 2023
1 parent 969e3f4 commit 3acd964
Show file tree
Hide file tree
Showing 17 changed files with 237 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,11 @@ describe(ForcedTradeOfferController.name, () => {
const pageContext: PageContext = {
user: undefined,
tradingMode: 'perpetual',
chainId: 1,
instanceName: 'dYdX',
collateralAsset: fakeCollateralAsset,
}

describe(
ForcedTradeOfferController.prototype.getOfferDetailsPage.name,
() => {
Expand Down
14 changes: 14 additions & 0 deletions packages/backend/src/core/PageContextService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,18 @@ describe(PageContextService.name, () => {
tradingMode: 'perpetual',
instanceName: 'dYdX',
collateralAsset: fakeCollateralAsset,
blockchain: {
chainId: 1,
},
},
} as Config
const spotConfig = {
starkex: {
tradingMode: 'spot',
instanceName: 'Myria',
blockchain: {
chainId: 5,
},
},
} as const as Config

Expand All @@ -39,6 +45,7 @@ describe(PageContextService.name, () => {
expect(context).toEqual({
user: undefined,
tradingMode: 'perpetual',
chainId: 1,
instanceName: perpetualConfig.starkex.instanceName,
collateralAsset: fakeCollateralAsset,
})
Expand All @@ -62,6 +69,7 @@ describe(PageContextService.name, () => {
expect(context).toEqual({
user: undefined,
tradingMode: 'spot',
chainId: 5,
instanceName: spotConfig.starkex.instanceName,
})
expect(mockedUserService.getUserDetails).toHaveBeenCalledWith(givenUser)
Expand All @@ -81,6 +89,7 @@ describe(PageContextService.name, () => {
const pageContext = {
user: givenUser,
tradingMode: 'perpetual',
chainId: 1,
instanceName: spotConfig.starkex.instanceName,
collateralAsset: fakeCollateralAsset,
} as const
Expand All @@ -101,6 +110,7 @@ describe(PageContextService.name, () => {
({
user: undefined,
tradingMode: 'perpetual',
chainId: 1,
instanceName: spotConfig.starkex.instanceName,
collateralAsset: fakeCollateralAsset,
} as const)
Expand All @@ -127,6 +137,7 @@ describe(PageContextService.name, () => {
const pageContext = {
user: givenUser,
tradingMode: 'perpetual',
chainId: 1,
instanceName: spotConfig.starkex.instanceName,
collateralAsset: fakeCollateralAsset,
} as const
Expand Down Expand Up @@ -166,6 +177,7 @@ describe(PageContextService.name, () => {
const pageContext = {
user: givenUser,
tradingMode: 'perpetual',
chainId: 1,
instanceName: spotConfig.starkex.instanceName,
collateralAsset: fakeCollateralAsset,
} as const
Expand All @@ -188,6 +200,7 @@ describe(PageContextService.name, () => {
const pageContext = {
user: undefined,
tradingMode: 'perpetual',
chainId: 5,
instanceName: spotConfig.starkex.instanceName,
collateralAsset: fakeCollateralAsset,
} as const
Expand All @@ -205,6 +218,7 @@ describe(PageContextService.name, () => {
const pageContext = {
user: undefined,
tradingMode: 'spot',
chainId: 5,
instanceName: spotConfig.starkex.instanceName,
} as const

Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/core/PageContextService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ export class PageContextService {
user,
tradingMode: this.config.starkex.tradingMode,
instanceName: this.config.starkex.instanceName,
chainId: this.config.starkex.blockchain.chainId,
collateralAsset: this.config.starkex.collateralAsset,
}
}

return {
user,
tradingMode: this.config.starkex.tradingMode,
chainId: this.config.starkex.blockchain.chainId,
instanceName: this.config.starkex.instanceName,
}
}
Expand Down
26 changes: 22 additions & 4 deletions packages/frontend/scripts/metamaskSpy.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,29 @@

const oldRequest = window.ethereum.request
window.ethereum.request = function (...args) {
console.log('request.args', args)
return Promise.resolve(oldRequest.apply(this, args)).then((value) => {
console.log('request.result', value)
console.log('[REQUEST] args', args)
return Promise.resolve(olDRequest.apply(this, args)).then((value) => {
console.log('[REQUEST] result', value)
return value
})
}

// TODO: add send, sendAsync support for dYdX
const oldSend = window.ethereum.send
window.ethereum.send = function (...args) {
console.log('[SEND] args', args)
return Promise.resolve(
oldSend.apply(this, args).then((value) => {
console.log('[SEND] result', value)
return value
})
)
}

const oldSendAsync = window.ethereum.sendAsync
window.ethereum.sendAsync = function (...args) {
console.log('[SEND ASYNC] args', args)
return Promise.resolve(oldSendAsync.apply(this, args)).then((value) => {
console.log('[SEND ASYNC] result', value)
return value
})
}
2 changes: 2 additions & 0 deletions packages/frontend/src/preview/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1571,6 +1571,7 @@ function getPerpetualPageContext(
return {
user,
instanceName: 'dYdX',
chainId: 1,
tradingMode: 'perpetual',
collateralAsset: fakeCollateralAsset,
} as const
Expand All @@ -1593,6 +1594,7 @@ function getSpotPageContext(
return {
user,
instanceName: 'Myria',
chainId: 1,
tradingMode: 'spot',
} as const
}
25 changes: 25 additions & 0 deletions packages/frontend/src/scripts/MetamaskClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export class MetamaskClient {
constructor(
private readonly provider: Provider,
private readonly instanceChainId: number
) {}

async getChainId(): Promise<string> {
return (await this.provider.request({ method: 'eth_chainId' })) as string
}

async switchToInstanceNetwork() {
return await this.switchToNetwork(`0x${this.instanceChainId.toString(16)}`)
}

async switchToNetwork(chainId: `0x${string}`) {
return await this.provider.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId }],
})
}

async requestAccounts() {
return await this.provider.request({ method: 'eth_requestAccounts' })
}
}
23 changes: 19 additions & 4 deletions packages/frontend/src/scripts/keys/keys.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { expect } from 'earl'

import {
getDydxStarkExKeyPairFromData,
getGenericStarkExKeyPairFromData,
getMyriaStarkExKeyPairFromData,
} from './keys'

describe(getDydxStarkExKeyPairFromData.name, () => {
it('correctly calculates the keys', () => {
describe(getGenericStarkExKeyPairFromData.name, () => {
it('correctly calculates the keys for dydx', () => {
const data = '0x12345678'
const pair = getDydxStarkExKeyPairFromData(data)
const pair = getGenericStarkExKeyPairFromData(data)

// Derived using:
// const x = require('@dydxprotocol/starkex-lib')
Expand All @@ -22,6 +22,21 @@ describe(getDydxStarkExKeyPairFromData.name, () => {
'0186532eaed1aa913e4bffc1b64e338cd7ce9c1afe35e395eee28777660dd959',
})
})

it('correctly calculates the keys for apex', () => {
const signature =
'0xde864c207981b865c601a77cda6d669169a20a17980c38f151a4302432efb8013ec02c74c2bf4fb111dadcd9924ea023d6f690512cde22edbb5213487a43ae031c03'
const pair = getGenericStarkExKeyPairFromData(signature)

expect(pair).toEqual({
publicKey:
'07e6d96a3ed5152d2686edc1429b82404135698008d5722bd065df8d0020c447',
publicKeyYCoordinate:
'0356cfb50aa20e625c9b82e2fbadcf3c93dbd8ac8f5d738354f7dc53b522d105',
privateKey:
'0381ed01dd751a7a16f09802527fe85176aed3ab4ca22b1e19592bf64dfd05d6',
})
})
})

describe('getMyriaStarkExKeyPairFromData', () => {
Expand Down
6 changes: 4 additions & 2 deletions packages/frontend/src/scripts/keys/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ export const Registration = z.object({
})

// Follows the same logic as https://github.com/dydxprotocol/starkex-lib
export function getDydxStarkExKeyPairFromData(hexData: string): StarkKeyPair {
const hashedData = keccak256(hexData)
export function getGenericStarkExKeyPairFromData(
signature: string
): StarkKeyPair {
const hashedData = keccak256(signature)
const privateKey = BigInt(hashedData) / 2n ** 5n
const normalized = normalizeHex32(privateKey.toString(16))

Expand Down
4 changes: 2 additions & 2 deletions packages/frontend/src/scripts/keys/recovery.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { EthereumAddress } from '@explorer/types'
import { expect } from 'earl'

import { getDydxStarkExKeyPairFromData } from './keys'
import { getGenericStarkExKeyPairFromData } from './keys'
import { signRegistration } from './recovery'

describe(signRegistration.name, () => {
it('returns correct data', () => {
const pair = getDydxStarkExKeyPairFromData('0x1234')
const pair = getGenericStarkExKeyPairFromData('0x1234')
const ethKey = EthereumAddress('0xdeadbeef12345678deadbeef12345678deadbeef')

const data = signRegistration(ethKey, pair)
Expand Down
16 changes: 13 additions & 3 deletions packages/frontend/src/scripts/keys/recovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { EthereumAddress, StarkKey } from '@explorer/types'

import { Wallet } from '../peripherals/wallet'
import {
getDydxStarkExKeyPairFromData,
getGenericStarkExKeyPairFromData,
getMyriaStarkExKeyPairFromData,
Registration,
signStarkMessage,
Expand All @@ -20,7 +20,7 @@ export async function recoverKeysDydx(
account: EthereumAddress
): Promise<RecoveredKeys> {
const ethSignature = await Wallet.signDydxKey(account)
const keyPair = getDydxStarkExKeyPairFromData(ethSignature + '00')
const keyPair = getGenericStarkExKeyPairFromData(ethSignature + '00')
const registration = signRegistration(account, keyPair)
return { account, starkKey: StarkKey(keyPair.publicKey), registration }
}
Expand All @@ -29,7 +29,7 @@ export async function recoverKeysDydxLegacy(
account: EthereumAddress
): Promise<RecoveredKeys> {
const ethSignature = await Wallet.signDydxKeyLegacy(account)
const keyPair = getDydxStarkExKeyPairFromData(ethSignature + '03')
const keyPair = getGenericStarkExKeyPairFromData(ethSignature + '03')
const registration = signRegistration(account, keyPair)
return { account, starkKey: StarkKey(keyPair.publicKey), registration }
}
Expand All @@ -43,6 +43,16 @@ export async function recoverKeysMyria(
return { account, starkKey: StarkKey(keyPair.publicKey), registration }
}

export async function recoverKeysApex(
account: EthereumAddress,
chainId: number
): Promise<RecoveredKeys> {
const ethSignature = await Wallet.signApexKey(account, chainId)
const keyPair = getGenericStarkExKeyPairFromData(ethSignature + '03')
const registration = signRegistration(account, keyPair)
return { account, starkKey: StarkKey(keyPair.publicKey), registration }
}

export function signRegistration(
account: EthereumAddress,
keyPair: StarkKeyPair
Expand Down
27 changes: 21 additions & 6 deletions packages/frontend/src/scripts/keys/starkKeyRecovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import Cookie from 'js-cookie'
import { RECOVER_STARK_KEY_BUTTON_ID } from '../../view'
import { getUsersInfo } from '../metamask'
import { makeQuery } from '../utils/query'
import { RecoveredKeys, recoverKeysDydx, recoverKeysMyria } from './recovery'
import {
RecoveredKeys,
recoverKeysApex,
recoverKeysDydx,
recoverKeysMyria,
} from './recovery'

export function initStarkKeyRecovery() {
const { $ } = makeQuery(document.body)
Expand All @@ -17,17 +22,25 @@ export function initStarkKeyRecovery() {
if (!registerButton || !account) {
return
}
const instanceName = InstanceName.parse(registerButton.dataset.instanceName)

const { instanceName, chainId } = getDataFromButton(registerButton)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
registerButton.addEventListener('click', async () => {
const keys = await recoverKeys(account, instanceName)
const keys = await recoverKeys(account, instanceName, chainId)
Cookie.set('starkKey', keys.starkKey.toString())
setToLocalStorage(account, keys)
window.location.href = `/users/${keys.starkKey.toString()}`
})
}

const getDataFromButton = (button: HTMLElement) => {
const instanceName = InstanceName.parse(button.dataset.instanceName)
if (button.dataset.chainId === undefined) {
throw new Error('chainId not passed to stark key recovery button')
}

return { instanceName, chainId: Number(button.dataset.chainId) }
}

const setToLocalStorage = (account: EthereumAddress, keys: RecoveredKeys) => {
const accountsMap = getUsersInfo()

Expand All @@ -41,15 +54,17 @@ const setToLocalStorage = (account: EthereumAddress, keys: RecoveredKeys) => {

const recoverKeys = (
account: EthereumAddress,
instanceName: InstanceName
instanceName: InstanceName,
chainId: number
): Promise<RecoveredKeys> => {
switch (instanceName) {
case 'dYdX':
return recoverKeysDydx(account)
case 'Myria':
return recoverKeysMyria(account)
case 'GammaX':
case 'ApeX':
return recoverKeysApex(account, chainId)
case 'GammaX':
//TODO: Implement
throw new Error('NIY')
default:
Expand Down
Loading

0 comments on commit 3acd964

Please sign in to comment.