@xmtp/cli-starter
TypeScript icon, indicating that this package has built-in type declarations

1.0.0 • Public • Published

cli-starter

Starter project for building an XMTP CLI

Setup

Prerequisites

  • Node.js version >16.7

Installation

  1. yarn install
  2. yarn build
  3. Run ./xmtp --help in another terminal window

Tools we will be using

  • xmtp-js for interacting with the XMTP network
  • yargs for command line parsing
  • ink for rendering the CLI using React components

Intialize random wallet

Initialize with a random wallet by running:

./xmtp init

Send a message to an address

In src/index.ts you will see a command already defined:

.command(
  "send <address> <message>",
  "Send a message to a blockchain address",
  {
    address: { type: "string", demand: true },
    message: { type: "string", demand: true },
  },
  async (argv) => {
    const { env, message, address } = argv
    const client = await Client.create(loadWallet(), {
      env: env as "dev" | "production" | "local",
    })
    const conversation = await client.conversations.newConversation(address)
    const sent = await conversation.send(message)
    render(<Message msg={sent} />)
  },
)

We want the user to be able to send the contents of the message argument to the specified address.

To start, you'll need to create an instance of the XMTP SDK, using the provided loadWallet() helper.

const { env, message, address } = argv
const client = await Client.create(loadWallet(), { env })

To send a message, you'll need to create a conversation instance and then send that message to the conversaiton.

const conversation = await client.conversations.newConversation(address)
const sent = await conversation.send(message)

So, putting it all together the command will look like:

.command(
  'send <address> <message>',
  'Send a message to a blockchain address',
  {
    address: { type: 'string', demand: true },
    message: { type: 'string', demand: true },
  },
  async (argv: any) => {
    const { env, message, address } = argv
    const client = await Client.create(loadWallet(), { env })
    const conversation = await client.conversations.newConversation(address)
    const sent = await conversation.send(message)
    // Use the Ink renderer provided in the example
    render(<Message {...sent} />)
  }
)

Verify it works

./xmtp send 0xF8cd371Ae43e1A6a9bafBB4FD48707607D24aE43 "Hello world"

List all messages from an address

The next command we are going to implement is list-messages. The starter looks like

.command(
  "list-messages <address>",
  "List all messages from an address",
  { address: { type: "string", demand: true } },
  async (argv) => {
    const { env, address } = argv
    const client = await Client.create(loadWallet(), {
      env: env as "dev" | "production" | "local",
    })
    const conversation = await client.conversations.newConversation(address)
    const messages = await conversation.messages()
    const title = `Messages between ${truncateEthAddress(
      client.address,
    )} and ${truncateEthAddress(conversation.peerAddress)}`

    render(<MessageList title={title} messages={messages} />)
  },
)

Load the Client the same as before, and then load the conversation with the supplied address

const client = await Client.create(loadWallet(), { env })
const convo = await client.conversations.newConversation(address)

Get all the messages in the conversation with

const messages = await convo.messages()

You can then render them prettily with the supplied renderer component

const title = `Messages between ${truncateEthAddress(
  client.address,
)} and ${truncateEthAddress(convo.peerAddress)}`
render(<MessageList title={title} messages={messages} />)

The completed command will look like:

.command(
    'list-messages <address>',
    'List all messages from an address',
    { address: { type: 'string', demand: true } },
    async (argv: any) => {
        const { env, address } = argv
        const client = await Client.create(loadWallet(), { env })
        const conversation = await client.conversations.newConversation(address)
        const messages = await conversation.messages()
        const title = `Messages between ${truncateEthAddress(
            client.address
        )} and ${truncateEthAddress(conversation.peerAddress)}`

        render(<MessageList title={title} messages={messages} />)
    }
)

Verify it works

./xmtp list-messages 0xF8cd371Ae43e1A6a9bafBB4FD48707607D24aE43

Stream all messages

To stream messages from an address, we'll want to use a stateful React component. This will require doing some work in the command, as well as the Ink component

The starter command in index.tsx should look like

.command(
  "stream-all",
  "Stream messages coming from any address",
  {},
  async (argv) => {
    const { env } = argv
    const client = await Client.create(loadWallet(), {
      env: env as "dev" | "production" | "local",
    })
    const stream = await client.conversations.streamAllMessages()
    render(<MessageStream stream={stream} title={`Streaming all messages`} />)
  },
)

There is also a starter React component that looks like this:

export const MessageStream = ({ stream, title }: MessageStreamProps) => {
  const [messages, setMessages] = useState<DecodedMessage[]>([])

  return <MessageList title={title} messages={messages} />
}

First, we will want to get a message Stream, which is just an Async Iterable.

const { env } = argv
const client = await Client.create(loadWallet(), { env })
const stream = await client.conversations.streamAllMessages()

Then we will pass that stream to the component with something like

render(<MessageStream stream={stream} title={`Streaming all messages`} />)

Update the MessageStream React component in renderers.tsx to listen to the stream and update the state as new messages come in.

We can accomplish that with a useEffect hook that pulls from the Async Iterable and updates the state each time a message comes in.

You'll want to keep track of seen messages, as duplicates are possible in a short time window.

useEffect(() => {
  if (!stream) {
    return
  }
  // Keep track of all seen messages.
  // Would be more performant to keep this to a limited buffer of the most recent 5 messages
  const seenMessages = new Set<string>()

  const listenForMessages = async () => {
    for await (const message of stream) {
      if (seenMessages.has(message.id)) {
        continue
      }
      // Add the message to the existing array
      setMessages((existing) => existing.concat(message))
      seenMessages.add(message.id)
    }
  }

  listenForMessages()

  // When unmounting, always remember to close the stream
  return () => {
    if (stream) {
      stream.return(undefined)
    }
  }
}, [stream, setMessages])

Verify it works

./xmtp stream-all

Listen for messages from a single address

The starter for this command should look like:

.command(
  "stream <address>",
  "Stream messages from an address",
  { address: { type: "string", demand: true } },
  async (argv) => {
    const { env, address } = argv // or message
    const client = await Client.create(loadWallet(), {
      env: env as "dev" | "production" | "local",
    })
    const conversation = await client.conversations.newConversation(address)
    const stream = await conversation.streamMessages()
    render(
      <MessageStream stream={stream} title={`Streaming conv messages`} />,
    )
  },
)

You can implement this challenge by combining what you learned from listing all messages in a conversation and rendering a message stream.

Hint: You can get a message stream from a Conversation by using the method conversation.streamMessages()

Verify it works

./xmtp stream 0xF8cd371Ae43e1A6a9bafBB4FD48707607D24aE43

Proper key management

All the examples thus far have been using a randomly generated wallet and a private key stored in a file on disk. It would be better if we could use this with any existing wallet, and if we weren't touching private keys at all.

With a simple webpage that uses Wagmi, Web3Modal, or any other library that returns an ethers.Signer you can export XMTP-specific keys and store those on the user's machine.

The command to export keys is Client.getKeys(wallet, { env }).

Readme

Keywords

none

Package Sidebar

Install

npm i @xmtp/cli-starter

Weekly Downloads

0

Version

1.0.0

License

MIT

Unpacked Size

31.4 kB

Total Files

26

Last publish

Collaborators

  • alexxmtp
  • fabriguespe
  • nplasterer-xmtp
  • saul-xmtp
  • nick-xmtp
  • xmtp-eng-robot
  • galligan