UNPKG

@pod-protocol/sdk-js

Version:
433 lines (389 loc) 13 kB
'use strict'; var base = require('../base-B0PMFl1k.js'); var anchor = require('@coral-xyz/anchor'); var web3_js = require('@solana/web3.js'); var utils_pda = require('../utils/pda.js'); var types = require('../types.js'); /** * Message service for PoD Protocol SDK */ /** * Service for managing messages in the PoD Protocol * * @class MessageService * @extends BaseService */ class MessageService extends base.BaseService { /** * Send a message to another agent * * @param {Object} options - Message options * @param {Object} wallet - Sender's wallet * @returns {Promise<string>} Transaction signature * * @example * ```javascript * const tx = await client.messages.send({ * recipient: recipientAddress, * content: 'Hello from PoD Protocol!', * messageType: MessageType.TEXT, * expirationDays: 7 * }, wallet); * ``` */ async send(options, wallet) { if (!this.isInitialized()) { throw new Error('Service not initialized. Call client.initialize() first.'); } // Derive recipient agent PDA const [recipientPDA] = await utils_pda.findAgentPDA(options.recipient, this.programId); // Create payload hash const payloadHash = await this.hashPayload(options.content); // Calculate expiration timestamp (default 7 days) const expirationDays = options.expirationDays || 7; const expiresAt = Math.floor(Date.now() / 1000) + (expirationDays * 24 * 60 * 60); // Derive message PDA const [messagePDA] = await utils_pda.findMessagePDA( wallet.publicKey, options.recipient, this.programId ); return this.retry(async () => { const tx = await this.program.methods .sendMessage( options.recipient, Array.from(payloadHash), options.content, options.messageType || 0, new anchor.BN(expiresAt) ) .accounts({ messageAccount: messagePDA, sender: wallet.publicKey, recipient: options.recipient, recipientAgent: recipientPDA, systemProgram: web3_js.SystemProgram.programId }) .rpc(); return tx; }); } /** * Get a message by its PDA * * @param {Object} messagePDA - Message PDA * @returns {Promise<Object|null>} Message account data * * @example * ```javascript * const message = await client.messages.get(messagePDA); * if (message) { * console.log('Message content:', message.payload); * console.log('Message status:', message.status); * } * ``` */ async get(messagePDA) { if (!this.isInitialized()) { throw new Error('Service not initialized. Call client.initialize() first.'); } try { const messageAccount = await this.program.account.messageAccount.fetch(messagePDA); return { pubkey: messagePDA, ...messageAccount, // Convert BN to number for JavaScript compatibility timestamp: messageAccount.timestamp.toNumber(), createdAt: messageAccount.timestamp.toNumber(), expiresAt: messageAccount.expiresAt.toNumber() }; } catch (error) { if (error.message?.includes('Account does not exist')) { return null; } throw error; } } /** * Get messages for an agent (sent or received) * * @param {Object} agentPubkey - Agent's public key * @param {Object} [options] - Query options * @param {string} [options.direction='both'] - 'sent', 'received', or 'both' * @param {number} [options.limit=100] - Maximum number of messages * @param {string} [options.status] - Filter by message status * @returns {Promise<Array>} Array of message accounts * * @example * ```javascript * // Get last 50 received messages * const messages = await client.messages.getForAgent(agentAddress, { * direction: 'received', * limit: 50 * }); * ``` */ async getForAgent(agentPubkey, options = {}) { if (!this.isInitialized()) { throw new Error('Service not initialized. Call client.initialize() first.'); } try { const accounts = await this.program.account.messageAccount.all(); let messages = accounts .map(account => ({ pubkey: account.publicKey, ...account.account, timestamp: account.account.timestamp.toNumber(), createdAt: account.account.timestamp.toNumber(), expiresAt: account.account.expiresAt.toNumber() })) .filter(message => { const direction = options.direction || 'both'; if (direction === 'sent') { return message.sender.equals(agentPubkey); } else if (direction === 'received') { return message.recipient.equals(agentPubkey); } else { return message.sender.equals(agentPubkey) || message.recipient.equals(agentPubkey); } }); // Filter by status if specified if (options.status) { messages = messages.filter(message => message.status === options.status); } // Sort by timestamp (newest first) and apply limit messages.sort((a, b) => b.timestamp - a.timestamp); if (options.limit) { messages = messages.slice(0, options.limit); } return messages; } catch (error) { throw new Error(`Failed to get messages: ${error.message}`); } } /** * Mark a message as read * * @param {Object} messagePDA - Message PDA * @param {Object} wallet - Recipient's wallet * @returns {Promise<string>} Transaction signature * * @example * ```javascript * const tx = await client.messages.markAsRead(messagePDA, wallet); * ``` */ async markAsRead(messagePDA, wallet) { if (!this.isInitialized()) { throw new Error('Service not initialized. Call client.initialize() first.'); } return this.retry(async () => { const tx = await this.program.methods .updateMessageStatus('read') .accounts({ messageAccount: messagePDA, signer: wallet.publicKey }) .rpc(); return tx; }); } /** * Delete a message (only sender can delete) * * @param {Object} messagePDA - Message PDA * @param {Object} wallet - Sender's wallet * @returns {Promise<string>} Transaction signature * * @example * ```javascript * const tx = await client.messages.delete(messagePDA, wallet); * ``` */ async delete(messagePDA, wallet) { if (!this.isInitialized()) { throw new Error('Service not initialized. Call client.initialize() first.'); } return this.retry(async () => { const tx = await this.program.methods .deleteMessage() .accounts({ messageAccount: messagePDA, sender: wallet.publicKey }) .rpc(); return tx; }); } /** * Get conversation between two agents * * @param {Object} agent1 - First agent's public key * @param {Object} agent2 - Second agent's public key * @param {Object} [options] - Query options * @param {number} [options.limit=100] - Maximum number of messages * @returns {Promise<Array>} Array of message accounts * * @example * ```javascript * const conversation = await client.messages.getConversation( * myAgentKey, * otherAgentKey, * { limit: 50 } * ); * ``` */ async getConversation(agent1, agent2, options = {}) { if (!this.isInitialized()) { throw new Error('Service not initialized. Call client.initialize() first.'); } try { const accounts = await this.program.account.messageAccount.all(); let messages = accounts .map(account => ({ pubkey: account.publicKey, ...account.account, timestamp: account.account.timestamp.toNumber(), createdAt: account.account.timestamp.toNumber(), expiresAt: account.account.expiresAt.toNumber() })) .filter(message => (message.sender.equals(agent1) && message.recipient.equals(agent2)) || (message.sender.equals(agent2) && message.recipient.equals(agent1)) ); // Sort by timestamp (oldest first for conversation view) messages.sort((a, b) => a.timestamp - b.timestamp); if (options.limit) { messages = messages.slice(-options.limit); // Get last N messages } return messages; } catch (error) { throw new Error(`Failed to get conversation: ${error.message}`); } } /** * Get unread message count for an agent * * @param {Object} agentPubkey - Agent's public key * @returns {Promise<number>} Number of unread messages * * @example * ```javascript * const unreadCount = await client.messages.getUnreadCount(agentAddress); * console.log(`You have ${unreadCount} unread messages`); * ``` */ async getUnreadCount(agentPubkey) { const messages = await this.getForAgent(agentPubkey, { direction: 'received', status: 'delivered' }); return messages.length; } /** * Send a message instruction (for batch operations) * * @param {Object} options - Message options * @param {Object} senderPubkey - Sender's public key * @returns {Promise<Object>} Send message instruction */ async createSendInstruction(options, senderPubkey) { if (!this.program) { throw new Error('Program not initialized'); } const payloadHash = await this.hashPayload(options.content); const [messagePDA] = await utils_pda.findMessagePDA( senderPubkey, options.recipient, this.programId ); const expirationDays = options.expirationDays || 7; const expiresAt = Math.floor(Date.now() / 1000) + (expirationDays * 24 * 60 * 60); return this.program.methods .sendMessage( Array.from(payloadHash), options.content, options.messageType || types.MessageType.TEXT, new anchor.BN(expiresAt) ) .accounts({ messageAccount: messagePDA, sender: senderPubkey, recipient: options.recipient, systemProgram: web3_js.SystemProgram.programId }) .instruction(); } /** * Update message status instruction * * @param {Object} messagePDA - Message PDA * @param {string} status - New status * @param {Object} signerPubkey - Signer's public key * @returns {Promise<Object>} Update status instruction */ async createUpdateStatusInstruction(messagePDA, status, signerPubkey) { if (!this.program) { throw new Error('Program not initialized'); } return this.program.methods .updateMessageStatus(status) .accounts({ messageAccount: messagePDA, signer: signerPubkey }) .instruction(); } /** * Hash payload for message integrity * @param {string} payload - Message content to hash * @returns {Promise<Uint8Array>} SHA-256 hash of the payload */ async hashPayload(payload) { try { // Use Web Crypto API for hashing const encoder = new TextEncoder(); const data = encoder.encode(payload); const hashBuffer = await crypto.subtle.digest('SHA-256', data); return new Uint8Array(hashBuffer); } catch (error) { console.error('Error hashing payload:', error); // Fallback to simple hash for JavaScript compatibility const encoder = new TextEncoder(); const data = encoder.encode(payload); const simpleHash = new Uint8Array(32); for (let i = 0; i < data.length && i < 32; i++) { simpleHash[i] = data[i] ^ (i % 256); } return simpleHash; } } /** * Get message by PDA * @private */ async _getMessageByPDA(senderPubkey, recipientPubkey) { try { const [messagePDA] = await utils_pda.findMessagePDA( senderPubkey, recipientPubkey, this.programId ); const messageAccount = await this.program.account.messageAccount.fetch(messagePDA); return { pubkey: messagePDA, ...messageAccount, // Convert BN fields to numbers for JavaScript compatibility timestamp: messageAccount.timestamp.toNumber(), expiresAt: messageAccount.expiresAt.toNumber() }; } catch (error) { if (error.message?.includes('Account does not exist')) { return null; } throw error; } } } exports.MessageService = MessageService; //# sourceMappingURL=message.js.map