diff --git a/example/src/tests.ts b/example/src/tests.ts index 814a091b3..1dff64244 100644 --- a/example/src/tests.ts +++ b/example/src/tests.ts @@ -869,3 +869,59 @@ test('returns keyMaterial for conversations', async () => { return true }) + +test('correctly handles lowercase addresses', async () => { + const bob = await Client.createRandom({ env: 'local' }) + await delayToPropogate() + const alice = await Client.createRandom({ env: 'local' }) + await delayToPropogate() + if (bob.address === alice.address) { + throw new Error('bob and alice should be different') + } + + const bobConversation = await bob.conversations.newConversation( + alice.address.toLocaleLowerCase() + ) + await delayToPropogate() + if (!bobConversation) { + throw new Error('bobConversation should exist') + } + const aliceConversation = (await alice.conversations.list())[0] + if (!aliceConversation) { + throw new Error('aliceConversation should exist') + } + + await bob.contacts.deny([aliceConversation.peerAddress.toLocaleLowerCase()]) + await delayToPropogate() + const deniedState = await bob.contacts.isDenied(aliceConversation.peerAddress) + const allowedState = await bob.contacts.isAllowed( + aliceConversation.peerAddress + ) + if (!deniedState) { + throw new Error(`contacts denied by bo should be denied not ${deniedState}`) + } + + if (allowedState) { + throw new Error( + `contacts denied by bo should be denied not ${allowedState}` + ) + } + const deniedLowercaseState = await bob.contacts.isDenied( + aliceConversation.peerAddress.toLocaleLowerCase() + ) + const allowedLowercaseState = await bob.contacts.isAllowed( + aliceConversation.peerAddress.toLocaleLowerCase() + ) + if (!deniedLowercaseState) { + throw new Error( + `contacts denied by bo should be denied not ${deniedLowercaseState}` + ) + } + + if (allowedLowercaseState) { + throw new Error( + `contacts denied by bo should be denied not ${allowedLowercaseState}` + ) + } + return true +}) diff --git a/package.json b/package.json index c21ef743b..24ebc7f2b 100644 --- a/package.json +++ b/package.json @@ -46,8 +46,10 @@ "dependencies": { "@ethersproject/bytes": "^5.7.0", "@msgpack/msgpack": "^3.0.0-beta2", + "@noble/hashes": "^1.3.3", "@xmtp/proto": "^3.25.0", - "buffer": "^6.0.3" + "buffer": "^6.0.3", + "text-encoding": "^0.7.0" }, "devDependencies": { "@babel/plugin-proposal-export-namespace-from": "^7.18.9", diff --git a/src/index.ts b/src/index.ts index b2039c9ef..7d0e2854f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,7 @@ import { import { Conversation } from './lib/Conversation' import { DecodedMessage } from './lib/DecodedMessage' import type { Query } from './lib/Query' +import { getAddress } from './utils/address' export { ReactionCodec } from './lib/NativeCodecs/ReactionCodec' export { ReplyCodec } from './lib/NativeCodecs/ReplyCodec' @@ -105,7 +106,7 @@ export async function canMessage( clientAddress: string, peerAddress: string ): Promise { - return await XMTPModule.canMessage(clientAddress, peerAddress) + return await XMTPModule.canMessage(clientAddress, getAddress(peerAddress)) } export async function staticCanMessage( @@ -113,7 +114,11 @@ export async function staticCanMessage( environment: 'local' | 'dev' | 'production', appVersion?: string | undefined ): Promise { - return await XMTPModule.staticCanMessage(peerAddress, environment, appVersion) + return await XMTPModule.staticCanMessage( + getAddress(peerAddress), + environment, + appVersion + ) } export async function encryptAttachment( @@ -212,7 +217,7 @@ export async function createConversation( JSON.parse( await XMTPModule.createConversation( client.address, - peerAddress, + getAddress(peerAddress), JSON.stringify(context || {}) ) ) diff --git a/src/lib/Contacts.ts b/src/lib/Contacts.ts index 3ea970f9f..9488fa115 100644 --- a/src/lib/Contacts.ts +++ b/src/lib/Contacts.ts @@ -1,6 +1,7 @@ import { Client } from './Client' import { ConsentListEntry } from './ConsentListEntry' import * as XMTPModule from '../index' +import { getAddress } from '../utils/address' export default class Contacts { client: Client @@ -10,19 +11,27 @@ export default class Contacts { } async isAllowed(address: string): Promise { - return await XMTPModule.isAllowed(this.client.address, address) + return await XMTPModule.isAllowed(this.client.address, getAddress(address)) } async isDenied(address: string): Promise { - return await XMTPModule.isDenied(this.client.address, address) + return await XMTPModule.isDenied(this.client.address, getAddress(address)) } async deny(addresses: string[]): Promise { - return await XMTPModule.denyContacts(this.client.address, addresses) + const checkSummedAddresses = addresses.map((address) => getAddress(address)) + return await XMTPModule.denyContacts( + this.client.address, + checkSummedAddresses + ) } async allow(addresses: string[]): Promise { - return await XMTPModule.allowContacts(this.client.address, addresses) + const checkSummedAddresses = addresses.map((address) => getAddress(address)) + return await XMTPModule.allowContacts( + this.client.address, + checkSummedAddresses + ) } async refreshConsentList(): Promise { diff --git a/src/lib/Conversations.ts b/src/lib/Conversations.ts index 4b13f7352..9363071b5 100644 --- a/src/lib/Conversations.ts +++ b/src/lib/Conversations.ts @@ -3,6 +3,7 @@ import { Conversation } from './Conversation' import { DecodedMessage } from './DecodedMessage' import { ConversationContext } from '../XMTP.types' import * as XMTPModule from '../index' +import { getAddress } from '../utils/address' export default class Conversations { client: Client @@ -50,9 +51,10 @@ export default class Conversations { peerAddress: string, context?: ConversationContext ): Promise> { + const checksumAddress = getAddress(peerAddress) return await XMTPModule.createConversation( this.client, - peerAddress, + checksumAddress, context ) } diff --git a/src/utils/address.ts b/src/utils/address.ts new file mode 100644 index 000000000..cbca82c70 --- /dev/null +++ b/src/utils/address.ts @@ -0,0 +1,50 @@ +import { keccak_256 } from '@noble/hashes/sha3' +import { TextEncoder } from 'text-encoding' + +const addressRegex = /^0x[a-fA-F0-9]{40}$/ +const encoder = new TextEncoder() + +export function stringToBytes(value: string): Uint8Array { + const bytes = encoder.encode(value) + return bytes +} + +export function keccak256(value: Uint8Array): Uint8Array { + const bytes = keccak_256(value) + return bytes +} + +export function isAddress(address: string): boolean { + return addressRegex.test(address) +} + +export function checksumAddress(address_: string, chainId?: number): string { + const hexAddress = chainId + ? `${chainId}${address_.toLowerCase()}` + : address_.substring(2).toLowerCase() + const hash = keccak256(stringToBytes(hexAddress)) + + const address = ( + chainId ? hexAddress.substring(`${chainId}0x`.length) : hexAddress + ).split('') + for (let i = 0; i < 40; i += 2) { + if (hash[i >> 1] >> 4 >= 8 && address[i]) { + address[i] = address[i].toUpperCase() + } + if ((hash[i >> 1] & 0x0f) >= 8 && address[i + 1]) { + address[i + 1] = address[i + 1].toUpperCase() + } + } + + return `0x${address.join('')}` +} + +const addressCache = new Map() + +export function getAddress(address: string, chainId?: number): string { + if (addressCache.has(address)) return addressCache.get(address) as string + if (!isAddress(address)) throw new Error('Invalid address' + address) + const checksumedAddress = checksumAddress(address, chainId) + addressCache.set(address, checksumedAddress) + return checksumedAddress +} diff --git a/yarn.lock b/yarn.lock index e7dff4aa2..672097565 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1536,7 +1536,7 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== -"@noble/hashes@~1.3.0", "@noble/hashes@~1.3.2": +"@noble/hashes@^1.3.3", "@noble/hashes@~1.3.0", "@noble/hashes@~1.3.2": version "1.3.3" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.3.tgz#39908da56a4adc270147bb07968bf3b16cfe1699" integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA== @@ -7958,6 +7958,11 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" +text-encoding@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.7.0.tgz#f895e836e45990624086601798ea98e8f36ee643" + integrity sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA== + text-extensions@^2.0.0: version "2.4.0" resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-2.4.0.tgz#a1cfcc50cf34da41bfd047cc744f804d1680ea34"