peterduhon / skia-poker-morph

Trustless Tricksters: A decentralized poker game built on Morph zkEVM. Featuring secure card shuffling, Chainlink VRF for fairness, Galadriel AI agents, XMTP chat, and Web3Auth login. Experience the future of poker on the blockchain. 🃏🔐🚀
3 stars 0 forks source link

[SP-12] Implement Private Messaging with XMTP #14

Open peterduhon opened 1 week ago

peterduhon commented 1 week ago

[SP-12] Implement Private Messaging with XMTP Summary: Implement secure private messaging between players using XMTP with end-to-end encryption. Tasks: Set up XMTP messaging infrastructure. Develop UI components for private messaging. Integrate messaging into the existing UI. Conduct security and functionality testing. Validate secure communication and encryption

peterduhon commented 1 week ago

Implementing Private Messaging with XMTP

1. Set up XMTP messaging infrastructure

First, install the necessary dependencies:

npm install @xmtp/xmtp-js ethers@5.7.2

Create a new file src/services/xmtpService.js:

import { Client } from '@xmtp/xmtp-js';
import { ethers } from 'ethers';

let xmtp;
let conversations = new Map();

export const initializeXMTP = async (signer) => {
  xmtp = await Client.create(signer, { env: 'production' });
  return xmtp;
};

export const startConversation = async (peerAddress) => {
  if (!xmtp) throw new Error('XMTP client not initialized');
  const conversation = await xmtp.conversations.newConversation(peerAddress);
  conversations.set(peerAddress, conversation);
  return conversation;
};

export const sendMessage = async (peerAddress, message) => {
  const conversation = conversations.get(peerAddress) || await startConversation(peerAddress);
  await conversation.send(message);
};

export const listenForMessages = (peerAddress, callback) => {
  const conversation = conversations.get(peerAddress);
  if (!conversation) throw new Error('Conversation not found');

  const stream = conversation.streamMessages();
  stream.on('message', callback);
  return () => stream.return(); // Call this function to stop listening
};

2. Develop UI components for private messaging

Create a new file src/components/PrivateMessaging.js:

import React, { useState, useEffect, useCallback } from 'react';
import { initializeXMTP, sendMessage, listenForMessages } from '../services/xmtpService';

const PrivateMessaging = ({ signer, peerAddress }) => {
  const [messages, setMessages] = useState([]);
  const [newMessage, setNewMessage] = useState('');
  const [xmtpClient, setXmtpClient] = useState(null);

  useEffect(() => {
    const setup = async () => {
      const client = await initializeXMTP(signer);
      setXmtpClient(client);
    };
    setup();
  }, [signer]);

  useEffect(() => {
    if (!xmtpClient || !peerAddress) return;

    const unsubscribe = listenForMessages(peerAddress, (message) => {
      setMessages((prev) => [...prev, message]);
    });

    return unsubscribe;
  }, [xmtpClient, peerAddress]);

  const handleSend = useCallback(async () => {
    if (!newMessage.trim()) return;
    await sendMessage(peerAddress, newMessage);
    setNewMessage('');
  }, [newMessage, peerAddress]);

  return (
    <div className="private-messaging">
      <div className="message-list">
        {messages.map((msg, index) => (
          <div key={index} className="message">
            <span className="sender">{msg.senderAddress === xmtpClient.address ? 'You' : 'Peer'}:</span>
            <span className="content">{msg.content}</span>
          </div>
        ))}
      </div>
      <div className="message-input">
        <input
          type="text"
          value={newMessage}
          onChange={(e) => setNewMessage(e.target.value)}
          placeholder="Type a message..."
        />
        <button onClick={handleSend}>Send</button>
      </div>
    </div>
  );
};

export default PrivateMessaging;

3. Integrate messaging into the existing UI

Update your main game component or wherever you want to include private messaging:

import React from 'react';
import PrivateMessaging from './PrivateMessaging';

const GameInterface = ({ signer, opponent }) => {
  return (
    <div className="game-interface">
      {/* Other game components */}
      <PrivateMessaging signer={signer} peerAddress={opponent.address} />
    </div>
  );
};

export default GameInterface;

4. Conduct security and functionality testing

Here's a basic test file to get you started. Create src/components/PrivateMessaging.test.js:

import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import PrivateMessaging from './PrivateMessaging';
import { initializeXMTP, sendMessage, listenForMessages } from '../services/xmtpService';

jest.mock('../services/xmtpService');

describe('PrivateMessaging', () => {
  const mockSigner = {};
  const mockPeerAddress = '0x1234...';

  beforeEach(() => {
    initializeXMTP.mockResolvedValue({});
    listenForMessages.mockImplementation((_, callback) => {
      callback({ senderAddress: mockPeerAddress, content: 'Test message' });
      return jest.fn();
    });
  });

  it('renders without crashing', () => {
    render(<PrivateMessaging signer={mockSigner} peerAddress={mockPeerAddress} />);
    expect(screen.getByPlaceholderText('Type a message...')).toBeInTheDocument();
  });

  it('sends a message when the send button is clicked', async () => {
    render(<PrivateMessaging signer={mockSigner} peerAddress={mockPeerAddress} />);

    fireEvent.change(screen.getByPlaceholderText('Type a message...'), { target: { value: 'Hello' } });
    fireEvent.click(screen.getByText('Send'));

    expect(sendMessage).toHaveBeenCalledWith(mockPeerAddress, 'Hello');
  });

  it('displays received messages', async () => {
    render(<PrivateMessaging signer={mockSigner} peerAddress={mockPeerAddress} />);

    expect(await screen.findByText('Test message')).toBeInTheDocument();
  });
});

To run these tests, make sure you have Jest and React Testing Library installed, then run:

npm test

5. Validate secure communication and encryption

XMTP handles encryption automatically, but you should still:

  1. Verify that messages are end-to-end encrypted by inspecting network traffic.
  2. Ensure that private keys are never exposed or transmitted.
  3. Test the system with multiple users to confirm that messages are only received by the intended recipients.

Remember to always use HTTPS in production to protect data in transit.

cc: @JW-dev0505

peterduhon commented 1 week ago

@tetyana-pol

An update about where to get the signer and peerAddress for the PrivateMessaging component. Let me clarify how to integrate this into our poker game:

  1. The signer should come from your Web3 provider (like Web3Auth or MetaMask). You typically get this when a user connects their wallet.

  2. The peerAddress is the Ethereum address of the player you want to message (usually your opponent in the game).

Here's how you can integrate this into your main game component:

  1. Update your main game component (let's call it PokerGame.js) like this:

import React, { useState, useEffect } from 'react';
import { ethers } from 'ethers';
import PrivateMessaging from './PrivateMessaging';
import { Web3Auth } from "@web3auth/modal";
// Import other necessary components

const PokerGame = () => {
  const [signer, setSigner] = useState(null);
  const [opponent, setOpponent] = useState(null);
  const [web3auth, setWeb3auth] = useState(null);

  useEffect(() => {
    const initWeb3Auth = async () => {
      const web3auth = new Web3Auth({
        clientId: "YOUR_WEB3AUTH_CLIENT_ID",
        chainConfig: {
          // ... your Morph chain configuration
        },
      });
      setWeb3auth(web3auth);
      await web3auth.initModal();
    };
    initWeb3Auth();
  }, []);

  const connectWallet = async () => {
    if (!web3auth) {
      console.log("web3auth not initialized yet");
      return;
    }
    const web3authProvider = await web3auth.connect();
    const ethersProvider = new ethers.providers.Web3Provider(web3authProvider);
    const signer = ethersProvider.getSigner();
    setSigner(signer);
  };

  // This function would be called when a game starts or when an opponent is assigned
  const setGameOpponent = (opponentAddress) => {
    setOpponent(opponentAddress);
  };

  return (
    <div>
      {!signer ? (
        <button onClick={connectWallet}>Connect Wallet</button>
      ) : (
        <div>
          {/* Your main game UI components */}
          {opponent && (
            <PrivateMessaging signer={signer} peerAddress={opponent} />
          )}
        </div>
      )}
    </div>
  );
};

export default PokerGame;
peterduhon commented 1 week ago

This setup above does the following:

  1. Initializes Web3Auth and provides a way for users to connect their wallet.
  2. Once connected, it creates a signer from the Web3 provider.
  3. The setGameOpponent function would be called when a game starts or an opponent is assigned. You'll need to implement the logic for this based on your game flow.
  4. The PrivateMessaging component is only rendered when both a signer and an opponent are available.

Remember to replace "YOUR_WEB3AUTH_CLIENT_ID" with your actual Web3Auth client ID.

Let me know if you need any clarification or have any questions about integrating this into your existing code!

peterduhon commented 1 week ago

Hi @tetyana-pol ,

I wanted to clarify a couple of points about the XMTP and Web3Auth integration:

  1. XMTP and Web3Auth Connection: The Web3Auth integration provides the signer (user's wallet) that XMTP needs for messaging. Essentially, Web3Auth handles the user authentication and wallet connection, while XMTP uses that connection for secure messaging.

  2. GameInterface Component: I wanted to confirm if you're still using a structure similar to this for your game interface:


// GameInterface.js
import React, { useState, useEffect } from 'react';
import { ethers } from 'ethers';
import PrivateMessaging from './PrivateMessaging';
import GameBoard from './GameBoard'; 
import PlayerInfo from './PlayerInfo'; 
import ActionButtons from './ActionButtons'; 

const GameInterface = ({ web3Provider, gameContract, playerAddress }) => {
  const [opponent, setOpponent] = useState(null);
  const [signer, setSigner] = useState(null);

  useEffect(() => {
    const setupSigner = async () => {
      if (web3Provider) {
        const signerInstance = web3Provider.getSigner();
        setSigner(signerInstance);
      }
    };

    const getOpponent = async () => {
      if (gameContract) {
        // This is a placeholder. You'll need to implement a method to get the opponent's address
        const opponentAddress = await gameContract.getOpponent(playerAddress);
        setOpponent(opponentAddress);
      }
    };

    setupSigner();
    getOpponent();
  }, [web3Provider, gameContract, playerAddress]);

  if (!signer || !opponent) {
    return <div>Loading game interface...</div>;
  }

  return (
    <div className="game-interface">
      <div className="game-board-container">
        <GameBoard contract={gameContract} playerAddress={playerAddress} />
      </div>
      <div className="sidebar">
        <PlayerInfo playerAddress={playerAddress} />
        <ActionButtons contract={gameContract} playerAddress={playerAddress} />
        <PrivateMessaging signer={signer} peerAddress={opponent} />
      </div>
    </div>
  );
};

export default GameInterface;
peterduhon commented 1 week ago

1. XMTP Integration (xmtpService.js):

import { Client } from "@xmtp/xmtp-js";

let xmtp;

export const initializeXMTP = async (signer) => {
  xmtp = await Client.create(signer, { env: "production" });
  return xmtp;
};

export const sendMessage = async (peerAddress, message) => {
  if (!xmtp) throw new Error("XMTP client not initialized");
  const conversation = await xmtp.conversations.newConversation(peerAddress);
  await conversation.send(message);
};

export const listenForMessages = (callback) => {
  if (!xmtp) throw new Error("XMTP client not initialized");
  const stream = xmtp.conversations.streamAllMessages();
  stream.on("message", callback);
  return () => stream.removeListener("message", callback);
};

2. Integrating XMTP in the game component (e.g., GameComponent.jsx):

import React, { useEffect, useState } from 'react';
import { initializeXMTP, sendMessage, listenForMessages } from './xmtpService';

const GameComponent = ({ web3 }) => {
  const [xmtpClient, setXmtpClient] = useState(null);
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    if (web3) {
      const initXmtp = async () => {
        const signer = web3.eth.personal.sign.bind(web3.eth.personal);
        const client = await initializeXMTP(signer);
        setXmtpClient(client);
      };
      initXmtp();
    }
  }, [web3]);

  useEffect(() => {
    if (xmtpClient) {
      const unsubscribe = listenForMessages((message) => {
        setMessages(prevMessages => [...prevMessages, message]);
      });
      return () => unsubscribe();
    }
  }, [xmtpClient]);

  const handleSendMessage = async (recipientAddress, messageContent) => {
    if (xmtpClient) {
      await sendMessage(recipientAddress, messageContent);
    }
  };

  // Rest of your game component...
};

export default GameComponent;
  1. The xmtpService.js file should be created to handle XMTP functionality. This service initializes the XMTP client, sends messages, and listens for incoming messages.

  2. In the game component (or wherever the chat functionality is needed), import and use the XMTP service. The waiting room. The component should:

    • Initialize XMTP when Web3 is available.
    • Set up a listener for incoming messages.
    • Provide a method to send messages.
  3. The XMTP client initialization uses the signer from Web3, which is already set up in the AuthPage.jsx file. Make sure to pass the Web3 instance to the game component after successful authentication.

  4. This setup allows for chat functionality in the waiting room before the game starts, as per your requirements.

  5. @tetyana-pol please install the XMTP package:

    npm install @xmtp/xmtp-js
peterduhon commented 1 week ago

Instructions for Integrating XMTP Chat

Hi @tetyana-pol ! Here's how to get started with the XMTP chat component:

  1. Create a new file called XMTPChat.jsx in your components directory and copy the provided code into it.

  2. Install necessary dependencies:

    npm install @xmtp/xmtp-js @web3auth/modal @web3auth/base
  3. Replace "YOUR_WEB3AUTH_CLIENT_ID" with the actual client ID from the Web3Auth dashboard.

  4. Import and use the component in your main App or relevant page:

    import XMTPChat from './components/XMTPChat';
    
    function App() {
     return (
       <div>
         <h1>Skia Poker</h1>
         <XMTPChat />
       </div>
     );
    }
  5. You can start using this component without smart contract addresses or ABIs. It will allow users to connect their wallet and send messages using XMTP.

  6. For testing, you can hardcode a recipient address in the sendMessage function. Replace "0x1234..." with a test Ethereum address.

  7. Run your React app locally to test the XMTP chat functionality.

Note: This setup uses Web3Auth for authentication and XMTP for messaging. It's configured to work with the Morph testnet, but actual blockchain interactions aren't implemented yet.

Next steps:

Let us know if you have any questions or run into any issues!

peterduhon commented 1 week ago

Instructions for Sharing Contract Information

Hi @JW-dev0505

To help Tetiana integrate the smart contracts with the frontend, please provide the following information:

  1. Deployed Contract Addresses:

    • For each smart contract (GameLogic, PlayerManagement, etc.), provide the address where it's deployed on the Morph testnet.
  2. Contract ABIs:

    • For each smart contract, provide the ABI (Application Binary Interface).
    • You can usually find this in the compilation output or artifact files generated by your development environment (e.g., Hardhat or Truffle).
  3. Any specific instructions for interacting with the contracts:

    • Are there any initialization steps required?
    • Are there any specific function calls that need to be made in a certain order?
  4. Test Accounts:

    • If you have any test accounts set up on the Morph testnet with ETH balance, please provide their addresses and private keys (only if these are throwaway accounts for testing purposes).

Please compile this information and share it with Tetiana. You can create a markdown file with the following structure:

# Smart Contract Integration Information

## Deployed Addresses
- GameLogic: 0x...
- PlayerManagement: 0x...
- (Other contracts...)

## ABIs
(Paste the ABIs here, one for each contract)

## Interaction Instructions
1. First, connect to the GameLogic contract...
2. Then, register a player using the PlayerManagement contract...
3. (Any other relevant instructions)

## Test Accounts
- Address: 0x...
  Private Key: 0x... (only if it's a throwaway account)
- (Add more if available)

This information will allow @tetyana-pol to create the necessary contract instances in the frontend and start integrating the blockchain functionality with the UI.

JW-dev0505 commented 1 week ago

@peterduhon @tetyana-pol Here is deployed addresses. AIPlayerManagement deployed to: 0x7bAcEEC35b09138b46b0d4edbe823E1d685691fE RoomManagement deployed to: 0x1d2AffDb37dfdb61442d6DDE7a4b3A242198ad6C UserManagement deployed to: 0x8EE046ef044C7fd8D41777259024f6d52Ba1d5b4 CardManagement deployed to: 0xdF100bFA4d60A7c26bF78E1987efeB1DaDC6De79 BettingAndPotManagement deployed to: 0x5ed17243C07BE0D9Ed8EdCcd82B3d25525cd7010 PokerGameProxy deployed to: 0xD708db9782597D6BB59D6D6B631a845cb891168f

And other necessary credentials. PRIVATE_KEY=736e5b9672babec26309c6d294861eaffcc2c9f4e91535b356c80bf569251cd9 API_URL=https://rpc.ankr.com/eth_holesky PRIVATE_KEY_LOCAL=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 (Test Acount)