{"id":13832646,"url":"https://github.com/franzos/nostr-ts","last_synced_at":"2026-02-11T20:28:13.984Z","repository":{"id":185424518,"uuid":"673493289","full_name":"franzos/nostr-ts","owner":"franzos","description":"Nostr Libraries (Web / Node) and Client (React / Node)","archived":false,"fork":false,"pushed_at":"2024-10-20T13:45:16.000Z","size":13525,"stargazers_count":13,"open_issues_count":6,"forks_count":4,"subscribers_count":1,"default_branch":"master","last_synced_at":"2024-10-30T04:55:41.167Z","etag":null,"topics":["nostr","nostr-client","nostr-protocol","react"],"latest_commit_sha":null,"homepage":"https://d2okqj4v2u9fts.cloudfront.net","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/franzos.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2023-08-01T18:49:10.000Z","updated_at":"2024-10-20T13:45:20.000Z","dependencies_parsed_at":"2023-12-18T23:31:48.498Z","dependency_job_id":"8bcec87b-fd81-4cdc-8865-62c57b5cb1c9","html_url":"https://github.com/franzos/nostr-ts","commit_stats":null,"previous_names":["franzos/nostr-ts"],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/franzos%2Fnostr-ts","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/franzos%2Fnostr-ts/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/franzos%2Fnostr-ts/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/franzos%2Fnostr-ts/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/franzos","download_url":"https://codeload.github.com/franzos/nostr-ts/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":225581798,"owners_count":17491791,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["nostr","nostr-client","nostr-protocol","react"],"created_at":"2024-08-04T11:00:25.744Z","updated_at":"2026-02-11T20:28:13.941Z","avatar_url":"https://github.com/franzos.png","language":"TypeScript","funding_links":[],"categories":["Libraries"],"sub_categories":["Client reviews and/or comparisons"],"readme":"# Nostr Library and Clients\n\nI wanted to learn more about Nostr, so I decided to implement libraries and clients.\n\n- `./client`: node client example\n- `./client-web`: react client example (`NWorker`, `RelayClient`, `NUser` React, Chakra, Zustand)\n- `./relay-docker`: high-performance gnost-relay docker image for local testing\n- `@nostr-ts/common`: `./packages/common`: common types and functions\n- `@nostr-ts/node`: `./packages/node`: client for usage with node `ws` library\n- `@nostr-ts/web`: `./packages/web`: client for usage with browser `WebSocket` API\n\nIf you want to know what I think about Nostr and how it compares to Mastodon, Matrix and others, checkout [this article](https://f-a.nz/gist/hello-nostr/).\n\n### Preview of the web client\n\nNostr web client built with React.\n\n- Relies on IndexedDB and local storage for data and accounts\n- implements `@nostr-ts/common` and `@nostr-ts/web`\n\nInitial support for `nos2x` and any other extention following NIP-07 is available.\n\nA new, live version builds from master on every commit: [https://d2okqj4v2u9fts.cloudfront.net](https://d2okqj4v2u9fts.cloudfront.net).\n\n![Preview](./client-web/preview.png)\n\n## Highlights\n\n- Supported NIP: 1, 2, 4, 10, 11, 13, 14, 18, 23, 25, 36, 39, 40, 42, 45, 56\n- Partial NIP: 19, 32, 57\n- `RelayClient` to handle websocket connection and message sending (node, web)\n- `RelayDiscovery` to make it easy to pickup new relays (node)\n- `NEvent` to assemble events (universal)\n- `NFilters` to filter events (universal)\n- `NUser` to handle user metadata (node, web - WIP)\n- `NWorker` to handle client-side processing and database (web)\n- `loadOrCreateKeypair` basic key handling (node, web)\n\nThe goal here is to make it as easy as possible to get started, so there's usually a convenience function for everything (NewShortTextNote, NewRecommendRelay, ...).\n\n### Storage\n\n- Sattelite CDN (web)\n\n## Usage notes\n\nOn Node.js use:\n\n```js\nimport { NewShortTextNote, NFilters } from \"@nostr-ts/common\";\nimport {\n  RelayClient,\n  RelayDiscovery,\n  loadOrCreateKeypair,\n  NUser,\n} from \"@nostr-ts/node\";\n```\n\ninstall with:\n\n```bash\n# or npm install, or yarn install\npnpm install @nostr-ts/common @nostr-ts/node\n```\n\nIn the browser use:\n\n```js\nimport { NewShortTextNote, NFilters } from \"@nostr-ts/common\";\nimport { RelayClient, loadOrCreateKeypair, NUser } from \"@nostr-ts/web\";\n```\n\ninstall with:\n\n```bash\n# or npm install, or yarn install\npnpm install @nostr-ts/common @nostr-ts/web\n```\n\nSo most types and utility functions comes from `@nostr-ts/common`, and anything related to file system, database or networking (requests), is in `@nostr-ts/node` and `@nostr-ts/web`.\n\n### Install \u0026 Build\n\n```bash\npnpm install -r\npnpm run build\n```\n\nThe build command will take care of `./packages/*`.\n\n## Features\n\n- [x] NIP-1: [Basic protocol flow description](https://github.com/nostr-protocol/nips/blob/master/01.md)\n\n**Generate a keypair** (Sign up):\n\n```js\nconst keypair = await loadOrCreateKeypair(\"./key\");\n```\n\n- This will look for a private key `./key` and a public key `./key.pub`\n- If they don't exist, they will be generated and saved to disk\n- If only the private key `./key` exists, a public key will be generated from it\n\n**Connect to the network**:\n\n```js\nlet client = new RelayClient([\n  {\n    url: \"wss://nostr.rocks\",\n    read: true,\n    write: true,\n  },\n  {\n    url: \"wss://nostr.lu.ke\",\n    read: true,\n    write: true,\n  },\n]);\nawait client.getRelayInformation();\n```\n\n**Send a message**:\n\n```js\nconst event = NewShortTextNote({ text: \"Hello nostr!\" });\nevent.signAndGenerateId(keypair);\nclient.sendEvent({ event });\n```\n\n**Receive messages**:\n\n```js\nconst filters = new NFilters();\nfilters.addAuthor(keypair.pub);\n\nclient.subscribe({\n  filters,\n});\n\nclient.listen((payload) =\u003e {\n  console.log(payload.meta.id, payload.meta.url);\n  logRelayMessage(payload.data);\n});\n```\n\n**Recommend a relay**\n\n```js\nconst event = NewRecommendRelay({\n  relayUrl: \"wss://nostr.rocks\",\n});\nevent.signAndGenerateId(keypair);\nclient.sendEvent({ event });\n```\n\n**Supported messages (events)**\n\n- `NewShortTextNote`: Send a short text note\n- `NewLongFormContent`: Send a long form content note\n- `NewShortTextNoteResponse`: Respond to a short text note\n- `NewReaction`: React to a note (`+`, `-`)\n- `NewQuoteRepost`: Repost a note\n- `NewGenericRepost`: Report any event\n- `NewUpdateUserMetadata`: Update user metadata (profile)\n- `NewRecommendRelay`: Recommend a relay\n- `NewReport`: Report an event or user\n- `NewZapRequest`: Request a zap\n- `NewSignedZapRequest`: Request a zap helper\n- `NewZapReceipt`: Zap receipt\n- `NewEventDeletion`: Delete an event\n\n**Event**\n\nYou can manually assemble an event:\n\n```js\nconst event = new NEvent({\n   kind: NEVENT_KIND_SHORT_TEXT_NOTE,\n   tags: [],\n   content: 'Hello nostr!',\n})\n\n// These are all the options; you do not (and usually should not) use all of them\n// If something doesn't add-up, these sometimes throw an error\nevent.addEventTag(...)\nevent.addPublicKeyTag(...)\nevent.addRelaysTag(...)\nevent.addEventCoordinatesTag(...)\nevent.addIdentifierTag(...)\nevent.addLnurlTag(...)\nevent.addAmountTag(...)\nevent.addKindTag(...)\nevent.addExpirationTag(...)\nevent.addSubjectTag(...)\nevent.addSubjectTag(makeSubjectResponse(subject));\nevent.addNonceTag(...)\nevent.addContentWarningTag(...)\nevent.addExternalIdentityClaimTag(...)\nevent.addReportTags(...)\n\n// Add custom tags\nevent.addTag(['p', 'myvalue'])\n\n// Mentions in event content; for ex. Checkout nostr:e21921600ecbcbea699a9f76c8156886bef112b71c4f79ce1b894386b5413466\nevent.mentionUsers([pubkey1, pubkey2])\nevent.hasMentions()\n\n// Sign\nevent.signAndGenerateId(keypair)\n\n// Ready to publish?\nconst ready = event.isReadyToPublish()\n\n// Required NIP? [13, 39, 40]\nconst nip = event.determineRequiredNIP()\n\n// Properties\nevent.hasPublicKeyTags()\nevent.hasRelaysTag()\nevent.hasEventCoordinatesTags()\nevent.hasIdentifierTags()\nevent.hasLnurlTags()\nevent.hasAmountTags()\nevent.hasExpirationTag()\nevent.hasSubjectTag()\nevent.hasNonceTag()\nevent.hasContentWarningTag()\nevent.hasExternalIdentityClaimTag()\nevent.hasReportTags()\n```\n\n- [x] NIP-2 [Contact List and Petnames](https://github.com/nostr-protocol/nips/blob/master/02.md)\n\n```js\nconst event = NewContactList({\n  contacts: [\n    {\n      key: \"5276ac499c9c6a353634d3d2cb6f4ada5167c3b886108ab4ddeb8ddf7b0fff70\",\n      relayUrl: \"wss://nostr.rocks\",\n      petname: \"nostrop\",\n    },\n  ],\n});\n```\n\n- [x] NIP-4 [Encrypted Direct Message](https://github.com/nostr-protocol/nips/blob/master/04.md)\n\n```js\nconst event = NewEncryptedPrivateMessage({\n    text: \"Let's make this secret plan happen!\",\n    recipientPubkey: \"...\",\n});\nconst encEv = await encryptEvent(event, keypair);\nevent.content = encEv.content as string;\nevent.signAndGenerateId(keypair);\n```\n\nThis is a bit ugly, as I did not want to include the encryption library in the common package.\n\nHere's what it might looks like:\n\n```js\nimport crypto from \"crypto\";\nimport { getSharedSecret } from \"@noble/secp256k1\";\n\nexport async function encryptEvent(\n  event: EventBase,\n  keyPair: {\n    privateKey: string,\n    publicKey: string,\n  }\n) {\n  const recipientPublicKey = event.tags ? event.tags[0][1] : undefined;\n  if (!recipientPublicKey) {\n    throw new Error(\n      \"No recipient public key set. Did you use NewEncryptedPrivateMessage?\"\n    );\n  }\n\n  let sharedPoint = await getSharedSecret(\n    keyPair.privateKey,\n    \"02\" + recipientPublicKey\n  );\n  let sharedX = sharedPoint.slice(1, 33);\n\n  let iv = crypto.randomFillSync(new Uint8Array(16));\n  var cipher = crypto.createCipheriv(\"aes-256-cbc\", Buffer.from(sharedX), iv);\n  let encryptedMessage = cipher.update(event.content || \"\", \"utf8\", \"base64\");\n  encryptedMessage += cipher.final(\"base64\");\n  let ivBase64 = Buffer.from(iv.buffer).toString(\"base64\");\n\n  event.content = encryptedMessage + \"?iv=\" + ivBase64;\n\n  return event;\n}\n```\n\n_Adapted from this example: [github.com/nostr-protocol/nips/blob/master/04](https://github.com/nostr-protocol/nips/blob/master/04.md)._\n\n- [x] NIP-11 [Relay Information Document](https://github.com/nostr-protocol/nips/blob/master/11.md)\n\n```js\nconst infos = await client.getRelayInformation();\n```\n\nBased on this information the client decides whether to publish to a rely:\n\n```\nneededNips [ 40 ]\nsupportedNips [\n   1,  2,  4,  9, 11,\n  12, 15, 16, 20, 22,\n  28, 33\n]\nEvent a04308c18a5f73b97be1f66fddba1741dd8dcf8a057701a2b4f1713d557ae384 not published to wss://nostr.wine because not all needed NIPS are supported.\n```\n\n- [x] NIP-13 [Proof of work](https://github.com/nostr-protocol/nips/blob/master/13.md)\n\n```js\nconst difficulty = 28;\nconst event = NewShortTextNote({\n  text: \"Let's have a discussion about Bitcoin!\",\n});\nevent.pubkey = keypair.pub;\nevent.proofOfWork(difficulty);\nevent.sign();\n```\n\nIf you need anything above ~20 bits and work in the browser, there's a helper function for web worker (`proofOfWork(event, bits)`):\n\n```js\n// pow-worker.ts\nimport { proofOfWork } from \"@nostr-ts/common\";\n\nself.onmessage = function (e) {\n  const data = e.data;\n  const result = proofOfWork(data.event, data.bits);\n  self.postMessage({ result });\n};\n\n// client.ts\nreturn new Promise((resolve, reject) =\u003e {\n  const worker = new Worker(new URL(\"./pow-worker.ts\", import.meta.url), {\n    type: \"module\",\n  });\n\n  // Setup an event listener to receive results from the worker\n  worker.onmessage = function (e) {\n    resolve(e.data.result);\n    // Terminate the worker after receiving the result\n    worker.terminate();\n  };\n\n  // Send a message to the worker to start the calculation\n  worker.postMessage({\n    event: event,\n    bits: bits,\n  });\n});\n```\n\n- [x] NIP-14 [Subject tag in Text events](https://github.com/nostr-protocol/nips/blob/master/14.md)\n\n```js\nconst event = NewShortTextNote({\n  text: \"Let's have a discussion about Bitcoin!\",\n});\nevent.addSubjectTag(\"All things Bitcoin\");\n```\n\nIf you want to respond to a note, keeping the subject:\n\n```js\nconst inResponseTo = {\n  id: \"e21921600ecbcbea699a9f76c8156886bef112b71c4f79ce1b894386b5413466\",\n  pubkey: \"5276ac499c9c6a353634d3d2cb6f4ada5167c3b886108ab4ddeb8ddf7b0fff70\",\n  created_at: 1690881792,\n  kind: 1,\n  tags: [[\"subject\", \"All things Bitcoin\"]],\n  content: \"Let's have a discussion about Bitcoin!\",\n  sig: \"6cee8c1d11ca5f8c7a0bd9839d0af5d3af3cc6a5de754fc449d34188c0066eee3e5b5b4e567cd77a2e0369f8c9525d60e064db175acd02d9c5374c3c0e912969\",\n};\nconst relayUrl = \"wss://nostr.rocks\";\nconst event = NewShortTextNoteResponse({\n  text: \"Sounds like a great idea. What do you think about the Lightning Network?\",\n  inResponseTo,\n  relayUrl,\n});\n```\n\nIf this is the first response, we prepend the subject with `Re: ` automatically. So you'd be responding with subject `Re: All things Bitcoin`.\n\n- [x] NIP-18 [Reposts](https://github.com/nostr-protocol/nips/blob/master/18.md)\n\n```js\nconst inReponseTo = {\n  id: \"e21921600ecbcbea699a9f76c8156886bef112b71c4f79ce1b894386b5413466\",\n  pubkey: \"5276ac499c9c6a353634d3d2cb6f4ada5167c3b886108ab4ddeb8ddf7b0fff70\",\n  created_at: 1690881792,\n  kind: 1,\n  tags: [],\n  content:\n    \"Hello everyone! I am working on a new ts library for nostr. This is just a test.\",\n  sig: \"6cee8c1d11ca5f8c7a0bd9839d0af5d3af3cc6a5de754fc449d34188c0066eee3e5b5b4e567cd77a2e0369f8c9525d60e064db175acd02d9c5374c3c0e912969\",\n};\nconst event = NewQuoteRepost({\n  relayUrl: \"https://nostr.rocks\",\n  inReponseTo,\n});\nevent.signAndGenerateId(keypair);\nclient.sendEvent({ event });\n```\n\nYou can also utilize `NewGenericRepost` to repost any kind of event.\n\n- [x] NIP-19 [bech32-encoded entities](https://github.com/nostr-protocol/nips/blob/master/19.md)\n\nThere are some helpers to get you started:\n\n- `encodeBech32(...)`\n- `decodeBech32(...)`\n\nShortcuts to the above, for specific use cases:\n\n- `bechEncodeProfile(pubkey, relayUrls)` (returns ex. `nprofile...`)\n- `bechEncodePublicKey(pubkey)`\n- `bechEncodePrivateKey(privkey)`\n- `decodeNostrPublicKeyString(nostr:npub...)` (returns public key)\n- `decodeNostrPrivateKeyString(nostr:nsec...)`\n- `decodeNostrProfileString(nostr:nprofile...)`\n- `encodeNostrString(prefix, tlvItems)` (returns ex. `nostr:npub...`)\n\nExamples public keys:\n\n```js\nconst src =\n  \"nostr:npub1kade5vf37snr4hv5hgstav6j5ygry6z09kkq0flp47p8cmeuz5zs7zz2an\";\nconst resO1 = decodeNostrPublicKeyString(src);\n// res = b75b9a3131f4263add94ba20beb352a11032684f2dac07a7e1af827c6f3c1505\n\nconst resO2 = decodeNostrUrl(src);\n// res = [{ prefix: 'npub', tlvItems: [{ type: 0, value: \"b75b9a3131f4263add94ba20beb352a11032684f2dac07a7e1af827c6f3c1505\" }] }]\n```\n\nExample profile:\n\n```js\nconst pubkey =\n  \"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d\";\nconst relays = [\"wss://r.x.com\", \"wss://djbas.sadkb.com\"];\nconst res01 = bechEncodeProfile(pubkey, relays);\n// res01 = nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p\n\nconst res02 = makeNostrProfileString(pubkey, relays);\n// res02 = nostr:nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p\n```\n\n- [x] NIP-23 [Long-form Content](https://github.com/nostr-protocol/nips/blob/master/23.md)\n\n```js\nconst event = NewLongFormContent({\n  text: \"This is a really long one. Like I mean, not your usual short note. This is a long one. I mean, really long. Like, really really long. Like, really really really long. Like, really really really really long. Like, really really really really really long. Like, really really really really really really long.\"\n  isDraft: false,\n  identifier: \"really-really-really-long\"\n})\n```\n\n- [x] NIP-25: [Reactions](https://github.com/nostr-protocol/nips/blob/master/25.md)\n\n```js\nconst event = NewReaction({\n  text: \"+\",\n  inResponseTo: {\n    id: \"e21921600ecbcbea699a9f76c8156886bef112b71c4f79ce1b894386b5413466\",\n    pubkey: \"5276ac499c9c6a353634d3d2cb6f4ada5167c3b886108ab4ddeb8ddf7b0fff70\",\n  },\n});\n\nevent.signAndGenerateId(keypair);\nclient.sendEvent({ event });\n```\n\n- [x] NIP-36 [Sensitive Content / Content Warning](https://github.com/nostr-protocol/nips/blob/master/36.md)\n\n```js\nconst event = NewShortTextNote({\n  text: \"This is a test note with explicit language.\",\n});\nevent.addContentWarningTag(\"explicit language\");\n```\n\n- [x] NIP-39 [External Identities in Profiles](https://github.com/nostr-protocol/nips/blob/master/39.md#nip-39)\n\n```js\nconst githubClaim = new ExternalIdentityClaim({\n  type: IDENTITY_CLAIM_TYPE.GITHUB,\n  identity: \"semisol\",\n  proof: \"9721ce4ee4fceb91c9711ca2a6c9a5ab\",\n});\n\nconst event = NewUpdateUserMetadata({\n  claims: [githubClaim],\n  userMetadata: {\n    name: \"Semisol\",\n  },\n});\n\nevent.signAndGenerateId(keypair);\nclient.sendEvent({ event });\n```\n\n- [x] NIP-40 [Expiration Timestamp](https://github.com/nostr-protocol/nips/blob/master/40.md)\n\n```js\nconst event = NewShortTextNote({ text: \"Meeting starts in 10 minutes ...\" });\nevent.addExpirationTag(1690990889);\n```\n\n- [x] NIP-42 [Authentication of clients to relays](https://github.com/nostr-protocol/nips/blob/master/42.md)\n\nAs far as I understand, relays should send the auth challenge either on connection, or when required.\nThe relay I'm testing with (gnost-relay) sends it on connection.\n\nHere's how you can respond to the challenge:\n\n```js\nconst challenge = \"abc\";\nconst event = NewAuthEvent({\n  relayUrl: \"wss://nostr-ts.relay\",\n  challenge: challenge,\n});\nevent.signAndGenerateId(keypair);\nclient.subscribe({\n  type: CLIENT_MESSAGE_TYPE.AUTH,\n  signedEvent: JSON.stringify(event.ToObj()),\n});\n```\n\n- [x] NIP-56 [Reporting](https://github.com/nostr-protocol/nips/blob/master/56.md)\n\nThe `publicKey` usually refers to the user that is being reported.\nIf the report refers to another event, use the `eventId` too (for ex. spam, illegal, profanity, nudity).\n\nImpersonation:\n\n```js\nconst event = NewReport({\n  publicKey: \"5276ac499c9c6a353634d3d2cb6f4ada5167c3b886108ab4ddeb8ddf7b0fff70\",\n  kind: NREPORT_KIND.IMPERSONATION,\n});\n```\n\nSpam:\n\n```js\nconst event = NewReport({\n  publicKey: \"5276ac499c9c6a353634d3d2cb6f4ada5167c3b886108ab4ddeb8ddf7b0fff70\",\n  eventId: \"e21921600ecbcbea699a9f76c8156886bef112b71c4f79ce1b894386b5413466\",\n  kind: NREPORT_KIND.SPAM,\n  // optionally pass some text\n  content: \"This is spam\",\n});\n```\n\n- [x] NIP-57 [Lightning Zaps](https://github.com/nostr-protocol/nips/blob/master/57.md)\n\nThis is a really rudimentary example to show the steps required.\nI will follow-up with a more realistig implementation.\n\nSupports:\n\n- Zap to a user: YES\n- Zap to from / to event: (just make sure you include the event ID in the event)\n\n```js\nconst recipient = new NUser({\n  pubkey: \"5276ac499c9c6a353634d3d2cb6f4ada5167c3b886108ab4ddeb8ddf7b0fff70\",\n});\n\n// Get filters for subscription to get user information\nconst filters = recipient.getMetadataFilter();\n\nlet client = new RelayClient([\n  {\n    url: \"wss://nostr.rocks\",\n    read: true,\n    write: true,\n  },\n  {\n    url: \"wss://nostr.lu.ke\",\n    read: true,\n    write: true,\n  },\n]);\nawait client.getRelayInformation();\n\nclient.subscribe({\n  filters,\n});\n\nclient.listen(async (payload) =\u003e {\n  console.log(payload.meta.id, payload.meta.url);\n  logRelayMessage(payload.data);\n\n  // Don't actually do exactly this\n  // for ex. if you're subscribed to multiple relays, you'll generate multiple payments\n  // This should be part of client logic\n  if (payload.data[0] === RELAY_MESSAGE_TYPE.EVENT) {\n    // Load user data from event\n    const success = recipient.fromEvent(payload.data[2]);\n\n    if (success) {\n      // Make ZAP request\n      const { pr: invoice, event } = await recipient.makeZapRequest(\n        {\n          relayUrls: [\"wss://nostr.rocks\"],\n          amount: 1000,\n        },\n        keypair\n      );\n\n      // Pay invoice with lightning wallet then continue here\n      const bolt11FromYourWallet = \"lnbc1...\";\n\n      const receipt = event.newZapReceipt({\n        bolt11: bolt11FromYourWallet,\n        description: \"Keep stacking sats!\",\n      });\n      receipt.signAndGenerateId(keypair);\n      client.sendEvent({ receipt });\n    }\n  }\n});\n```\n\n## Examples\n\n### Collect a list of recommended relays\n\n1. Setup a filter for kind 2\n2. Subscribe with the filter\n3. Pass incoming events to discovery\n4. Save to json file\n\n```js\nimport { CLIENT_MESSAGE_TYPE, EventsRequest, NEVENT_KIND, NFilters } from \"@nostr-ts/common\";\nimport {\n  RelayClient,\n  RelayDiscovery,\n} from \"@nostr-ts/node\";\n\nconst main = async () =\u003e {\n  let client = new RelayClient([\n    {\n      url: \"wss://nostr.rocks\",\n      read: true,\n      write: true,\n    },\n    {\n      url: \"wss://nostr.lu.ke\",\n      read: true,\n      write: true,\n    },\n  ]);\n\n  const relayDiscovery = new RelayDiscovery();\n  const filters = new NFilters();\n  filters.addKind(NEVENT_KIND.RECOMMEND_RELAY);\n\n  const request: EventsRequest = {\n    type: CLIENT_MESSAGE_TYPE.REQ,\n    filters,\n    options: {\n      timeoutIn: 10000,\n    }\n  }\n\n  client.subscribe(request);\n\n  client.listen(async (payload) =\u003e {\n    await relayDiscovery.add(payload.data);\n  });\n\n  await client.getRelayInformation();\n  await new Promise((resolve) =\u003e setTimeout(resolve, 1 * 30 * 1000)).then(\n    async () =\u003e {\n      client.disconnect();\n      await relayDiscovery.saveToFile();\n    }\n  );\n};\n\nmain();\n```\n\nYou will get two files\n\n1. `discovered-relays.json` with all valid relays\n2. `discovered-relays-error.json` with all invalid relays\n\nThis is what an excerpt of `discovered-relays.json` looks like (a more complete one is included in this repo):\n\n```json\n[\n  {\n    \"url\": \"wss://relay.nostrplebs.com\",\n    \"info\": {\n      \"contact\": \"nostr@semisol.dev\",\n      \"description\": \"Nostr Plebs paid relay.\",\n      \"name\": \"relay.nostrplebs.com\",\n      \"pubkey\": \"52b4a076bcbbbdc3a1aefa3735816cf74993b1b8db202b01c883c58be7fad8bd\",\n      \"software\": \"git+https://github.com/hoytech/strfry.git\",\n      \"supported_nips\": [1, 9, 11, 12, 15, 16, 20, 22],\n      \"version\": \"v92-84ba68b\"\n    }\n  },\n  {\n    \"url\": \"wss://nostr-pub.wellorder.net\",\n    \"info\": {\n      \"id\": \"wss://nostr-pub.wellorder.net/\",\n      \"name\": \"Public Wellorder Relay\",\n      \"description\": \"Public relay for nostr development and use.\",\n      \"pubkey\": \"35d26e4690cbe1a898af61cc3515661eb5fa763b57bd0b42e45099c8b32fd50f\",\n      \"contact\": \"mailto:relay@wellorder.net\",\n      \"supported_nips\": [1, 2, 9, 11, 12, 15, 16, 20, 22, 33, 40, 42],\n      \"software\": \"https://git.sr.ht/~gheartsfield/nostr-rs-relay\",\n      \"version\": \"0.8.9\",\n      \"limitation\": {\n        \"payment_required\": false\n      }\n    }\n  },\n  {\n    \"url\": \"wss://relay.nostrview.com\",\n    \"info\": {\n      \"name\": \"relay.nostrview.com\",\n      \"description\": \"Nostrview relay\",\n      \"pubkey\": \"2e9397a8c9268585668b76479f88e359d0ee261f8e8ea07b3b3450546d1601c8\",\n      \"contact\": \"2e9397a8c9268585668b76479f88e359d0ee261f8e8ea07b3b3450546d1601c8\",\n      \"supported_nips\": [\n        1, 2, 4, 9, 11, 12, 15, 16, 20, 22, 26, 28, 33, 40, 111\n      ],\n      \"software\": \"git+https://github.com/Cameri/nostream.git\",\n      \"version\": \"1.22.2\",\n      \"limitation\": {\n        \"max_message_length\": 524288,\n        \"max_subscriptions\": 10,\n        \"max_filters\": 10,\n        \"max_limit\": 5000,\n        \"max_subid_length\": 256,\n        \"min_prefix\": 4,\n        \"max_event_tags\": 2500,\n        \"max_content_length\": 102400,\n        \"min_pow_difficulty\": 0,\n        \"auth_required\": false,\n        \"payment_required\": true\n      },\n      \"payments_url\": \"https://relay.nostrview.com/invoices\",\n      \"fees\": {\n        \"admission\": [\n          {\n            \"amount\": 4000000,\n            \"unit\": \"msats\"\n          }\n        ]\n      }\n    }\n  }\n]\n```\n\nand here's `discovered-relays-error.json`:\n\n```json\n[\n  {\n    \"url\": \"wss://nostr.rocks\"\n  },\n  {\n    \"url\": \"wss://rsslay.fiatjaf.com\"\n  },\n  {\n    \"url\": \"wss://nostr.rdfriedl.com\"\n  },\n  {\n    \"url\": \"wss://expensive-relay.fiatjaf.com\"\n  },\n  {\n    \"url\": \"wss://relayer.fiatjaf.com\"\n  },\n  {\n    \"url\": \"wss://nostr-relay.wlvs.space\"\n  }\n]\n```\n\n### Use list of relays with Relay Client\n\nOnce you've collected a list of relays, you can feed them to Relay Client.\n\nA couple of points:\n\n- You might not want to connect to hundreds of relays at once\n- I will add some randomization and limits in the future\n\n```js\nconst client = new RelayClient();\nconst relayDiscovery = new RelayDiscovery();\nawait relayDiscovery.loadFromFile();\n\nawait client.loadFromDiscovered(relayDiscovery.get());\n\n// Now continue as usual ...\nconst filters = new NFilters();\nfilters.addKind(1);\n\nclient.subscribe({\n  filters,\n});\n\nclient.listen(async (payload) =\u003e {\n  logRelayMessage(payload.data);\n});\n\nawait client.getRelayInformation();\n```\n\nIf you prefer to apply limits yourself, you could do something like this:\n\n```js\nconst relays = relayDiscovery.get().slice(0, 10);\nawait client.loadFromDiscovered(relays);\n```\n\n### Sattelite CDN\n\nThe `@nostr-ts/web` package includes a [Sattelite CDN](https://github.com/lovvtide/satellite-web/blob/master/docs/cdn.md) implementation.\n\n```js\nconst keypair = generateClientKeys();\n\n// Request credit (1 GB)\nconst request = sCDNCreditRequest(1);\nrequest.signAndGenerateId(keypair);\n\n// Get terms\nconst terms = await sCDNGetTerms(request);\n\n// Accept terms (sign)\n// as of writing, the amount would be 184000 msats\nconst payment = new NEvent(terms.payment);\npayment.signAndGenerateId(keypair);\n\n// Get invoice\nconst invoice = await sCDNGetInvoice(terms, payment);\n\n// invoice.pr contains the lightning invoice\n```\n\n## Notes\n\nIf you're new to Nostr, also checkout [awesome-nostr](https://github.com/aljazceru/awesome-nostr).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffranzos%2Fnostr-ts","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffranzos%2Fnostr-ts","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffranzos%2Fnostr-ts/lists"}