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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- fixed: (Avalanche) Derive C-Chain addresses at coin type 60 so seeds imported from EVM wallets (Exodus, MetaMask, Trust) produce a matching receive address. Existing wallets keep their cached address and are unaffected.

## 4.82.1 (2026-05-27)

- changed: (FIO) Replace forked `@fioprotocol/fiosdk` with official npm 1.10.3.
Expand Down
4 changes: 3 additions & 1 deletion src/ethereum/info/avalancheInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,9 @@ const networkInfo: EthereumNetworkInfo = {
name: 'AVAX Mainnet'
},
supportsEIP1559: true,
hdPathCoinType: 9000,
// AVAX C-Chain is EVM and standard wallets (Exodus, MetaMask, Trust) derive it
// at Ethereum's coin type so imported seeds yield a matching receive address.
hdPathCoinType: 60,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an incomplete fix. Existing wallets opened on a new device will be derive an address at the new path. One way to resolve is to save the path when creating the wallet and have derive pubkey use it if it's present or fallback to another value if it isn't. I believe we've done this for polkadot tools or another currency tools in the past.

Another solution could be to finally support passing in the path as an optional import option. That would require we save the path in the keys object.

pluginMnemonicKeyName: 'avalancheMnemonic',
pluginRegularKeyName: 'avalancheKey',
evmGasStationUrl: null,
Expand Down
63 changes: 63 additions & 0 deletions test/ethereum/avalancheDerivation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { assert } from 'chai'
import {
EdgeCorePluginOptions,
EdgeCurrencyPlugin,
EdgeCurrencyTools,
makeFakeIo
} from 'edge-core-js'
import { before, describe, it } from 'mocha'
import fetch from 'node-fetch'

import edgeCorePlugins from '../../src/index'
import { fakeLog } from '../fake/fakeLog'

// A 12-word seed and the receive address it yields on EVM wallets (Exodus,
// MetaMask, Trust) which all derive at coin type 60. Ethereum, Polygon and
// Avalanche C-Chain are all EVM, so importing this seed into Edge must produce
// the same address on each. Avalanche was the regression (it derived at coin
// type 9000); Ethereum and Polygon are the chains the report flagged as most
// important, so they are locked here too.
const MNEMONIC =
'room soda device label bicycle hill fork nest lion knee purpose hen'
const EXPECTED_EVM_ADDRESS = '0x21D45Fd06e291C49AbFa135460DE827b6579Cef5'

const makeOpts = (): EdgeCorePluginOptions => {
const fakeIo = makeFakeIo()
return {
infoPayload: {},
initOptions: {},
io: { ...fakeIo, fetch, fetchCors: fetch },
log: fakeLog,
nativeIo: {},
pluginDisklet: fakeIo.disklet
}
}

const CASES = [
{ pluginId: 'ethereum', mnemonicKey: 'ethereumMnemonic' },
{ pluginId: 'polygon', mnemonicKey: 'polygonMnemonic' },
{ pluginId: 'avalanche', mnemonicKey: 'avalancheMnemonic' }
] as const

describe('EVM derivation parity (coin type 60)', function () {
for (const { pluginId, mnemonicKey } of CASES) {
describe(pluginId, function () {
let tools: EdgeCurrencyTools

before('Tools', async function () {
const factory = edgeCorePlugins[pluginId]
const plugin: EdgeCurrencyPlugin = factory(makeOpts())
tools = await plugin.makeCurrencyTools()
})

it('derives the Exodus-matching EVM address from an imported seed', async function () {
const keys = await tools.derivePublicKey({
id: 'id',
keys: { [mnemonicKey]: MNEMONIC },
type: `wallet:${pluginId}`
})
assert.equal(keys.publicKey, EXPECTED_EVM_ADDRESS)
})
})
}
})
Loading