345 lines
7.6 KiB
Markdown
345 lines
7.6 KiB
Markdown
# TS-Roots-WS - Nostr WebSocket Transport for TypeScript
|
|
|
|
Source: https://git.wisehodl.dev/jay/ts-roots-ws
|
|
|
|
Mirror: https://github.com/wisehodl/ts-roots-ws
|
|
|
|
## What this library does
|
|
|
|
`ts-roots-ws` is a consensus-layer Nostr protocol websocket transport library for TypeScript. It only provides primitives for working with Nostr protocol websocket connection states and messages:
|
|
|
|
- WebSocket Connection States
|
|
- Envelope Structure
|
|
- Message Validation
|
|
- Protocol Message Creation
|
|
- Protocol Message Parsing
|
|
- Standard Label Handling
|
|
|
|
## What this library does not do
|
|
|
|
`ts-roots-ws` serves as a foundation for other libraries and applications to implement higher level transport abstractions on top of it, including:
|
|
|
|
- Connection Management
|
|
- Event Loops
|
|
- Subscription Handling
|
|
- State Management
|
|
- Reconnection Logic
|
|
|
|
## Installation
|
|
|
|
1. Add `ts-roots-ws` to your project:
|
|
|
|
```bash
|
|
npm install @wisehodl/roots-ws
|
|
```
|
|
|
|
2. Import the packages:
|
|
|
|
```typescript
|
|
import {
|
|
encloseEvent,
|
|
encloseSubscriptionEvent,
|
|
findEvent,
|
|
findSubscriptionEvent,
|
|
getLabel,
|
|
isStandardLabel
|
|
} from "@wisehodl/roots-ws/envelope";
|
|
|
|
import { ConnectionStatus } from "@wisehodl/roots-ws";
|
|
import { InvalidJSONError, WrongEnvelopeLabelError } from "@wisehodl/roots-ws/errors";
|
|
```
|
|
|
|
3. Access functions with appropriate namespaces.
|
|
|
|
## Usage Examples
|
|
|
|
### Envelope Creation
|
|
|
|
#### Create EVENT envelope
|
|
|
|
```typescript
|
|
// Create an event using ts-roots
|
|
import { Event } from "@wisehodl/roots/events";
|
|
|
|
const event: Event = {
|
|
id: "abc123",
|
|
pubkey: "def456",
|
|
kind: 1,
|
|
content: "Hello Nostr!",
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: [],
|
|
sig: ""
|
|
};
|
|
|
|
// Convert to JSON
|
|
const eventJSON = JSON.stringify(event);
|
|
|
|
// Create envelope
|
|
const env = encloseEvent(eventJSON);
|
|
// Result: ["EVENT",{"id":"abc123","pubkey":"def456","kind":1,"content":"Hello Nostr!","created_at":1636394097}]
|
|
```
|
|
|
|
#### Create subscription EVENT envelope
|
|
|
|
```typescript
|
|
// Create an event using ts-roots
|
|
import { Event } from "@wisehodl/roots/events";
|
|
|
|
const event: Event = {
|
|
id: "abc123",
|
|
pubkey: "def456",
|
|
kind: 1,
|
|
content: "Hello Nostr!",
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: [],
|
|
sig: ""
|
|
};
|
|
|
|
// Convert to JSON
|
|
const eventJSON = JSON.stringify(event);
|
|
|
|
// Create envelope with subscription ID
|
|
const subID = "sub1";
|
|
const env = encloseSubscriptionEvent(subID, eventJSON);
|
|
// Result: ["EVENT","sub1",{"id":"abc123","pubkey":"def456","kind":1,"content":"Hello Nostr!","created_at":1636394097}]
|
|
```
|
|
|
|
#### Create REQ envelope
|
|
|
|
```typescript
|
|
// Create filters using ts-roots
|
|
import { Filter } from "@wisehodl/roots/filters";
|
|
|
|
const since = Math.floor(Date.now() / 1000) - (24 * 60 * 60);
|
|
const limit = 50;
|
|
|
|
const filter1: Filter = {
|
|
kinds: [1],
|
|
limit: limit,
|
|
since: since
|
|
};
|
|
|
|
const filter2: Filter = {
|
|
authors: ["def456"]
|
|
};
|
|
|
|
// Convert to JSON
|
|
const filter1JSON = JSON.stringify(filter1);
|
|
const filter2JSON = JSON.stringify(filter2);
|
|
|
|
// Create envelope
|
|
const subID = "sub1";
|
|
const filtersJSON = [filter1JSON, filter2JSON];
|
|
const env = encloseReq(subID, filtersJSON);
|
|
// Result: ["REQ","sub1",{"kinds":[1],"limit":50,"since":1636307697},{"authors":["def456"]}]
|
|
```
|
|
|
|
#### Create other envelope types
|
|
|
|
```typescript
|
|
// Create CLOSE envelope
|
|
const env1 = encloseClose("sub1");
|
|
// Result: ["CLOSE","sub1"]
|
|
|
|
// Create EOSE envelope
|
|
const env2 = encloseEOSE("sub1");
|
|
// Result: ["EOSE","sub1"]
|
|
|
|
// Create NOTICE envelope
|
|
const env3 = encloseNotice("This is a notice");
|
|
// Result: ["NOTICE","This is a notice"]
|
|
|
|
// Create OK envelope
|
|
const env4 = encloseOK("abc123", true, "Event accepted");
|
|
// Result: ["OK","abc123",true,"Event accepted"]
|
|
|
|
// Create AUTH challenge
|
|
const env5 = encloseAuthChallenge("random-challenge-string");
|
|
// Result: ["AUTH","random-challenge-string"]
|
|
|
|
// Create AUTH response
|
|
import { Event } from "@wisehodl/roots/events";
|
|
|
|
const authEvent: Event = {
|
|
id: "abc123",
|
|
pubkey: "def456",
|
|
kind: 22242,
|
|
content: "",
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: [],
|
|
sig: ""
|
|
};
|
|
|
|
// Convert to JSON
|
|
const authEventJSON = JSON.stringify(authEvent);
|
|
|
|
// Create envelope
|
|
const env6 = encloseAuthResponse(authEventJSON);
|
|
// Result: ["AUTH",{"id":"abc123","pubkey":"def456","kind":22242,"content":"","created_at":1636394097}]
|
|
```
|
|
|
|
---
|
|
|
|
### Envelope Parsing
|
|
|
|
#### Extract label from envelope
|
|
|
|
```typescript
|
|
const env = `["EVENT",{"id":"abc123","pubkey":"def456","kind":1,"content":"Hello Nostr!"}]`;
|
|
try {
|
|
const label = getLabel(env);
|
|
// label: "EVENT"
|
|
|
|
// Check if label is standard
|
|
const isStandard = isStandardLabel(label);
|
|
// isStandard: true
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
```
|
|
|
|
#### Extract event from EVENT envelope
|
|
|
|
```typescript
|
|
const env = `["EVENT",{"id":"abc123","pubkey":"def456","kind":1,"content":"Hello Nostr!"}]`;
|
|
try {
|
|
const eventObj = findEvent(env);
|
|
|
|
// Parse into ts-roots Event if needed
|
|
import { Event, validate } from "@wisehodl/roots/events";
|
|
|
|
// Validate the event
|
|
try {
|
|
validate(eventObj as Event);
|
|
} catch (err) {
|
|
console.error(`Invalid event: ${err.message}`);
|
|
}
|
|
|
|
// Now you can access event properties
|
|
console.log(eventObj.id, eventObj.kind, eventObj.content);
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
```
|
|
|
|
#### Extract subscription event
|
|
|
|
```typescript
|
|
const env = `["EVENT","sub1",{"id":"abc123","pubkey":"def456","kind":1,"content":"Hello Nostr!"}]`;
|
|
try {
|
|
const [subID, eventObj] = findSubscriptionEvent(env);
|
|
|
|
// Parse into ts-roots Event if needed
|
|
import { Event } from "@wisehodl/roots/events";
|
|
|
|
console.log(`Subscription: ${subID}, Event ID: ${eventObj.id}`);
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
```
|
|
|
|
#### Extract subscription request
|
|
|
|
```typescript
|
|
const env = `["REQ","sub1",{"kinds":[1],"limit":50},{"authors":["def456"]}]`;
|
|
try {
|
|
const [subID, filtersObj] = findReq(env);
|
|
|
|
// Parse each filter
|
|
import { Filter } from "@wisehodl/roots/filters";
|
|
|
|
// Now you can use the filter objects
|
|
filtersObj.forEach((filter, i) => {
|
|
console.log(`Filter ${i}: `, filter);
|
|
});
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
```
|
|
|
|
#### Extract other envelope types
|
|
|
|
```typescript
|
|
// Extract OK response
|
|
const env1 = `["OK","abc123",true,"Event accepted"]`;
|
|
try {
|
|
const [eventID, status, message] = findOK(env1);
|
|
// eventID: "abc123"
|
|
// status: true
|
|
// message: "Event accepted"
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
|
|
// Extract EOSE message
|
|
const env2 = `["EOSE","sub1"]`;
|
|
try {
|
|
const subID = findEOSE(env2);
|
|
// subID: "sub1"
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
|
|
// Extract CLOSE message
|
|
const env3 = `["CLOSE","sub1"]`;
|
|
try {
|
|
const subID = findClose(env3);
|
|
// subID: "sub1"
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
|
|
// Extract CLOSED message
|
|
const env4 = `["CLOSED","sub1","Subscription complete"]`;
|
|
try {
|
|
const [subID, message] = findClosed(env4);
|
|
// subID: "sub1"
|
|
// message: "Subscription complete"
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
|
|
// Extract NOTICE message
|
|
const env5 = `["NOTICE","This is a notice"]`;
|
|
try {
|
|
const message = findNotice(env5);
|
|
// message: "This is a notice"
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
|
|
// Extract AUTH challenge
|
|
const env6 = `["AUTH","random-challenge-string"]`;
|
|
try {
|
|
const challenge = findAuthChallenge(env6);
|
|
// challenge: "random-challenge-string"
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
|
|
// Extract AUTH response
|
|
const env7 = `["AUTH",{"id":"abc123","pubkey":"def456","kind":22242,"content":""}]`;
|
|
try {
|
|
const authEvent = findAuthResponse(env7);
|
|
|
|
// Parse into ts-roots Event if needed
|
|
import { Event } from "@wisehodl/roots/events";
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
```
|
|
|
|
## Testing
|
|
|
|
This library contains a comprehensive suite of unit tests. Run them with:
|
|
|
|
```bash
|
|
npm test
|
|
```
|
|
|
|
Or for a single run:
|
|
|
|
```bash
|
|
npm run test:run
|
|
```
|