Skip to content
Open
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
13 changes: 8 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
},
"dependencies": {
"@ambire/signature-validator": "^1.5.0",
"@corpus-core/colibri-stateless": "^1.1.30",
"@lifi/types": "^17.7.1",
"@metamask/eth-sig-util": "^8.2.0",
"@safe-global/api-kit": "^4.0.1",
Expand Down
51 changes: 51 additions & 0 deletions src/controllers/domains/domains.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,57 @@ describe('Domains', () => {
expect(domainsController.domainToAddresses[name]?.address).toBe(address)
expect(domainsController.domainToAddresses[name]?.type).toBe('ens')
})
it('should fail Colibri verification for a changed ENS address and succeed for the resolved address', async () => {
const domain = '0xbobby.eth'
const resolvedAddress = getAddress('0x4ba5250000000000000000000000000003bc63d4')
const changedAddress = getAddress('0x0000000000000000000000000000000000000001')
const provider = {} as any
const verificationProvider = {} as any
const getReadyProvider = jest.fn(() => undefined as any)
const { restore } = suppressConsole()
const controller = new DomainsController({
providers: { ['1']: provider },
verification: { getReadyProvider } as any
})
const resolveENSDomainSpy = jest
.spyOn(ensDomainsModule, 'resolveENSDomain')
.mockResolvedValue({ address: resolvedAddress, avatar: null })

try {
await controller.resolveDomain({ domain })

expect(controller.domainToAddresses[domain]?.address).toBe(resolvedAddress)
expect(controller.verifiedDomainsStatus[domain]).toBeUndefined()

getReadyProvider.mockReturnValue(verificationProvider)
controller.domainToAddresses[domain] = {
address: changedAddress,
type: 'ens'
}

await controller.resolveDomain({ domain })

expect(controller.resolveDomainsStatus[domain]).toBeUndefined()
expect(controller.resolveDomainsErrors[domain]).toBe(
`ENS resolution mismatch for ${domain}: RPC returned ${changedAddress}, Colibri returned ${resolvedAddress}`
)
expect(controller.verifiedDomainsStatus[domain]).toBeUndefined()

controller.domainToAddresses[domain] = {
address: resolvedAddress,
type: 'ens'
}

await controller.resolveDomain({ domain })

expect(controller.resolveDomainsStatus[domain]).toBeUndefined()
expect(controller.resolveDomainsErrors[domain]).toBeUndefined()
expect(controller.verifiedDomainsStatus[domain]).toBe('VERIFIED')
} finally {
resolveENSDomainSpy.mockRestore()
restore()
}
})
it(`reverse lookup should expire after ${
PERSIST_DOMAIN_FOR_IN_MS / 1000 / 60
} min, if the lookup succeeds (the happy case)`, async () => {
Expand Down
117 changes: 113 additions & 4 deletions src/controllers/domains/domains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getAddress, isAddress } from 'ethers'
import { IDomainsController } from '../../interfaces/domains'
import { IEventEmitterRegistryController } from '../../interfaces/eventEmitter'
import { RPCProviders } from '../../interfaces/provider'
import { IVerificationController } from '../../interfaces/verification'
import {
getEnsAvatar,
getIsNamoshiDomain,
Expand Down Expand Up @@ -34,6 +35,15 @@ interface Domains {
// 15 minutes
export const PERSIST_DOMAIN_FOR_IN_MS = 15 * 60 * 1000
export const PERSIST_DOMAIN_FOR_FAILED_LOOKUP_IN_MS = 5 * 60 * 1000 // 5 minutes
const USER_FACING_RESOLUTION_ERROR_PREFIX = 'ENS resolution mismatch for'

const getUserFacingResolutionError = (error: any) => {
const message = error?.message
if (typeof message !== 'string') return undefined
if (!message.startsWith(USER_FACING_RESOLUTION_ERROR_PREFIX)) return undefined

return message
}

/**
* Domains controller- responsible for handling the reverse lookup of addresses to ENS names.
Expand All @@ -42,6 +52,8 @@ export const PERSIST_DOMAIN_FOR_FAILED_LOOKUP_IN_MS = 5 * 60 * 1000 // 5 minutes
export class DomainsController extends EventEmitter implements IDomainsController {
#providers: RPCProviders = {}

#verification?: IVerificationController

#defaultNetworksMode: 'mainnet' | 'testnet' = 'mainnet'

/** Stores ENS names, avatars, and metadata (timestamps) indexed by account address */
Expand All @@ -63,23 +75,84 @@ export class DomainsController extends EventEmitter implements IDomainsControlle

resolveDomainsStatus: { [domain: string]: 'LOADING' | 'RESOLVED' | 'FAILED' | undefined } = {}

resolveDomainsErrors: { [domain: string]: string | undefined } = {}

verifiedDomainsStatus: { [domain: string]: 'VERIFIED' | undefined } = {}

#reverseLookupPromises: { [address: string]: Promise<void> | undefined } = {}

constructor({
eventEmitterRegistry,
providers,
verification,
defaultNetworksMode
}: {
eventEmitterRegistry?: IEventEmitterRegistryController
providers: RPCProviders
verification?: IVerificationController
defaultNetworksMode?: 'mainnet' | 'testnet'
}) {
super(eventEmitterRegistry)

this.#providers = providers
this.#verification = verification
if (defaultNetworksMode) this.#defaultNetworksMode = defaultNetworksMode
}

async #verifyEnsResolution({
providerChainId,
domain,
address,
isNamoshiDomain
}: {
providerChainId: string
domain: string
address: string
isNamoshiDomain: boolean
}) {
if (isNamoshiDomain) return false

const verificationProvider = this.#verification?.getReadyProvider(BigInt(providerChainId))
if (!verificationProvider) return false

const verifiedResult = await withTimeout(
() =>
resolveENSDomain({
provider: verificationProvider,
domain,
options: { isNamoshiDomain }
}),
{ timeoutMs: 15000 }
)

if (!verifiedResult.address && !address) return false
if (
verifiedResult.address &&
address &&
getAddress(verifiedResult.address) === getAddress(address)
) {
return true
}

throw new Error(
`ENS resolution mismatch for ${domain}: RPC returned ${address}, Colibri returned ${verifiedResult.address}`
)
}

#setResolveDomainFailure(domain: string, error: any) {
const message = getUserFacingResolutionError(error)

if (message) {
this.resolveDomainsErrors = {
...this.resolveDomainsErrors,
[domain]: message
}
} else {
delete this.resolveDomainsErrors[domain]
}
this.resolveDomainsStatus[domain] = 'FAILED'
}

async batchReverseLookup(addresses: string[]) {
const normalizedAddresses = this.#normalizeAddresses(addresses)
const addressesToLookup = this.#getAddressesToLookup(normalizedAddresses)
Expand Down Expand Up @@ -142,12 +215,34 @@ export class DomainsController extends EventEmitter implements IDomainsControlle
}

this.resolveDomainsStatus[domain] = 'LOADING'
delete this.resolveDomainsErrors[domain]
await this.forceEmitUpdate()

if (this.domainToAddresses[domain]) {
this.resolveDomainsStatus[domain] = 'RESOLVED'
await this.forceEmitUpdate()
this.resolveDomainsStatus[domain] = undefined
try {
if (this.domainToAddresses[domain]?.address) {
const isEnsVerifiedByColibri = await this.#verifyEnsResolution({
providerChainId,
domain,
address: this.domainToAddresses[domain]!.address!,
isNamoshiDomain
})
if (isEnsVerifiedByColibri) this.verifiedDomainsStatus[domain] = 'VERIFIED'
}

this.resolveDomainsStatus[domain] = 'RESOLVED'
await this.forceEmitUpdate()
this.resolveDomainsStatus[domain] = undefined
} catch (e: any) {
this.emitError({
error: e,
message: `ENS resolution failed for ${domain}: ${e?.message || e}`,
level: 'silent'
})
this.#setResolveDomainFailure(domain, e)
await this.forceEmitUpdate()
this.resolveDomainsStatus[domain] = undefined
}
return
}

Expand All @@ -158,6 +253,14 @@ export class DomainsController extends EventEmitter implements IDomainsControlle
})
.then(async ({ address, avatar }) => {
if (address) {
const isEnsVerifiedByColibri = await this.#verifyEnsResolution({
providerChainId,
domain,
address,
isNamoshiDomain
})
if (isEnsVerifiedByColibri) this.verifiedDomainsStatus[domain] = 'VERIFIED'

this.domainToAddresses[domain] = {
address: getAddress(address),
type: isNamoshiDomain ? 'namoshi' : 'ens'
Expand All @@ -170,12 +273,18 @@ export class DomainsController extends EventEmitter implements IDomainsControlle
})
}
this.resolveDomainsStatus[domain] = 'RESOLVED'
delete this.resolveDomainsErrors[domain]
await this.forceEmitUpdate()
this.resolveDomainsStatus[domain] = undefined
})
.catch(async (e) => {
console.error(`Failed to resolve ENS domain: ${domain}`, e)
this.resolveDomainsStatus[domain] = 'FAILED'
this.emitError({
error: e,
message: `ENS resolution failed for ${domain}: ${e?.message || e}`,
level: 'silent'
})
this.#setResolveDomainFailure(domain, e)
await this.forceEmitUpdate()
this.resolveDomainsStatus[domain] = undefined
})
Expand Down
17 changes: 15 additions & 2 deletions src/controllers/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { TransactionManagerController } from '@/controllers/transaction/transact
import { TransferController } from '@/controllers/transfer/transfer'
import { TransfersScannerController } from '@/controllers/transfersScanner/transfersScanner'
import { UiController } from '@/controllers/ui/ui'
import { VerificationController } from '@/controllers/verification/verification'
import { Account, IAccountsController } from '@/interfaces/account'
import { IAccountPickerController } from '@/interfaces/accountPicker'
import { IActivityController } from '@/interfaces/activity'
Expand Down Expand Up @@ -88,6 +89,7 @@ import { ITransferController } from '@/interfaces/transfer'
import { ITransfersScannerController } from '@/interfaces/transferScanner'
import { IUiController, UiManager, View } from '@/interfaces/ui'
import { BenzinUserRequest, CallsUserRequest } from '@/interfaces/userRequest'
import { IVerificationController } from '@/interfaces/verification'
import { getDefaultSelectedAccount } from '@/libs/account/account'
import { AccountOp } from '@/libs/accountOp/accountOp'
import {
Expand Down Expand Up @@ -150,6 +152,8 @@ export class MainController extends EventEmitter implements IMainController {

providers: IProvidersController

verification: IVerificationController

accountPicker: IAccountPickerController

portfolio: IPortfolioController
Expand Down Expand Up @@ -285,7 +289,10 @@ export class MainController extends EventEmitter implements IMainController {
},
onAddOrUpdateNetworks: async (networks: Network[]) => {
networks.forEach((n) => n.disabled && this.removeNetworkData(n.chainId))
networks.filter((net) => !net.disabled).forEach((n) => this.providers.setProvider(n))
await Promise.all(
networks.filter((net) => !net.disabled).map((n) => this.providers.setProvider(n))
)
this.verification?.updateNetworks(networks)
await this.reloadSelectedAccount({ chainIds: networks.map((n) => n.chainId) })
},
onReady: async () => {
Expand All @@ -299,6 +306,10 @@ export class MainController extends EventEmitter implements IMainController {
getNetworks: () => this.networks.allNetworks,
sendUiMessage: this.ui.message.sendUiMessage
})
this.verification = new VerificationController({
eventEmitterRegistry,
networks: this.networks
})
this.accounts = new AccountsController(
this.storage,
this.providers,
Expand Down Expand Up @@ -393,7 +404,8 @@ export class MainController extends EventEmitter implements IMainController {
velcroUrl,
this.banner,
this.featureFlags,
eventEmitterRegistry
eventEmitterRegistry,
this.verification
)
if (this.featureFlags.isFeatureEnabled('withEmailVaultController')) {
this.emailVault = new EmailVaultController(
Expand Down Expand Up @@ -563,6 +575,7 @@ export class MainController extends EventEmitter implements IMainController {
this.domains = new DomainsController({
eventEmitterRegistry,
providers: this.providers.providers,
verification: this.verification,
defaultNetworksMode: this.networks.defaultNetworksMode
})

Expand Down
3 changes: 2 additions & 1 deletion src/controllers/networks/networks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,9 +489,10 @@ export class NetworksController extends EventEmitter implements INetworksControl
...changedNetwork
}

if (!skipUpdate) void this.#onAddOrUpdateNetworks([this.#networks[chainId.toString()]!])
await this.#storage.set('networks', this.#networks)

if (!skipUpdate) void this.#onAddOrUpdateNetworks([this.#networks[chainId.toString()]!])

const checkRPC = async (
networkToAddOrUpdate: {
chainId: bigint
Expand Down
Loading