This project combines three key technologies:
- Nostr (decentralized messaging protocol)
- Ethereum EIP-191 signatures (secure cryptographic signing)
- VLC (Verifiable Logical Clock)
Together, these components enable:
- Decentralized authentication
- Secure message signing using Ethereum standards
- Distributed event ordering and causality tracking
- Message consistency in distributed systems
This integration creates a robust system for secure, decentralized communication with verifiable event ordering.
# bun
curl -fsSL https://bun.sh/install | bash
# install just for linux
curl -LO https://github.com/casey/just/releases/download/1.28.0/just-1.28.0-x86_64-unknown-linux-musl.tar.gz
tar -xvf just-1.28.0-x86_64-unknown-linux-musl.tar.gz
sudo mv just /usr/local/bin/
# or for macOS
brew install just
# compile by yourself
just build
# or install by npm
npm install --save nostr-tools
# jsr
npx jsr add @nostr/tools
If using TypeScript, this package requires TypeScript >= 5.0.
import { generateSecretKey, getPublicKey } from 'nostr-tools/pure'
let sk = generateSecretKey() // `sk` is a Uint8Array
let pk = getPublicKey(sk) // `pk` is a hex string
To get the secret key in hex format, use
import { bytesToHex, hexToBytes } from '@noble/hashes/utils' // already an installed dependency
let skHex = bytesToHex(sk)
let backToBytes = hexToBytes(skHex)
import { finalizeEvent, verifyEvent } from 'nostr-tools/pure'
let event = finalizeEvent({
kind: 1,
created_at: Math.floor(Date.now() / 1000),
tags: [],
content: 'hello',
}, sk)
let isGood = verifyEvent(event)
Doesn't matter what you do, you always should be using a SimplePool
:
import { finalizeEvent, generateSecretKey, getPublicKey } from 'nostr-tools/pure'
import { SimplePool } from 'nostr-tools/pool'
const pool = new SimplePool()
// let's query for an event that exists
const event = relay.get(
['wss://relay.example.com'],
{
ids: ['d7dd5eb3ab747e16f8d0212d53032ea2a7cadef53837e5a6c66d42849fcb9027'],
},
)
if (event) {
console.log('it exists indeed on this relay:', event)
}
// let's publish a new event while simultaneously monitoring the relay for it
let sk = generateSecretKey()
let pk = getPublicKey(sk)
pool.subscribe(
['wss://a.com', 'wss://b.com', 'wss://c.com'],
{
kinds: [1],
authors: [pk],
},
{
onevent(event) {
console.log('got event:', event)
}
}
)
let eventTemplate = {
kind: 1,
created_at: Math.floor(Date.now() / 1000),
tags: [],
content: 'hello world',
}
// this assigns the pubkey, calculates the event id and signs the event in a single step
const signedEvent = finalizeEvent(eventTemplate, sk)
await Promise.any(pool.publish(['wss://a.com', 'wss://b.com'], signedEvent))
relay.close()
To use this on Node.js you first must install ws
and call something like this:
import { useWebSocketImplementation } from 'nostr-tools/pool'
// or import { useWebSocketImplementation } from 'nostr-tools/relay' if you're using the Relay directly
import WebSocket from 'ws'
useWebSocketImplementation(WebSocket)
import * as nip27 from '@nostr/tools/nip27'
for (let block of nip27.parse(evt.content)) {
switch (block.type) {
case 'text':
console.log(block.text)
break
case 'reference': {
if ('id' in block.pointer) {
console.log("it's a nevent1 uri", block.pointer)
} else if ('identifier' in block.pointer) {
console.log("it's a naddr1 uri", block.pointer)
} else {
console.log("it's an npub1 or nprofile1 uri", block.pointer)
}
break
}
case 'url': {
console.log("it's a normal url:", block.url)
break
}
case 'image':
case 'video':
case 'audio':
console.log("it's a media url:", block.url)
case 'relay':
console.log("it's a websocket url, probably a relay address:", block.url)
default:
break
}
}
import { generateSecretKey, getPublicKey } from '@nostr/tools/pure'
import { BunkerSigner, parseBunkerInput } from '@nostr/tools/nip46'
import { SimplePool } from '@nostr/tools/pool'
// the client needs a local secret key (which is generally persisted) for communicating with the bunker
const localSecretKey = generateSecretKey()
// parse a bunker URI
const bunkerPointer = await parseBunkerInput('bunker://abcd...?relay=wss://relay.example.com')
if (!bunkerPointer) {
throw new Error('Invalid bunker input')
}
// create the bunker instance
const pool = new SimplePool()
const bunker = new BunkerSigner(localSecretKey, bunkerPointer, { pool })
await bunker.connect()
// and use it
const pubkey = await bunker.getPublicKey()
const event = await bunker.signEvent({
kind: 1,
created_at: Math.floor(Date.now() / 1000),
tags: [],
content: 'Hello from bunker!'
})
// cleanup
await signer.close()
pool.close([])
import * as nip10 from '@nostr/tools/nip10'
// event is a nostr event with tags
const refs = nip10.parse(event)
// get the root event of the thread
if (refs.root) {
console.log('root event:', refs.root.id)
console.log('root event relay hints:', refs.root.relays)
console.log('root event author:', refs.root.author)
}
// get the immediate parent being replied to
if (refs.reply) {
console.log('reply to:', refs.reply.id)
console.log('reply relay hints:', refs.reply.relays)
console.log('reply author:', refs.reply.author)
}
// get any mentioned events
for (let mention of refs.mentions) {
console.log('mentioned event:', mention.id)
console.log('mention relay hints:', mention.relays)
console.log('mention author:', mention.author)
}
// get any quoted events
for (let quote of refs.quotes) {
console.log('quoted event:', quote.id)
console.log('quote relay hints:', quote.relays)
}
// get any referenced profiles
for (let profile of refs.profiles) {
console.log('referenced profile:', profile.pubkey)
console.log('profile relay hints:', profile.relays)
}
import { queryProfile } from 'nostr-tools/nip05'
let profile = await queryProfile('jb55.com')
console.log(profile.pubkey)
// prints: 32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245
console.log(profile.relays)
// prints: [wss://relay.damus.io]
To use this on Node.js < v18, you first must install node-fetch@2
and call something like this:
import { useFetchImplementation } from 'nostr-tools/nip05'
useFetchImplementation(require('node-fetch'))
import type { WindowNostr } from 'nostr-tools/nip07'
declare global {
interface Window {
nostr?: WindowNostr;
}
}
import { generateSecretKey, getPublicKey } from 'nostr-tools/pure'
import * as nip19 from 'nostr-tools/nip19'
let sk = generateSecretKey()
let nsec = nip19.nsecEncode(sk)
let { type, data } = nip19.decode(nsec)
assert(type === 'nsec')
assert(data === sk)
let pk = getPublicKey(generateSecretKey())
let npub = nip19.npubEncode(pk)
let { type, data } = nip19.decode(npub)
assert(type === 'npub')
assert(data === pk)
let pk = getPublicKey(generateSecretKey())
let relays = ['wss://relay.nostr.example.mydomain.example.com', 'wss://nostr.banana.com']
let nprofile = nip19.nprofileEncode({ pubkey: pk, relays })
let { type, data } = nip19.decode(nprofile)
assert(type === 'nprofile')
assert(data.pubkey === pk)
assert(data.relays.length === 2)
nostr-wasm
is a thin wrapper over libsecp256k1 compiled to WASM just for hashing, signing and verifying Nostr events.
import { setNostrWasm, generateSecretKey, finalizeEvent, verifyEvent } from 'nostr-tools/wasm'
import { initNostrWasm } from 'nostr-wasm'
// make sure this promise resolves before your app starts calling finalizeEvent or verifyEvent
initNostrWasm().then(setNostrWasm)
// or use 'nostr-wasm/gzipped' or even 'nostr-wasm/headless',
// see https://www.npmjs.com/package/nostr-wasm for options
If you're going to use Relay
and SimplePool
you must also import nostr-tools/abstract-relay
and/or nostr-tools/abstract-pool
instead of the defaults and then instantiate them by passing the verifyEvent
:
import { setNostrWasm, verifyEvent } from 'nostr-tools/wasm'
import { AbstractRelay } from 'nostr-tools/abstract-relay'
import { AbstractSimplePool } from 'nostr-tools/abstract-pool'
import { initNostrWasm } from 'nostr-wasm'
initNostrWasm().then(setNostrWasm)
const relay = AbstractRelay.connect('wss://relayable.org', { verifyEvent })
const pool = new AbstractSimplePool({ verifyEvent })
This may be faster than the pure-JS noble libraries used by default and in nostr-tools/pure
. Benchmarks:
benchmark time (avg) (min … max) p75 p99 p995
------------------------------------------------- -----------------------------
• relay read message and verify event (many events)
------------------------------------------------- -----------------------------
wasm 34.94 ms/iter (34.61 ms … 35.73 ms) 35.07 ms 35.73 ms 35.73 ms
pure js 239.7 ms/iter (235.41 ms … 243.69 ms) 240.51 ms 243.69 ms 243.69 ms
trusted 402.71 µs/iter (344.57 µs … 2.98 ms) 407.39 µs 745.62 µs 812.59 µs
summary for relay read message and verify event
wasm
86.77x slower than trusted
6.86x faster than pure js
<script src="https://unpkg.com/nostr-tools/lib/nostr.bundle.js"></script>
<script>
window.NostrTools.generateSecretKey('...') // and so on
</script>
To develop nostr-tools
, install just
and run just -l
to see commands available.