diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 403bfd3..24f7d21 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -108,7 +108,7 @@ jobs: - name: Setup dotnet uses: actions/setup-dotnet@v4 with: - dotnet-version: '9.0.x' + dotnet-version: '10.0.x' - name: Check working-directory: snippets/csharp diff --git a/snippets/csharp/Nip09.cs b/snippets/csharp/Nip09.cs new file mode 100644 index 0000000..7bea384 --- /dev/null +++ b/snippets/csharp/Nip09.cs @@ -0,0 +1,22 @@ +namespace Snippets; + +using Nostr.Sdk; + +public class Nip09 +{ + public static void Run() + { + var keys = Keys.Parse("3501454135014541350145413501453fefb02227e449e57cf4d3a3ce05378683"); + + // ANCHOR: build + // Construct the request + var id = EventId.Parse("7469af3be8c8e06e1b50ef1caceba30392ddc0b6614507398b7d7daa4c218e96"); + var request = new EventDeletionRequest(ids: [id], coordinates: [], reason: "these posts were published by accident"); + + // Build the event + Event e = EventBuilder.Delete(request).SignWithKeys(keys); + // ANCHOR_END: build + + Console.WriteLine($"Event deletion request: {e.AsPrettyJson()}"); + } +} \ No newline at end of file diff --git a/snippets/csharp/Program.cs b/snippets/csharp/Program.cs index c73c55a..5ad3226 100644 --- a/snippets/csharp/Program.cs +++ b/snippets/csharp/Program.cs @@ -19,6 +19,9 @@ public static async Task Main() // Event Building await EventBuilding.Build(); + // NIP09 + Nip09.Run(); + // NIP44 Nip44.Run(); diff --git a/snippets/csharp/Snippets.csproj b/snippets/csharp/Snippets.csproj index eb4e6bd..2b6f06e 100644 --- a/snippets/csharp/Snippets.csproj +++ b/snippets/csharp/Snippets.csproj @@ -2,7 +2,7 @@ Exe - net9.0 + net10.0 enable enable diff --git a/snippets/js/src/nip09.ts b/snippets/js/src/nip09.ts new file mode 100644 index 0000000..1b15716 --- /dev/null +++ b/snippets/js/src/nip09.ts @@ -0,0 +1,24 @@ +import {EventBuilder, EventDeletionRequest, EventId, Keys, loadWasmSync} from "@rust-nostr/nostr-sdk"; + +function run() { + // Load WASM + loadWasmSync(); + + let keys = Keys.parse("3501454135014541350145413501453fefb02227e449e57cf4d3a3ce05378683"); + + // ANCHOR: build + // Construct the request + let id = EventId.parse("7469af3be8c8e06e1b50ef1caceba30392ddc0b6614507398b7d7daa4c218e96"); + let request = new EventDeletionRequest(); + request.ids = [id]; + request.reason = "these posts were published by accident"; + + + // Build the event + let event = EventBuilder.delete(request).signWithKeys(keys); + // ANCHOR_END: build + + console.log("Event deletion request: " + event.asPrettyJson()) +} + +run(); diff --git a/snippets/kotlin/src/main/kotlin/nip09.kt b/snippets/kotlin/src/main/kotlin/nip09.kt new file mode 100644 index 0000000..fc91de2 --- /dev/null +++ b/snippets/kotlin/src/main/kotlin/nip09.kt @@ -0,0 +1,24 @@ +package rust.nostr.snippets + +// ANCHOR: full +import rust.nostr.sdk.* + +fun nip09() { + val keys: Keys = Keys.parse("3501454135014541350145413501453fefb02227e449e57cf4d3a3ce05378683") + + // ANCHOR: build + // Construct the request + val id = EventId.parse("7469af3be8c8e06e1b50ef1caceba30392ddc0b6614507398b7d7daa4c218e96") + val request = EventDeletionRequest(ids = listOf(id), coordinates = listOf(), reason = "these posts were published by accident") + + // Build the event + val event: Event = EventBuilder.delete(request).signWithKeys(keys) + // ANCHOR_END: build + + println("Event deletion request: ${event.asPrettyJson()}") +} + +fun main() { + nip09() +} +// ANCHOR_END: full diff --git a/snippets/python/src/nip09.py b/snippets/python/src/nip09.py new file mode 100644 index 0000000..cae0b30 --- /dev/null +++ b/snippets/python/src/nip09.py @@ -0,0 +1,21 @@ +# ANCHOR: full +from nostr_sdk import Keys, EventDeletionRequest, EventId, Event, EventBuilder + + +def nip09(): + keys: Keys = Keys.parse("3501454135014541350145413501453fefb02227e449e57cf4d3a3ce05378683") + + # ANCHOR: build + # Construct the request + id = EventId.parse("7469af3be8c8e06e1b50ef1caceba30392ddc0b6614507398b7d7daa4c218e96") + request = EventDeletionRequest(ids=[id], coordinates=[], reason="these posts were published by accident") + + # Build the event + event: Event = EventBuilder.delete(request).sign_with_keys(keys) + # ANCHOR_END: build + + print(f"Event deletion request: {event.as_pretty_json()}") + +if __name__ == '__main__': + nip09() +# ANCHOR_END: full \ No newline at end of file diff --git a/snippets/rust/src/main.rs b/snippets/rust/src/main.rs index 7d1d4a6..80d8e42 100644 --- a/snippets/rust/src/main.rs +++ b/snippets/rust/src/main.rs @@ -13,6 +13,7 @@ mod nip47; mod nip49; mod nip59; mod req; +mod nip09; #[tokio::main] async fn main() {} diff --git a/snippets/rust/src/nip09.rs b/snippets/rust/src/nip09.rs new file mode 100644 index 0000000..55975f8 --- /dev/null +++ b/snippets/rust/src/nip09.rs @@ -0,0 +1,23 @@ +// ANCHOR: full +use nostr_sdk::prelude::*; + +pub fn nip09() -> Result<()> { + let keys: Keys = Keys::parse("3501454135014541350145413501453fefb02227e449e57cf4d3a3ce05378683")?; + + // ANCHOR: build + // Construct the request + let request = EventDeletionRequest::new() + .id(EventId::from_hex("7469af3be8c8e06e1b50ef1caceba30392ddc0b6614507398b7d7daa4c218e96")?) + // optionally add coordinates for replaceable events + // .coordinate(...) + .reason("these posts were published by accident"); + + // Build the event + let event: Event = EventBuilder::delete(request).sign_with_keys(&keys)?; + // ANCHOR_END: build + + println!("Event deletion request: {}", event.as_pretty_json()); + + Ok(()) +} +// ANCHOR_END: full diff --git a/snippets/swift/Sources/Nip09.swift b/snippets/swift/Sources/Nip09.swift new file mode 100644 index 0000000..b192d49 --- /dev/null +++ b/snippets/swift/Sources/Nip09.swift @@ -0,0 +1,20 @@ +// ANCHOR: full +import Foundation +import NostrSDK + +func nip09() throws { + let keys = try Keys.parse(secretKey: "3501454135014541350145413501453fefb02227e449e57cf4d3a3ce05378683") + + // ANCHOR: build + // Construct the request + let id = try EventId.parse(id: "7469af3be8c8e06e1b50ef1caceba30392ddc0b6614507398b7d7daa4c218e96") + let request = EventDeletionRequest(ids: [id], coordinates: [], reason: "these posts were published by accident") + + // Build the event + let builder = EventBuilder.delete(request: request) + let event = try builder.signWithKeys(keys: keys) + // ANCHOR_END: build + + print("Event deletion request: \(try event.asPrettyJson())"); +} +// ANCHOR_END: full diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 03d789b..2213295 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -37,6 +37,7 @@ - [NIP-05: Mapping nostr keys to DNS-based internet identifiers](sdk/nips/05.md) - [NIP-06: Key derivation from seed phrase](sdk/nips/06.md) - [NIP-07: Browser Extension Signer](sdk/nips/07.md) + - [NIP-09: Event Deletion Request](sdk/nips/09.md) - [NIP-17: Private Direct Messages](sdk/nips/17.md) - [NIP-19: bech32-encoded entities](sdk/nips/19.md) - [NIP-21: nostr URI scheme](sdk/nips/21.md) diff --git a/src/sdk/nips/09.md b/src/sdk/nips/09.md new file mode 100644 index 0000000..7bd7575 --- /dev/null +++ b/src/sdk/nips/09.md @@ -0,0 +1,88 @@ +# NIP-09: Event Deletion Request + +[NIP-09] defines a best-effort mechanism for retracting events you previously published. +It does **not** guarantee erasure—relays and downstream clients are allowed to cache or ignore deletion requests—so treat it as a courtesy protocol for well-behaved peers. + +## When to send a deletion event + +1. You control the secret key that signed the original event. +2. You know the event IDs (or coordinates for replaceable/parameterized events) you want to retract. +3. You accept that the payload remains public even after the deletion request propagates. + +## Building the request + + + +
Rust
+
+ +```rust,ignore +{{#include ../../../snippets/rust/src/nip09.rs:build}} +``` + +
+ +
Python
+
+ +```python,ignore +{{#include ../../../snippets/python/src/nip09.py:build}} +``` + +
+ +
JavaScript
+
+ +```typescript,ignore +{{#include ../../../snippets/js/src/nip09.ts:build}} +``` + +
+ +
Kotlin
+
+ +```kotlin,ignore +{{#include ../../../snippets/kotlin/src/main/kotlin/nip09.kt:build}} +``` + +
+ +
Swift
+
+ +```swift,ignore +{{#include ../../../snippets/swift/Sources/Nip09.swift:build}} +``` + +
+ +
C#
+
+ +```cs,ignore +{{#include ../../../snippets/csharp/Nip09.cs:build}} +``` + +
+ +
Flutter
+
+ +TODO + +
+
+ +Then, the deletion event can be sent to every relay that received the original event. + +## Caveats + +- Relays are free to ignore deletion events entirely or only apply them to new subscribers. +- Archive relays and scrapers can continue serving the old content indefinitely. +- Deleting a parameterized replaceable event without its coordinate will have no effect. + +In short: use [NIP-09] as part of your UX, but do not treat it as a hard delete. + +[NIP-09]: https://github.com/nostr-protocol/nips/blob/master/09.md