import {
Client,
PublicIdentity,
ReplyCodec,
ConsentRecord,
} from '@xmtp/react-native-sdk';
import {IS_SANDBOX} from 'dok-wallet-blockchain-networks/config/config';
import {ContentTypeCustomReplyCodec} from './xmtpContentReplyType';
import crypto from 'react-native-quick-crypto';
import SensitiveInfo from 'react-native-sensitive-info';
import {Buffer} from 'buffer';
import RNFS from 'react-native-fs';
const XMTP_DB_KEY_STORAGE = 'xmtp_db_encryption_key';
const KEYCHAIN_SERVICE = 'com.dokwallet.xmtp';
const XMTP_ENV = IS_SANDBOX ? 'dev' : 'production';
const getOrCreateDbEncryptionKey = async () => {
try {
const stored = await SensitiveInfo.getItem(XMTP_DB_KEY_STORAGE, {
keychainService: KEYCHAIN_SERVICE,
});
if (stored) {
return Buffer.from(stored, 'hex');
}
} catch (e) {
console.warn('XMTP: failed to read encryption key from storage', e);
}
const key = crypto.randomBytes(32);
try {
await SensitiveInfo.setItem(XMTP_DB_KEY_STORAGE, key.toString('hex'), {
keychainService: KEYCHAIN_SERVICE,
});
} catch (e) {
console.warn('XMTP: failed to persist encryption key', e);
}
return key;
};
const clearXmtpDatabase = async () => {
try {
const items = await RNFS.readDir(RNFS.DocumentDirectoryPath);
await Promise.all(
items
.filter(
f =>
f.name.endsWith('.db3') ||
f.name.endsWith('.db3-shm') ||
f.name.endsWith('.db3-wal'),
)
.map(f => RNFS.unlink(f.path)),
);
} catch (e) {
console.warn('XMTP: failed to delete database files', e);
}
try {
await SensitiveInfo.deleteItem(XMTP_DB_KEY_STORAGE, {
keychainService: KEYCHAIN_SERVICE,
});
} catch (e) {
console.warn('XMTP: failed to delete stored key', e);
}
};
export const XMTP = {
client: null,
initializeClient: async ({wallet, address}) => {
try {
const signer = {
getIdentifier: async () =>
new PublicIdentity(wallet.address, 'ETHEREUM'),
getChainId: () => undefined,
getBlockNumber: () => undefined,
signerType: () => 'EOA',
signMessage: async message => {
const sig = await wallet.signMessage(message);
return {signature: Buffer.from(sig.replace('0x', ''), 'hex')};
},
};
if (XMTP.client?.publicIdentity?.identifier !== address?.toLowerCase()) {
const createClient = async () => {
const dbEncryptionKey = await getOrCreateDbEncryptionKey();
return Client.create(signer, {
env: XMTP_ENV,
dbEncryptionKey,
codecs: [new ReplyCodec(), new ContentTypeCustomReplyCodec()],
});
};
try {
XMTP.client = await createClient();
} catch (storageError) {
if (
storageError?.message?.includes('PRAGMA key') ||
storageError?.message?.includes('Storage error')
) {
console.warn(
'XMTP: stale database detected, clearing and retrying',
);
await clearXmtpDatabase();
XMTP.client = await createClient();
} else {
throw storageError;
}
}
}
return XMTP.client;
} catch (error) {
console.log('XMTP initializeClient error:', error);
return null;
}
},
getClient: () => XMTP.client,
getConversations: async () => {
try {
if (!XMTP.client) {
console.warn('XMTP: Please initialize client first');
return [];
}
await XMTP.client.conversations.sync();
const conversations = await XMTP.client.conversations.list(
{lastMessage: true, consentState: true},
undefined,
['allowed', 'unknown'],
);
return XMTP.formatConversation(conversations);
} catch (error) {
console.log('XMTP getConversations error:', error);
return [];
}
},
// Static call — no initialized client needed
checkAccountExists: async ({address}) => {
try {
const result = await Client.canMessage(XMTP_ENV, [
new PublicIdentity(address, 'ETHEREUM'),
]);
return result[address.toLowerCase()] ?? false;
} catch (error) {
console.log('XMTP checkAccountExists error:', error);
return false;
}
},
getMessages: async ({topic, limit = 20, before = null, after = null}) => {
try {
if (!XMTP.client) {
console.warn('XMTP: Please initialize client first');
return [];
}
const conversation =
await XMTP.client.conversations.findConversationByTopic(topic);
if (!conversation) {
return [];
}
const messages = await conversation.messages({
limit,
...(before && {beforeNs: before}),
...(after && {afterNs: after}),
});
return XMTP.formatMessage(messages);
} catch (error) {
console.log('XMTP getMessages error:', error);
return [];
}
},
newConversation: async ({address}) => {
try {
if (!XMTP.client) {
console.warn('XMTP: Please initialize client first');
return null;
}
return await XMTP.client.conversations.findOrCreateDmWithIdentity(
new PublicIdentity(address, 'ETHEREUM'),
);
} catch (error) {
console.log('XMTP newConversation error:', error);
return null;
}
},
getConversation: async ({topic}) => {
try {
if (!XMTP.client) {
console.warn('XMTP: Please initialize client first');
return null;
}
let conv = await XMTP.client.conversations.findConversationByTopic(topic);
if (!conv) {
// Conversation not in local DB yet — sync from network and retry once
await XMTP.client.conversations.sync();
conv = await XMTP.client.conversations.findConversationByTopic(topic);
}
return conv ?? null;
} catch (error) {
console.log('XMTP getConversation error:', error);
return null;
}
},
blockConversation: async ({peerAddress}) => {
try {
if (!XMTP.client) {
console.warn('XMTP: Please initialize client first');
return;
}
const inboxId = await XMTP.client.findInboxIdFromIdentity(
new PublicIdentity(peerAddress, 'ETHEREUM'),
);
if (!inboxId) {
return;
}
await XMTP.client.preferences.setConsentState(
new ConsentRecord(inboxId, 'inbox_id', 'denied'),
);
} catch (error) {
console.log('XMTP blockConversation error:', error);
}
},
unBlockConversation: async ({peerAddress}) => {
try {
if (!XMTP.client) {
console.warn('XMTP: Please initialize client first');
return;
}
const inboxId = await XMTP.client.findInboxIdFromIdentity(
new PublicIdentity(peerAddress, 'ETHEREUM'),
);
if (!inboxId) {
return;
}
await XMTP.client.preferences.setConsentState(
new ConsentRecord(inboxId, 'inbox_id', 'allowed'),
);
} catch (error) {
console.log('XMTP unBlockConversation error:', error);
}
},
unSubscribeStream: () => {
if (!XMTP.client) {
return;
}
XMTP.client.conversations.cancelStreamAllMessages();
XMTP.client.conversations.cancelStream();
},
sendMessage: async ({topic, message}) => {
try {
if (!XMTP.client) {
console.warn('XMTP: Please initialize client first');
return null;
}
const conversation =
await XMTP.client.conversations.findConversationByTopic(topic);
if (!conversation) {
return null;
}
return await conversation.send(message);
} catch (error) {
console.log('XMTP sendMessage error:', error);
return null;
}
},
formatMessage: messages => {
const tempMessages = Array.isArray(messages) ? messages : [];
const finalMessages = [];
for (const msg of tempMessages) {
// sentNs is nanoseconds in v5 — convert to ms for Date
const createdAt = new Date(msg.sentNs / 1_000_000).toISOString();
const user = {_id: msg.senderInboxId};
if (msg.contentTypeId === 'xmtp.org/text:1.0') {
finalMessages.push({
_id: msg.id,
text: msg.content(),
createdAt,
user,
});
} else if (msg.contentTypeId === 'xmtp.org/reply:1.0') {
const reply = msg.content();
finalMessages.push({
_id: msg.id,
text: reply?.content?.text ?? '',
reference: reply?.reference,
createdAt,
user,
});
} else if (msg.contentTypeId === 'com.dok.wallet/customReply:1.1') {
const customReply = msg.content();
finalMessages.push({
_id: msg.id,
text: customReply?.message ?? '',
reference: customReply?.repliedMessageId,
repliedMessage: customReply?.repliedMessage,
repliedUserId: customReply?.senderAddress,
createdAt,
user,
});
}
}
return finalMessages;
},
formatConversation: async conversations => {
const tempConversations = Array.isArray(conversations) ? conversations : [];
return Promise.all(
tempConversations.map(async conv => {
const peerInboxId = conv.peerInboxId
? await conv.peerInboxId()
: undefined;
let peerAddress;
if (peerInboxId && XMTP.client) {
try {
const inboxStates = await XMTP.client.inboxStates(true, [
peerInboxId,
]);
peerAddress = inboxStates?.[0]?.identities?.[0]?.identifier;
} catch (e) {
console.warn('XMTP: failed to resolve peer address', e);
}
}
return {
topic: conv.topic,
peerInboxId,
peerAddress,
createdAt: new Date(conv.createdAt).toISOString(),
version: conv.version,
clientAddress: XMTP.client?.publicIdentity?.identifier,
consentState: conv.state,
};
}),
);
},
};
For further actions, you may consider blocking this person and/or reporting abuse
Top comments (0)