Research
Security News
Malicious npm Packages Inject SSH Backdoors via Typosquatted Libraries
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
@xmtp/cli-starter
Advanced tools
Starter project for building an XMTP CLI
yarn install
yarn build
./xmtp --help
in another terminal windowInitialize with a random wallet by running:
./xmtp init
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} />)
}
)
./xmtp send 0xF8cd371Ae43e1A6a9bafBB4FD48707607D24aE43 "Hello world"
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} />)
}
)
./xmtp list-messages 0xF8cd371Ae43e1A6a9bafBB4FD48707607D24aE43
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])
./xmtp stream-all
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()
./xmtp stream 0xF8cd371Ae43e1A6a9bafBB4FD48707607D24aE43
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 })
.
FAQs
Starter kit for building a XMTP CLI
We found that @xmtp/cli-starter demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 7 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
Security News
MITRE's 2024 CWE Top 25 highlights critical software vulnerabilities like XSS, SQL Injection, and CSRF, reflecting shifts due to a refined ranking methodology.
Security News
In this segment of the Risky Business podcast, Feross Aboukhadijeh and Patrick Gray discuss the challenges of tracking malware discovered in open source softare.