Skip to content

Commit

Permalink
Merge pull request #528 from xmtp/rygine/ethers-to-viem
Browse files Browse the repository at this point in the history
Replace `ethers` with `viem`
  • Loading branch information
rygine authored Feb 28, 2024
2 parents 6579a0d + 1a3500d commit 410be93
Show file tree
Hide file tree
Showing 20 changed files with 323 additions and 124 deletions.
19 changes: 7 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,22 +57,16 @@
"browser": "dist/browser/index.js",
"types": "dist/index.d.ts",
"files": [
"dist/index.cjs",
"dist/index.cjs.map",
"dist/index.d.ts",
"dist/index.js",
"dist/index.js.map",
"dist/browser/index.js",
"dist/browser/index.js.map",
"dist/bundler/index.js",
"dist/bundler/index.js.map"
"dist",
"src",
"tsconfig.json"
],
"scripts": {
"autolint": "prettier --write . && eslint --fix .",
"bench": "yarn build:bench && node dist/bench/index.cjs",
"build": "yarn clean:dist && rollup -c",
"build:bench": "rollup -c rollup.config.bench.js",
"build:docs": "yarn clean:docs && mkdir -p tmp && cp README.md tmp/ && sed -i.bak '/badge.svg/d' tmp/README.md && typedoc --excludePrivate --readme tmp/README.md src/index.ts",
"build:docs": "yarn clean:docs && mkdir -p tmp && cp README.md tmp/ && sed -i.bak '/badge.svg/d' tmp/README.md && typedoc",
"clean": "yarn clean:artifacts && yarn clean:dist && yarn clean:docs && yarn clean:deps",
"clean:artifacts": "rm -rf docs tmp package.tgz",
"clean:deps": "rm -rf node_modules",
Expand Down Expand Up @@ -108,9 +102,8 @@
"@xmtp/user-preferences-bindings-wasm": "^0.3.6",
"async-mutex": "^0.4.1",
"elliptic": "^6.5.4",
"ethers": "^5.7.2",
"long": "^5.2.3",
"viem": "^1.21.4"
"viem": "^2.7.8"
},
"devDependencies": {
"@commitlint/cli": "17.8.1",
Expand All @@ -126,6 +119,7 @@
"@types/node": "^18.19.18",
"@typescript-eslint/eslint-plugin": "^7.1.0",
"@typescript-eslint/parser": "^7.1.0",
"@vitest/coverage-v8": "^1.3.1",
"@xmtp/rollup-plugin-resolve-extensions": "1.0.1",
"benny": "^3.7.1",
"dd-trace": "^5.5.0",
Expand All @@ -138,6 +132,7 @@
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-promise": "^6.1.1",
"ethers": "^5.7.2",
"happy-dom": "^13.6.2",
"husky": "^7.0.4",
"prettier": "^3.2.5",
Expand Down
2 changes: 1 addition & 1 deletion rollup.config.bench.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ const external = [
'benny',
'crypto',
'elliptic',
'ethers',
'long',
'viem',
]

const plugins = [
Expand Down
1 change: 0 additions & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ const external = [
'async-mutex',
'crypto',
'elliptic',
'ethers',
'long',
'viem',
]
Expand Down
13 changes: 6 additions & 7 deletions src/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
EnvelopeMapperWithMessage,
EnvelopeWithMessage,
} from './utils'
import { utils } from 'ethers'
import { Signer } from './types/Signer'
import { Conversations } from './conversations'
import { ContentTypeText, TextCodec } from './codecs/Text'
Expand Down Expand Up @@ -44,7 +43,7 @@ import {
import { hasMetamaskWithSnaps } from './keystore/snapHelpers'
import { packageName, version } from './snapInfo.json'
import { ExtractDecodedType } from './types/client'
import type { WalletClient } from 'viem'
import { getAddress, type WalletClient } from 'viem'
import { Contacts } from './Contacts'
import { KeystoreInterfaces } from './keystore/rpcDefinitions'
const { Compression } = proto
Expand Down Expand Up @@ -430,7 +429,7 @@ export default class Client<ContentTypes = any> {
async getUserContact(
peerAddress: string
): Promise<PublicKeyBundle | SignedPublicKeyBundle | undefined> {
peerAddress = utils.getAddress(peerAddress) // EIP55 normalize the address case.
peerAddress = getAddress(peerAddress) // EIP55 normalize the address case.
const existingBundle = this.knownPublicKeyBundles.get(peerAddress)
if (existingBundle) {
return existingBundle
Expand All @@ -456,7 +455,7 @@ export default class Client<ContentTypes = any> {
): Promise<(PublicKeyBundle | SignedPublicKeyBundle | undefined)[]> {
// EIP55 normalize all peer addresses
const normalizedAddresses = peerAddresses.map((address) =>
utils.getAddress(address)
getAddress(address)
)
// The logic here is tricky because we need to do a batch query for any uncached bundles,
// then interleave back into an ordered array. So we create a map<string, keybundle|undefined>
Expand Down Expand Up @@ -501,7 +500,7 @@ export default class Client<ContentTypes = any> {
* Used to force getUserContact fetch contact from the network.
*/
forgetContact(peerAddress: string) {
peerAddress = utils.getAddress(peerAddress) // EIP55 normalize the address case.
peerAddress = getAddress(peerAddress) // EIP55 normalize the address case.
this.knownPublicKeyBundles.delete(peerAddress)
}

Expand Down Expand Up @@ -552,7 +551,7 @@ export default class Client<ContentTypes = any> {
const rawPeerAddresses: string[] = peerAddress
// Try to normalize each of the peer addresses
const normalizedPeerAddresses = rawPeerAddresses.map((address) =>
utils.getAddress(address)
getAddress(address)
)
// The getUserContactsFromNetwork will return false instead of throwing
// on invalid envelopes
Expand All @@ -563,7 +562,7 @@ export default class Client<ContentTypes = any> {
return contacts.map((contact) => !!contact)
}
try {
peerAddress = utils.getAddress(peerAddress) // EIP55 normalize the address case.
peerAddress = getAddress(peerAddress) // EIP55 normalize the address case.
} catch (e) {
return false
}
Expand Down
3 changes: 1 addition & 2 deletions src/authn/LocalAuthenticator.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { authn, signature, publicKey } from '@xmtp/proto'
import AuthData from './AuthData'
import { PrivateKey } from '../crypto'
import { hexToBytes } from '../crypto/utils'
import Token from './Token'
import { keccak256 } from 'viem'
import { hexToBytes, keccak256 } from 'viem'

export default class LocalAuthenticator {
private identityKey: PrivateKey
Expand Down
4 changes: 2 additions & 2 deletions src/conversations/Conversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
concat,
toNanoString,
} from '../utils'
import { utils } from 'ethers'
import Stream from '../Stream'
import Client, {
ListMessagesOptions,
Expand All @@ -33,6 +32,7 @@ import { sha256 } from '../crypto/encryption'
import { buildDecryptV1Request, getResultOrThrow } from '../utils/keystore'
import { ContentTypeText } from '../codecs/Text'
import { ConsentState } from '../Contacts'
import { getAddress } from 'viem'

/**
* Conversation represents either a V1 or V2 conversation with a common set of methods.
Expand Down Expand Up @@ -178,7 +178,7 @@ export class ConversationV1<ContentTypes>
private client: Client<ContentTypes>

constructor(client: Client<ContentTypes>, address: string, createdAt: Date) {
this.peerAddress = utils.getAddress(address)
this.peerAddress = getAddress(address)
this.client = client
this.createdAt = createdAt
}
Expand Down
19 changes: 7 additions & 12 deletions src/crypto/PublicKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { publicKey } from '@xmtp/proto'
import * as secp from '@noble/secp256k1'
import Long from 'long'
import Signature, { WalletSigner } from './Signature'
import { equalBytes, hexToBytes } from './utils'
import { utils } from 'ethers'
import { computeAddress, equalBytes, splitSignature } from './utils'
import { Signer } from '../types/Signer'
import { sha256 } from './encryption'
import { hashMessage, Hex, hexToBytes } from 'viem'

// SECP256k1 public key in uncompressed format with prefix
type secp256k1Uncompressed = {
Expand Down Expand Up @@ -90,7 +90,7 @@ export class UnsignedPublicKey implements publicKey.UnsignedPublicKey {

// Derive Ethereum address from this public key.
getEthereumAddress(): string {
return utils.computeAddress(this.secp256k1Uncompressed.bytes)
return computeAddress(this.secp256k1Uncompressed.bytes)
}

// Encode public key into bytes.
Expand Down Expand Up @@ -256,16 +256,11 @@ export class PublicKey
const sigString = await wallet.signMessage(
WalletSigner.identitySigRequestText(this.bytesToSign())
)
const eSig = utils.splitSignature(sigString)
const r = hexToBytes(eSig.r)
const s = hexToBytes(eSig.s)
const sigBytes = new Uint8Array(64)
sigBytes.set(r)
sigBytes.set(s, r.length)
const { bytes, recovery } = splitSignature(sigString as Hex)
this.signature = new Signature({
ecdsaCompact: {
bytes: sigBytes,
recovery: eSig.recoveryParam,
bytes,
recovery,
},
})
}
Expand All @@ -278,7 +273,7 @@ export class PublicKey
throw new Error('key is not signed')
}
const digest = hexToBytes(
utils.hashMessage(WalletSigner.identitySigRequestText(this.bytesToSign()))
hashMessage(WalletSigner.identitySigRequestText(this.bytesToSign()))
)
const pk = this.signature.getPublicKey(digest)
if (!pk) {
Expand Down
17 changes: 6 additions & 11 deletions src/crypto/Signature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import Long from 'long'
import * as secp from '@noble/secp256k1'
import { PublicKey, UnsignedPublicKey, SignedPublicKey } from './PublicKey'
import { SignedPrivateKey } from './PrivateKey'
import { utils } from 'ethers'
import { Signer } from '../types/Signer'
import { bytesToHex, equalBytes, hexToBytes } from './utils'
import { bytesToHex, equalBytes, splitSignature } from './utils'
import { Hex, hashMessage, hexToBytes } from 'viem'

// ECDSA signature with recovery bit.
export type ECDSACompactWithRecovery = {
Expand Down Expand Up @@ -164,7 +164,7 @@ export class WalletSigner implements KeySigner {
signature: ECDSACompactWithRecovery
): UnsignedPublicKey | undefined {
const digest = hexToBytes(
utils.hashMessage(this.identitySigRequestText(key.bytesToSign()))
hashMessage(this.identitySigRequestText(key.bytesToSign()))
)
return ecdsaSignerKey(digest, signature)
}
Expand All @@ -174,16 +174,11 @@ export class WalletSigner implements KeySigner {
const sigString = await this.wallet.signMessage(
WalletSigner.identitySigRequestText(keyBytes)
)
const eSig = utils.splitSignature(sigString)
const r = hexToBytes(eSig.r)
const s = hexToBytes(eSig.s)
const sigBytes = new Uint8Array(64)
sigBytes.set(r)
sigBytes.set(s, r.length)
const { bytes, recovery } = splitSignature(sigString as Hex)
const signature = new Signature({
walletEcdsaCompact: {
bytes: sigBytes,
recovery: eSig.recoveryParam,
bytes,
recovery,
},
})
return new SignedPublicKey({ keyBytes, signature })
Expand Down
48 changes: 36 additions & 12 deletions src/crypto/utils.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
import * as secp from '@noble/secp256k1'
import {
Hex,
getAddress,
hexToSignature,
keccak256,
hexToBytes,
bytesToHex as viemBytesToHex,
} from 'viem'

export const bytesToHex = secp.utils.bytesToHex

export function hexToBytes(s: string): Uint8Array {
if (s.startsWith('0x')) {
s = s.slice(2)
}
const bytes = new Uint8Array(s.length / 2)
for (let i = 0; i < bytes.length; i++) {
const j = i * 2
bytes[i] = Number.parseInt(s.slice(j, j + 2), 16)
}
return bytes
}

export function bytesToBase64(bytes: Uint8Array): string {
return Buffer.from(bytes).toString('base64')
}
Expand All @@ -29,3 +25,31 @@ export function equalBytes(b1: Uint8Array, b2: Uint8Array): boolean {
}
return true
}

/**
* Compute the Ethereum address from uncompressed PublicKey bytes
*/
export function computeAddress(bytes: Uint8Array) {
const publicKey = viemBytesToHex(bytes.slice(1)) as Hex
const hash = keccak256(publicKey)
const address = hash.substring(hash.length - 40)
return getAddress(`0x${address}`)
}

/**
* Split an Ethereum signature hex string into bytes and a recovery bit
*/
export function splitSignature(signature: Hex) {
const eSig = hexToSignature(signature)
const r = hexToBytes(eSig.r)
const s = hexToBytes(eSig.s)
let v = Number(eSig.v)
if (v === 0 || v === 1) {
v += 27
}
const recovery = 1 - (v % 2)
const bytes = new Uint8Array(64)
bytes.set(r)
bytes.set(s, r.length)
return { bytes, recovery }
}
31 changes: 16 additions & 15 deletions src/keystore/providers/NetworkKeyManager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { utils } from 'ethers'
import { Signer } from '../../types/Signer'
import crypto from '../../crypto/crypto'
import {
Expand All @@ -10,10 +9,11 @@ import {
} from '../../crypto'
import type { PreEventCallback } from '../../Client'
import { LocalAuthenticator } from '../../authn'
import { bytesToHex, hexToBytes } from '../../crypto/utils'
import { bytesToHex } from '../../crypto/utils'
import Ciphertext from '../../crypto/Ciphertext'
import { privateKey as proto } from '@xmtp/proto'
import TopicPersistence from '../persistence/TopicPersistence'
import { Hex, getAddress, hexToBytes, verifyMessage } from 'viem'

const KEY_BUNDLE_NAME = 'key_bundle'
/**
Expand All @@ -39,7 +39,7 @@ export default class NetworkKeyManager {
// I think we want to namespace the storage address by wallet
// This will allow us to support switching between multiple wallets in the same browser
let walletAddress = await this.signer.getAddress()
walletAddress = utils.getAddress(walletAddress)
walletAddress = getAddress(walletAddress)
return `${walletAddress}/${name}`
}

Expand Down Expand Up @@ -91,24 +91,23 @@ export default class NetworkKeyManager {
if (this.preEnableIdentityCallback) {
await this.preEnableIdentityCallback()
}
let sig = await wallet.signMessage(input)
const sig = await wallet.signMessage(input)

// Check that the signature is correct, was created using the expected
// input, and retry if not. This mitigates a bug in interacting with
// LedgerLive for iOS, where the previous signature response is
// returned in some cases.
let address = utils.verifyMessage(input, sig)
if (address !== walletAddr) {
sig = await wallet.signMessage(input)
console.log('invalid signature, retrying')

address = utils.verifyMessage(input, sig)
if (address !== walletAddr) {
throw new Error('invalid signature')
}
const valid = verifyMessage({
address: walletAddr as `0x${string}`,
message: input,
signature: sig as Hex,
})

if (!valid) {
throw new Error('invalid signature')
}

const secret = hexToBytes(sig)
const secret = hexToBytes(sig as Hex)
const ciphertext = await encrypt(bytes, secret)
return proto.EncryptedPrivateKeyBundle.encode({
v1: {
Expand Down Expand Up @@ -136,7 +135,9 @@ export default class NetworkKeyManager {
await this.preEnableIdentityCallback()
}
const secret = hexToBytes(
await wallet.signMessage(storageSigRequestText(eBundle.walletPreKey))
(await wallet.signMessage(
storageSigRequestText(eBundle.walletPreKey)
)) as Hex
)

// Ledger uses the last byte = v=[0,1,...] but Metamask and other wallets generate with
Expand Down
Loading

0 comments on commit 410be93

Please sign in to comment.