LessonsExtensions: Built inLesson 65

Nostr with NostrXT

Post to Nostr, the open social network with no accounts, with NostrXT: key pairs and npub names, events and their ids, signatures, JSON on the wire, tags and replies, profiles, links, private messages and the filters relays answer.

  • Extensions
  • Lesson 65 of 83
  • About 14 minutes
  • 8 examples

A network of signed notes

Nostr is a social network with no company in the middle and no accounts. Everything on it is a short, signed piece of JSON called an event, and relays, servers anyone can run, keep events and pass them on. Because every event is signed, a reader can check who wrote it whichever relay it came from. NostrXT is the xTalk Suite's Nostr library, written in xTalk over CoinXT's signatures and SodiumXT's encryption; its handlers begin with nx.

The examples make keys from phrases, so that every run gives the same answers; a real app calls nxKeyGenerate() for a random one.

You are a key pair

On Nostr your secret key is 64 hex digits that only you hold, and your public key, which comes from it, is who you are to everyone. People share keys as npub names (and back them up as nsec names), which have a typo check built in:

ExampleRun it in the playground
-- for learning only: a real app calls nxKeyGenerate()
put nxHexEncode(cxSha256(textEncode("alice learns nostr", "UTF-8"))) into tSecret
put nxKeyIsValid(tSecret)
put nxKeyPublic(tSecret) into tPublic
put tPublic
put nxNpubEncode(tPublic) into tNpub
put tNpub
put nxNpubDecode(tNpub) is tPublic
put char 1 to 5 of nxNsecEncode(tSecret)
Output
true
4c03b4839d256423f8601551d16f46d9251d29f294cc79cfd51676542d78267a
npub1fspmfquay4jz87rqz4gazm6xmyj3620jjnx8nn74zem9gttcyeaqtkq2jp
true
nsec1

Events, ids and signatures

An event has a kind (1 is a short note, 0 a profile, 7 a reaction), a time in seconds since 1970, its text, tags and its author. nxEventBuild makes one, and nxEventSign fills in the author, the id (the SHA-256 fingerprint of its exact text) and a Schnorr signature. nxEventVerify says true only when both check out, so treat anything else as fake:

ExampleRun it in the playground
put nxHexEncode(cxSha256(textEncode("alice learns nostr", "UTF-8"))) into tSecret
put nxEventBuild(1, "Hello, Nostr!", empty, 1700000000) into tEvent
put nxEventSign(tEvent, tSecret, empty) into tEvent
put tEvent["kind"] && tEvent["created_at"]
put the length of tEvent["id"] && "hex digits of id," && the length of tEvent["sig"] && "of signature"
put nxEventVerify(tEvent)
-- someone changes the note on its way
put "Hello! Send me coins." into tEvent["content"]
put nxEventVerify(tEvent)
Output
1 1700000000
64 hex digits of id, 128 of signature
true
false

The empty third parameter of nxEventSign asks for fresh randomness in the signature, so the same note signed twice has two different signatures, and both check.

On the wire: JSON

To travel, an event becomes JSON. nxEventToJson writes it, nxEventFromJson reads it back into an array, and nxJsonGet takes one value out by its name. Text from the internet could say anything, so verify what you read before you trust it:

ExampleRun it in the playground
put nxHexEncode(cxSha256(textEncode("alice learns nostr", "UTF-8"))) into tSecret
put nxEventSign(nxEventBuild(1, "Hello, Nostr!", empty, 1700000000), tSecret, empty) into tEvent
put nxEventToJson(tEvent) into tJson
put char 1 to 10 of tJson
put nxJsonGet(tJson, "content")
put nxJsonGet(tJson, "kind")
put nxEventVerify(nxEventFromJson(tJson))
Output
{"id":"4ff
Hello, Nostr!
1
true

Tags, replies and reactions

Tags label an event: "t" for a topic, "p" for a person mentioned, "e" for another event. nxTagsFromText makes them from text, a line each with a tab between the parts, and nxTagFirst and nxTagValues read them back. A conversation is events that point at each other with tags: nxReplyBuild makes a reply with an "e" tag for the post and a "p" tag for its author, and nxReactionBuild a reaction, kind 7, with "+" for a like:

ExampleRun it in the playground
put nxHexEncode(cxSha256(textEncode("alice learns nostr", "UTF-8"))) into tSecret
put "t" & tab & "openxtalk" & return & "t" & tab & "learning" into tTags
put nxEventBuild(1, "Hi #openxtalk", nxTagsFromText(tTags), 1700000000) into tPost
put nxEventSign(tPost, tSecret, empty) into tPost
put nxTagCount(tPost) && "tags"
put nxTagFirst(tPost, "t")
put nxTagValues(tPost, "t")

put nxReplyBuild(tPost, "Me too!", 1700000060) into tReply
put nxTagFirst(tReply, "e") is tPost["id"]
put nxTagFirst(tReply, "p") is nxKeyPublic(tSecret)
put nxReactionBuild(tPost, empty, 1700000090) into tLike
put tLike["kind"] && tLike["content"]
Output
2 tags
openxtalk
openxtalk
learning
true
true
7 +

A reply and a reaction come back unsigned, ready to sign with nxEventSign as any event.

Profiles and links

Your profile is an event too, of kind 0, with your name and a few words about you; post a new one to change it. nxMetadataBuild makes one, and nxMetadataParse reads it. To share a note, nxNeventEncode packs its id with a relay that has it, and nxUriEncode makes a nostr: link that apps open; nxEntityDecode reads any such name back:

ExampleRun it in the playground
put nxHexEncode(cxSha256(textEncode("alice learns nostr", "UTF-8"))) into tSecret
put nxEventSign(nxMetadataBuild("Alice", "Learning xTalk", empty, 1700000000), tSecret, empty) into tProfile
put tProfile["kind"]
put nxMetadataParse(tProfile) into tFields
put tFields["name"] & ":" && tFields["about"]

put nxEventSign(nxEventBuild(1, "Hello, Nostr!", empty, 1700000000), tSecret, empty) into tNote
put nxUriEncode(nxNeventEncode(tNote["id"], "wss://relay.example.com", nxKeyPublic(tSecret), 1)) into tLink
put char 1 to 14 of tLink
put nxEntityDecode(tLink) into tFields
put tFields["type"] && tFields["relays"]
put tFields["id"] is tNote["id"]
Output
0
Alice: Learning xTalk
nostr:nevent1q
nevent wss://relay.example.com
true

Private messages

Notes are public. For a private message, NIP-44 locks it so that only one person can read it. nxNip44ConversationKey shows the trick: your secret key with their public key gives the same shared key as their secret key with yours, and it is never sent. nxNip44Encrypt locks a message for them, and nxNip44Decrypt opens it, or gives empty with the wrong keys:

ExampleRun it in the playground
put nxHexEncode(cxSha256(textEncode("alice learns nostr", "UTF-8"))) into tAlice
put nxHexEncode(cxSha256(textEncode("bob learns too", "UTF-8"))) into tBob
put nxNip44ConversationKey(tAlice, nxKeyPublic(tBob)) is nxNip44ConversationKey(tBob, nxKeyPublic(tAlice))
-- Alice locks a message for Bob (empty: a fresh random nonce)
put nxNip44Encrypt(tAlice, nxKeyPublic(tBob), "Meet at noon", empty) into tLocked
put tLocked contains "noon"
put nxNip44Decrypt(tBob, nxKeyPublic(tAlice), tLocked)
put nxNip44Decrypt(tAlice, nxKeyPublic(tAlice), tLocked) is empty
Output
true
false
Meet at noon
true

Asking a relay

A relay answers filters: whose events, which kinds, how many. nxFilterBuild makes one from an array, and nxFilterMatches says whether an event fits, as a relay would decide. You talk to a relay in short JSON messages over a websocket: nxClientEvent publishes an event, nxClientReq asks with a filter under a name of your choice, and nxRelayParse reads the relay's answers, such as EOSE, "that is all, for now":

ExampleRun it in the playground
put nxHexEncode(cxSha256(textEncode("alice learns nostr", "UTF-8"))) into tSecret
put nxKeyPublic(tSecret) into tAsk["authors"]
put "1" into tAsk["kinds"]
put 10 into tAsk["limit"]
put nxFilterBuild(tAsk) into tFilter
put nxEventSign(nxEventBuild(1, "A note", empty, 1700000000), tSecret, empty) into tNote
put nxEventSign(nxReactionBuild(tNote, empty, 1700000060), tSecret, empty) into tLike
put nxFilterMatches(tFilter, tNote)
put nxFilterMatches(tFilter, tLike)

put char 1 to 16 of nxClientReq("feed", tFilter)
put nxClientClose("feed")
put nxRelayParse("[" & quote & "EOSE" & quote & "," & quote & "feed" & quote & "]") into tMessage
put tMessage["type"] && tMessage["subId"]
Output
true
false
["REQ","feed",{"
["CLOSE","feed"]
EOSE feed

The nostr-relay library that comes with NostrXT opens the websockets and sends these for you; Learn NostrXT (Help > Examples) and the Dictionary have the rest, proof of work included.

Try it yourself

Write a function trustedNotes that takes a list of events as JSON, a line each, and returns the text of only those that verify, a line each.

Show a solution
ExampleRun it in the playground
function trustedNotes pLines
   local tNotes, tEvent
   repeat for each line tJson in pLines
      put nxEventFromJson(tJson) into tEvent
      if nxEventVerify(tEvent) then
         put tEvent["content"] & return after tNotes
      end if
   end repeat
   return line 1 to -1 of tNotes
end trustedNotes

put nxHexEncode(cxSha256(textEncode("alice learns nostr", "UTF-8"))) into tSecret
put nxEventSign(nxEventBuild(1, "first", empty, 1700000000), tSecret, empty) into tOne
put nxEventSign(nxEventBuild(1, "second", empty, 1700000060), tSecret, empty) into tTwo
put "forged" into tTwo["content"]
put nxEventSign(nxEventBuild(1, "third", empty, 1700000120), tSecret, empty) into tThree
put nxEventToJson(tOne) & return & nxEventToJson(tTwo) & return & nxEventToJson(tThree) into tFeed
put trustedNotes(tFeed)
Output
first
third

Words in this lesson

Each one finds every lesson that uses it. The Dictionary has the whole story of each, with its syntax and more examples.