Skip to content

Commit

Permalink
add Zod validation of state chain objects, infer types (#1485)
Browse files Browse the repository at this point in the history
* add Zod validation of state chain objects, infer types

* move state files to TS, add more inferred types

* fix persist mock for tests

* add types to main state

* update test

* add mute notification schema

* move type export to state main

* move legacy file to TS

* try new migration format

* update legacy migrations and tests

* fix gas fees type

* fix gas type

* move legacy mapping

* final migration prototype

* finish migration poc

* finish cleaning up migrations

* test cleanup

* fix compilation error

* fix state parsing
  • Loading branch information
mholtzman committed Apr 19, 2023
1 parent 213498b commit ad0c8fd
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 77 deletions.
43 changes: 0 additions & 43 deletions main/store/migrate/migrations/35.ts

This file was deleted.

110 changes: 110 additions & 0 deletions main/store/migrate/migrations/35/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { z } from 'zod'
import log from 'electron-log'

import {
v35Chain,
v35ChainSchema,
v35ChainsSchema,
v35Connection,
v35MainSchema,
v35StateSchema
} from './schema'

const pylonChainIds = ['1', '5', '10', '137', '42161', '11155111']
const retiredChainIds = ['3', '4', '42']
const chainsToMigrate = [...pylonChainIds, ...retiredChainIds]

// because this is the first migration that uses Zod parsing and validation,
// create a version of the schema that removes invalid chains, allowing them to
// also be "false" so that we can filter them out later in a transform. future migrations
// that use this schema can be sure that the chains are all valid afterwards
const ParsedChainSchema = z.union([v35ChainSchema, z.boolean()]).catch(false)

const EthereumChainsSchema = z.record(z.coerce.number(), ParsedChainSchema).transform((chains) => {
// remove any chains that failed to parse, which will now be set to "false"
// TODO: we can insert default chain data here from the state defaults in the future
return Object.fromEntries(
Object.entries(chains).filter(([id, chain]) => {
if (chain === false) {
log.info(`Migration 35: removing invalid chain ${id} from state`)
return false
}

return true
})
)
})

const ChainsSchema = v35ChainsSchema.merge(
z.object({
ethereum: EthereumChainsSchema
})
)

const MainSchema = v35MainSchema
.merge(
z.object({
networks: ChainsSchema
})
)
.passthrough()

const StateSchema = v35StateSchema.merge(z.object({ main: MainSchema }))

const migrate = (initial: unknown) => {
let showMigrationWarning = false

const updateChain = (chain: v35Chain) => {
const removeRpcConnection = (connection: v35Connection) => {
const isServiceRpc = connection.current === 'infura' || connection.current === 'alchemy'

if (isServiceRpc) {
log.info(`Migration 35: removing ${connection.current} preset from chain ${chain.id}`)
showMigrationWarning = true
}

return {
...connection,
current: isServiceRpc ? 'custom' : connection.current,
custom: isServiceRpc ? '' : connection.custom
}
}

const { primary, secondary } = chain.connection

const updatedChain = {
...chain,
connection: {
...chain.connection,
primary: removeRpcConnection(primary),
secondary: removeRpcConnection(secondary)
}
}

return updatedChain
}

try {
const state = StateSchema.parse(initial)

const chainEntries = Object.entries(state.main.networks.ethereum)

const migratedChains = chainEntries
.filter(([id]) => chainsToMigrate.includes(id))
.map(([id, chain]) => [id, updateChain(chain as v35Chain)])

state.main.networks.ethereum = Object.fromEntries([...chainEntries, ...migratedChains])
state.main.mute.migrateToPylon = !showMigrationWarning

return state
} catch (e) {
log.error('Migration 35: could not parse state', e)
}

return initial
}

export default {
version: 35,
migrate
}
47 changes: 47 additions & 0 deletions main/store/migrate/migrations/35/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { z } from 'zod'

const v35MuteSchema = z
.object({
migrateToPylon: z.boolean().default(false)
})
.passthrough()
.default({})

const v35ConnectionSchema = z
.object({
current: z.enum(['local', 'custom', 'infura', 'alchemy', 'poa']),
custom: z.string().default('')
})
.passthrough()

export const v35ChainSchema = z
.object({
id: z.coerce.number(),
connection: z.object({
primary: v35ConnectionSchema,
secondary: v35ConnectionSchema
})
})
.passthrough()

const EthereumChainsSchema = z.record(z.coerce.number(), v35ChainSchema)

export const v35ChainsSchema = z.object({
ethereum: EthereumChainsSchema
})

export const v35MainSchema = z
.object({
networks: v35ChainsSchema,
mute: v35MuteSchema
})
.passthrough()

export const v35StateSchema = z
.object({
main: v35MainSchema
})
.passthrough()

export type v35Connection = z.infer<typeof v35ConnectionSchema>
export type v35Chain = z.infer<typeof v35ChainSchema>
26 changes: 0 additions & 26 deletions main/store/migrate/migrations/36.ts

This file was deleted.

48 changes: 48 additions & 0 deletions main/store/migrate/migrations/36/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import log from 'electron-log'

import { v35Connection, v35StateSchema } from '../35/schema'

function removePoaConnection(connection: v35Connection) {
// remove Gnosis chain preset
const isPoa = connection.current === 'poa'

if (isPoa) {
log.info('Migration 36: removing POA presets from Gnosis chain')
}

return {
...connection,
current: isPoa ? 'custom' : connection.current,
custom: isPoa ? 'https://rpc.gnosischain.com' : connection.custom
}
}

const migrate = (initial: unknown) => {
try {
const state = v35StateSchema.parse(initial)
const gnosisChainPresent = '100' in state.main.networks.ethereum

if (gnosisChainPresent) {
const gnosisChain = state.main.networks.ethereum[100]

state.main.networks.ethereum[100] = {
...gnosisChain,
connection: {
primary: removePoaConnection(gnosisChain.connection.primary),
secondary: removePoaConnection(gnosisChain.connection.secondary)
}
}
}

return state
} catch (e) {
log.error('Migration 36: could not parse state', e)
}

return initial
}

export default {
version: 36,
migrate
}
1 change: 0 additions & 1 deletion main/store/migrate/types.ts

This file was deleted.

20 changes: 13 additions & 7 deletions test/main/store/migrate/migrations/36.test.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,34 @@
import migrate from '../../../../../main/store/migrate/migrations/36'
import migration from '../../../../../main/store/migrate/migrations/36'
import { createState } from '../setup'

let state

beforeEach(() => {
state = createState()
state = createState(migration.version - 1)

state.main.networks.ethereum = {
100: {
id: 100,
connection: {
primary: { current: 'custom' },
secondary: { current: 'local' }
primary: { current: 'custom', custom: 'myrpc' },
secondary: { current: 'local', custom: '' }
}
}
}
})

it('should have migration version 36', () => {
const { version } = migration
expect(version).toBe(36)
})

const connectionPriorities = ['primary', 'secondary']

connectionPriorities.forEach((priority) => {
it(`updates a ${priority} Gnosis connection`, () => {
state.main.networks.ethereum[100].connection[priority].current = 'poa'

const updatedState = migrate(state)
const updatedState = migration.migrate(state)
const gnosis = updatedState.main.networks.ethereum[100]

expect(gnosis.connection[priority].current).toBe('custom')
Expand All @@ -33,7 +39,7 @@ connectionPriorities.forEach((priority) => {
state.main.networks.ethereum[100].connection[priority].current = 'custom'
state.main.networks.ethereum[100].connection[priority].custom = 'https://myconnection.io'

const updatedState = migrate(state)
const updatedState = migration.migrate(state)
const gnosis = updatedState.main.networks.ethereum[100]

expect(gnosis.connection[priority].current).toBe('custom')
Expand All @@ -44,7 +50,7 @@ connectionPriorities.forEach((priority) => {
it('takes no action if no Gnosis chain is present', () => {
delete state.main.networks.ethereum[100]

const updatedState = migrate(state)
const updatedState = migration.migrate(state)

expect(updatedState.main.networks).toStrictEqual({ ethereum: {} })
})

0 comments on commit ad0c8fd

Please sign in to comment.