Add NIP-34 git stuff implementation with event creation helpers for repository announcements, state, patches, PRs, issues, status, grasp lists, and nostr:// URL parsing

This commit is contained in:
Laan Tungir
2026-08-02 15:43:58 -04:00
parent 4a53455190
commit f35b966fe9
7 changed files with 1517 additions and 5 deletions
+1
View File
@@ -51,6 +51,7 @@ else()
nostr_core/nip017.c
nostr_core/nip019.c
nostr_core/nip021.c
nostr_core/nip034.c
nostr_core/nip042.c
nostr_core/nip044.c
nostr_core/nip046.c
+2 -2
View File
@@ -43,7 +43,7 @@ A C library for NOSTR protocol implementation. Work in progress.
- [ ] [NIP-31](nips/31.md) - Dealing with Unknown Events
- [ ] [NIP-32](nips/32.md) - Labeling
- [ ] [NIP-33](nips/33.md) - Parameterized Replaceable Events
- [ ] [NIP-34](nips/34.md) - `git` stuff
- [x] [NIP-34](nips/34.md) - `git` stuff
- [ ] [NIP-35](nips/35.md) - Torrents
- [ ] [NIP-36](nips/36.md) - Sensitive Content / Content Warning
- [ ] [NIP-37](nips/37.md) - Draft Events
@@ -96,7 +96,7 @@ A C library for NOSTR protocol implementation. Work in progress.
**Legend:** ✅ Fully Implemented | ⚠️ Partial Implementation | ❌ Not Implemented | Not Applicable
**Implementation Summary:** 13 of 96+ NIPs fully implemented (13.5%)
**Implementation Summary:** 14 of 96+ NIPs fully implemented (14.6%)
## 📦 Quick Start
+1 -1
View File
@@ -1 +1 @@
0.6.11
0.6.12
+625
View File
@@ -0,0 +1,625 @@
/*
* NIP-34: `git` stuff Implementation
* https://github.com/nostr-protocol/nips/blob/master/34.md
*/
#define _GNU_SOURCE
#include "nip034.h"
#include "nip001.h"
#include "utils.h"
#include "nostr_common.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
// Forward declarations for crypto functions
int nostr_ec_public_key_from_private_key(const unsigned char* private_key, unsigned char* public_key);
/**
* Helper: add a string array tag to a cJSON tags array.
* Creates ["<name>", "<value>"] and appends it.
*/
static cJSON* add_simple_tag(cJSON* tags, const char* name, const char* value) {
if (!tags || !name || !value) return NULL;
cJSON* tag = cJSON_CreateArray();
if (!tag) return NULL;
cJSON_AddItemToArray(tag, cJSON_CreateString(name));
cJSON_AddItemToArray(tag, cJSON_CreateString(value));
cJSON_AddItemToArray(tags, tag);
return tag;
}
/**
* Helper: add a multi-value tag (same name, multiple values).
* Creates ["<name>", "<value>"] for each value and appends it.
*/
static int add_multi_tag(cJSON* tags, const char* name, const char** values, int count) {
if (!tags || !name || !values || count <= 0) return -1;
for (int i = 0; i < count; i++) {
if (values[i]) {
cJSON* tag = cJSON_CreateArray();
if (!tag) return -1;
cJSON_AddItemToArray(tag, cJSON_CreateString(name));
cJSON_AddItemToArray(tag, cJSON_CreateString(values[i]));
cJSON_AddItemToArray(tags, tag);
}
}
return 0;
}
/**
* Helper: add a three-value tag.
* Creates ["<name>", "<value1>", "<value2>"] and appends it.
*/
static cJSON* add_tag_with_extra(cJSON* tags, const char* name, const char* value1, const char* value2) {
if (!tags || !name || !value1) return NULL;
cJSON* tag = cJSON_CreateArray();
if (!tag) return NULL;
cJSON_AddItemToArray(tag, cJSON_CreateString(name));
cJSON_AddItemToArray(tag, cJSON_CreateString(value1));
if (value2) {
cJSON_AddItemToArray(tag, cJSON_CreateString(value2));
}
cJSON_AddItemToArray(tags, tag);
return tag;
}
/**
* NIP-34: Create a repository announcement event (kind 30617)
*/
cJSON* nostr_nip34_create_repo_announcement(
nostr_signer_t* signer,
const char* repo_id,
const char* name,
const char* description,
const char* web_url,
const char** clone_urls,
int clone_count,
const char** relay_urls,
int relay_count,
const char* euc_commit,
const char** maintainers,
int maintainer_count
) {
if (!signer || !repo_id) {
return NULL;
}
cJSON* tags = cJSON_CreateArray();
if (!tags) return NULL;
// d tag (required)
add_simple_tag(tags, "d", repo_id);
// name tag (optional)
if (name) {
add_simple_tag(tags, "name", name);
}
// description tag (optional)
if (description) {
add_simple_tag(tags, "description", description);
}
// web tag (optional, can be multiple)
if (web_url) {
add_simple_tag(tags, "web", web_url);
}
// clone tags (optional, can be multiple)
if (clone_urls && clone_count > 0) {
add_multi_tag(tags, "clone", clone_urls, clone_count);
}
// relays tags (optional, can be multiple)
if (relay_urls && relay_count > 0) {
add_multi_tag(tags, "relays", relay_urls, relay_count);
}
// r tag with euc marker (optional)
if (euc_commit) {
cJSON* r_tag = cJSON_CreateArray();
if (r_tag) {
cJSON_AddItemToArray(r_tag, cJSON_CreateString("r"));
cJSON_AddItemToArray(r_tag, cJSON_CreateString(euc_commit));
cJSON_AddItemToArray(r_tag, cJSON_CreateString("euc"));
cJSON_AddItemToArray(tags, r_tag);
}
}
// maintainers tags (optional, can be multiple)
if (maintainers && maintainer_count > 0) {
add_multi_tag(tags, "maintainers", maintainers, maintainer_count);
}
cJSON* event = nostr_create_and_sign_event_with_signer(
NOSTR_NIP34_KIND_REPO_ANNOUNCEMENT, "", tags, signer, 0
);
cJSON_Delete(tags);
return event;
}
/**
* NIP-34: Create a repository state announcement event (kind 30618)
*/
cJSON* nostr_nip34_create_repo_state(
nostr_signer_t* signer,
const char* repo_id,
const char** ref_names,
const char** ref_values,
int ref_count
) {
if (!signer || !repo_id) {
return NULL;
}
cJSON* tags = cJSON_CreateArray();
if (!tags) return NULL;
// d tag (required)
add_simple_tag(tags, "d", repo_id);
// refs tags (optional, can be multiple)
if (ref_names && ref_values && ref_count > 0) {
for (int i = 0; i < ref_count; i++) {
if (ref_names[i] && ref_values[i]) {
add_simple_tag(tags, ref_names[i], ref_values[i]);
}
}
}
cJSON* event = nostr_create_and_sign_event_with_signer(
NOSTR_NIP34_KIND_REPO_STATE, "", tags, signer, 0
);
cJSON_Delete(tags);
return event;
}
/**
* NIP-34: Create a patch event (kind 1617)
*/
cJSON* nostr_nip34_create_patch(
nostr_signer_t* signer,
const char* repo_address,
const char* euc_commit,
const char* patch_content,
const char* commit_id,
const char* parent_commit_id,
const char* commit_pgp_sig,
const char* committer_name,
const char* committer_email,
const char* committer_timestamp,
int committer_tz_offset,
int is_root,
int is_root_revision
) {
if (!signer || !repo_address || !patch_content) {
return NULL;
}
cJSON* tags = cJSON_CreateArray();
if (!tags) return NULL;
// a tag pointing to the repository announcement
add_simple_tag(tags, "a", repo_address);
// r tag with euc (optional)
if (euc_commit) {
add_tag_with_extra(tags, "r", euc_commit, NULL);
}
// t tags for root/root-revision markers
if (is_root) {
add_simple_tag(tags, "t", "root");
}
if (is_root_revision) {
add_simple_tag(tags, "t", "root-revision");
}
// commit tag (optional, for stable commit IDs)
if (commit_id) {
add_simple_tag(tags, "commit", commit_id);
}
// parent-commit tag (optional)
if (parent_commit_id) {
add_simple_tag(tags, "parent-commit", parent_commit_id);
}
// commit-pgp-sig tag (optional)
if (commit_pgp_sig) {
add_simple_tag(tags, "commit-pgp-sig", commit_pgp_sig);
}
// committer tag (optional)
if (committer_name && committer_email && committer_timestamp) {
char tz_str[16];
snprintf(tz_str, sizeof(tz_str), "%d", committer_tz_offset);
cJSON* committer_tag = cJSON_CreateArray();
if (committer_tag) {
cJSON_AddItemToArray(committer_tag, cJSON_CreateString("committer"));
cJSON_AddItemToArray(committer_tag, cJSON_CreateString(committer_name));
cJSON_AddItemToArray(committer_tag, cJSON_CreateString(committer_email));
cJSON_AddItemToArray(committer_tag, cJSON_CreateString(committer_timestamp));
cJSON_AddItemToArray(committer_tag, cJSON_CreateString(tz_str));
cJSON_AddItemToArray(tags, committer_tag);
}
}
cJSON* event = nostr_create_and_sign_event_with_signer(
NOSTR_NIP34_KIND_PATCH, patch_content, tags, signer, 0
);
cJSON_Delete(tags);
return event;
}
/**
* NIP-34: Create a pull request event (kind 1618)
*/
cJSON* nostr_nip34_create_pull_request(
nostr_signer_t* signer,
const char* repo_address,
const char* euc_commit,
const char* subject,
const char* content,
const char* tip_commit,
const char** clone_urls,
int clone_count,
const char* branch_name,
const char* root_patch_event_id,
const char* merge_base
) {
if (!signer || !repo_address) {
return NULL;
}
const char* body = content ? content : "";
cJSON* tags = cJSON_CreateArray();
if (!tags) return NULL;
// a tag pointing to the repository announcement
add_simple_tag(tags, "a", repo_address);
// r tag with euc (optional)
if (euc_commit) {
add_tag_with_extra(tags, "r", euc_commit, NULL);
}
// subject tag (optional)
if (subject) {
add_simple_tag(tags, "subject", subject);
}
// c tag for tip commit (optional)
if (tip_commit) {
add_simple_tag(tags, "c", tip_commit);
}
// clone tags (optional, can be multiple)
if (clone_urls && clone_count > 0) {
add_multi_tag(tags, "clone", clone_urls, clone_count);
}
// branch-name tag (optional)
if (branch_name) {
add_simple_tag(tags, "branch-name", branch_name);
}
// e tag for revision of existing patch (optional)
if (root_patch_event_id) {
add_simple_tag(tags, "e", root_patch_event_id);
}
// merge-base tag (optional)
if (merge_base) {
add_simple_tag(tags, "merge-base", merge_base);
}
cJSON* event = nostr_create_and_sign_event_with_signer(
NOSTR_NIP34_KIND_PULL_REQUEST, body, tags, signer, 0
);
cJSON_Delete(tags);
return event;
}
/**
* NIP-34: Create a pull request update event (kind 1619)
*/
cJSON* nostr_nip34_create_pr_update(
nostr_signer_t* signer,
const char* repo_address,
const char* euc_commit,
const char* pr_event_id,
const char* pr_author,
const char* tip_commit,
const char** clone_urls,
int clone_count,
const char* merge_base
) {
if (!signer || !repo_address || !pr_event_id || !pr_author) {
return NULL;
}
cJSON* tags = cJSON_CreateArray();
if (!tags) return NULL;
// a tag pointing to the repository announcement
add_simple_tag(tags, "a", repo_address);
// r tag with euc (optional)
if (euc_commit) {
add_tag_with_extra(tags, "r", euc_commit, NULL);
}
// NIP-22 tags for the parent PR
add_simple_tag(tags, "E", pr_event_id);
add_simple_tag(tags, "P", pr_author);
// c tag for updated tip commit (optional)
if (tip_commit) {
add_simple_tag(tags, "c", tip_commit);
}
// clone tags (optional, can be multiple)
if (clone_urls && clone_count > 0) {
add_multi_tag(tags, "clone", clone_urls, clone_count);
}
// merge-base tag (optional)
if (merge_base) {
add_simple_tag(tags, "merge-base", merge_base);
}
cJSON* event = nostr_create_and_sign_event_with_signer(
NOSTR_NIP34_KIND_PR_UPDATE, "", tags, signer, 0
);
cJSON_Delete(tags);
return event;
}
/**
* NIP-34: Create an issue event (kind 1621)
*/
cJSON* nostr_nip34_create_issue(
nostr_signer_t* signer,
const char* repo_address,
const char* subject,
const char* content,
const char** labels,
int label_count
) {
if (!signer || !repo_address) {
return NULL;
}
const char* body = content ? content : "";
cJSON* tags = cJSON_CreateArray();
if (!tags) return NULL;
// a tag pointing to the repository announcement
add_simple_tag(tags, "a", repo_address);
// subject tag (optional)
if (subject) {
add_simple_tag(tags, "subject", subject);
}
// t tags for labels (optional, can be multiple)
if (labels && label_count > 0) {
add_multi_tag(tags, "t", labels, label_count);
}
cJSON* event = nostr_create_and_sign_event_with_signer(
NOSTR_NIP34_KIND_ISSUE, body, tags, signer, 0
);
cJSON_Delete(tags);
return event;
}
/**
* NIP-34: Create a status event (kind 1630-1633)
*/
cJSON* nostr_nip34_create_status(
nostr_signer_t* signer,
int kind,
const char* root_id,
const char* repo_address,
const char* euc_commit,
const char* content
) {
if (!signer || !root_id) {
return NULL;
}
// Validate kind is a valid status kind
if (kind != NOSTR_NIP34_KIND_STATUS_OPEN &&
kind != NOSTR_NIP34_KIND_STATUS_MERGED &&
kind != NOSTR_NIP34_KIND_STATUS_CLOSED &&
kind != NOSTR_NIP34_KIND_STATUS_DRAFT) {
return NULL;
}
const char* body = content ? content : "";
cJSON* tags = cJSON_CreateArray();
if (!tags) return NULL;
// e tag pointing to the root issue, PR, or patch
add_tag_with_extra(tags, "e", root_id, "root");
// a tag for the repository (optional)
if (repo_address) {
add_simple_tag(tags, "a", repo_address);
}
// r tag with euc (optional)
if (euc_commit) {
add_tag_with_extra(tags, "r", euc_commit, NULL);
}
cJSON* event = nostr_create_and_sign_event_with_signer(
kind, body, tags, signer, 0
);
cJSON_Delete(tags);
return event;
}
/**
* NIP-34: Create a user grasp list event (kind 10317)
*/
cJSON* nostr_nip34_create_grasp_list(
nostr_signer_t* signer,
const char** grasp_urls,
int grasp_count
) {
if (!signer) {
return NULL;
}
cJSON* tags = cJSON_CreateArray();
if (!tags) return NULL;
// g tags for grasp server URLs (optional, can be multiple)
if (grasp_urls && grasp_count > 0) {
add_multi_tag(tags, "g", grasp_urls, grasp_count);
}
cJSON* event = nostr_create_and_sign_event_with_signer(
NOSTR_NIP34_KIND_GRASP_LIST, "", tags, signer, 0
);
cJSON_Delete(tags);
return event;
}
/**
* Percent-decode a string in-place.
* Returns the number of characters after decoding.
*/
static int percent_decode_inplace(char* s) {
if (!s) return 0;
char* src = s;
char* dst = s;
while (*src) {
if (*src == '%' && *(src+1) && *(src+2)) {
char hex[3] = { src[1], src[2], '\0' };
*dst++ = (char)strtol(hex, NULL, 16);
src += 3;
} else {
*dst++ = *src++;
}
}
*dst = '\0';
return (int)(dst - s);
}
/**
* NIP-34: Parse a nostr:// git URL into its components
*
* Supports formats:
* nostr://<naddr>
* nostr://<npub|nip05>/<identifier>
* nostr://<npub|nip05>/<relay-hint>/<identifier>
*
* Uses manual character scanning (no strtok) to avoid reentrancy issues.
*/
int nostr_nip34_parse_nostr_url(
const char* url,
char* npub_out,
char* identifier_out,
char* relay_hint_out
) {
if (!url || !npub_out || !identifier_out) {
return -1;
}
// Check for nostr:// prefix
const char* prefix = "nostr://";
size_t prefix_len = strlen(prefix);
if (strncmp(url, prefix, prefix_len) != 0) {
return -1;
}
const char* p = url + prefix_len;
if (*p == '\0') {
return -1;
}
// Make a mutable copy of the path
char* path_copy = strdup(p);
if (!path_copy) {
return -1;
}
// Find the three possible segments separated by '/'
// Segment 0: npub or nip05 address (up to first '/')
// Segment 1: relay hint (between first and second '/')
// Segment 2: identifier (after second '/')
char* segments[3] = {NULL, NULL, NULL};
int seg_count = 0;
segments[0] = path_copy;
seg_count = 1;
for (char* q = path_copy; *q; q++) {
if (*q == '/') {
*q = '\0'; // null-terminate current segment
if (seg_count < 3) {
segments[seg_count] = q + 1;
seg_count++;
}
}
}
// Percent-decode each segment in-place
for (int i = 0; i < seg_count; i++) {
if (segments[i]) {
percent_decode_inplace(segments[i]);
}
}
// Copy segments to output buffers
if (seg_count >= 1 && segments[0]) {
strncpy(npub_out, segments[0], 255);
npub_out[255] = '\0';
} else {
npub_out[0] = '\0';
}
if (seg_count >= 2 && segments[1]) {
strncpy(identifier_out, segments[1], 255);
identifier_out[255] = '\0';
} else {
identifier_out[0] = '\0';
}
if (relay_hint_out) {
if (seg_count >= 3 && segments[2]) {
// With 3 segments, segment[1] is the relay hint, segment[2] is the identifier
strncpy(relay_hint_out, segments[1], 511);
relay_hint_out[511] = '\0';
// Move identifier from segment[2] to identifier_out
strncpy(identifier_out, segments[2], 255);
identifier_out[255] = '\0';
} else {
relay_hint_out[0] = '\0';
}
} else {
// No relay_hint_out provided, but we still need to handle 3-segment case
if (seg_count >= 3 && segments[2]) {
strncpy(identifier_out, segments[2], 255);
identifier_out[255] = '\0';
}
}
free(path_copy);
return 0;
}
+268
View File
@@ -0,0 +1,268 @@
/*
* NIP-34: `git` stuff
* https://github.com/nostr-protocol/nips/blob/master/34.md
*
* Defines event kinds and helpers for code collaboration using git on Nostr.
*/
#ifndef NOSTR_NIP034_H
#define NOSTR_NIP034_H
#include <stddef.h>
#include "nostr_signer.h"
#include "../cjson/cJSON.h"
#ifdef __cplusplus
extern "C" {
#endif
// NIP-34 event kinds
#define NOSTR_NIP34_KIND_REPO_ANNOUNCEMENT 30617
#define NOSTR_NIP34_KIND_REPO_STATE 30618
#define NOSTR_NIP34_KIND_PATCH 1617
#define NOSTR_NIP34_KIND_PULL_REQUEST 1618
#define NOSTR_NIP34_KIND_PR_UPDATE 1619
#define NOSTR_NIP34_KIND_ISSUE 1621
#define NOSTR_NIP34_KIND_STATUS_OPEN 1630
#define NOSTR_NIP34_KIND_STATUS_MERGED 1631
#define NOSTR_NIP34_KIND_STATUS_CLOSED 1632
#define NOSTR_NIP34_KIND_STATUS_DRAFT 1633
#define NOSTR_NIP34_KIND_GRASP_LIST 10317
/**
* NIP-34: Create a repository announcement event (kind 30617)
*
* Announces a git repository's existence on Nostr.
*
* @param signer Signer to use for event creation
* @param repo_id Repository identifier (d tag, usually kebab-case short name)
* @param name Human-readable project name (can be NULL)
* @param description Brief human-readable project description (can be NULL)
* @param web_url URL for browsing the repository (can be NULL)
* @param clone_urls Array of git clone URLs (can be NULL)
* @param clone_count Number of clone URLs
* @param relay_urls Array of relay URLs for patches and issues (can be NULL)
* @param relay_count Number of relay URLs
* @param euc_commit Earliest unique commit ID for fork grouping (can be NULL)
* @param maintainers Array of maintainer pubkeys (can be NULL)
* @param maintainer_count Number of maintainers
* @return Signed cJSON event, or NULL on error
*/
cJSON* nostr_nip34_create_repo_announcement(
nostr_signer_t* signer,
const char* repo_id,
const char* name,
const char* description,
const char* web_url,
const char** clone_urls,
int clone_count,
const char** relay_urls,
int relay_count,
const char* euc_commit,
const char** maintainers,
int maintainer_count
);
/**
* NIP-34: Create a repository state announcement event (kind 30618)
*
* Announces the current state of branches and tags in a repository.
*
* @param signer Signer to use for event creation
* @param repo_id Repository identifier (matches the d tag in the announcement)
* @param ref_names Array of reference names (e.g., "refs/heads/master")
* @param ref_values Array of reference values (commit hashes or "ref: refs/heads/<branch>")
* @param ref_count Number of references
* @return Signed cJSON event, or NULL on error
*/
cJSON* nostr_nip34_create_repo_state(
nostr_signer_t* signer,
const char* repo_id,
const char** ref_names,
const char** ref_values,
int ref_count
);
/**
* NIP-34: Create a patch event (kind 1617)
*
* Contains the output of git format-patch for proposing changes.
*
* @param signer Signer to use for event creation
* @param repo_address Address tag value "30617:<pubkey>:<repo-id>"
* @param euc_commit Earliest unique commit ID of the repo (can be NULL)
* @param patch_content The git format-patch output
* @param commit_id Current commit ID for stable commit IDs (can be NULL)
* @param parent_commit_id Parent commit ID (can be NULL)
* @param commit_pgp_sig PGP signature for the commit (can be NULL, empty string for unsigned)
* @param committer_name Committer name (can be NULL)
* @param committer_email Committer email (can be NULL)
* @param committer_timestamp Committer timestamp (can be NULL)
* @param committer_tz_offset Committer timezone offset in minutes (0 if unused)
* @param is_root 1 if this is the root patch in a series, 0 otherwise
* @param is_root_revision 1 if this is the first patch in a revision, 0 otherwise
* @return Signed cJSON event, or NULL on error
*/
cJSON* nostr_nip34_create_patch(
nostr_signer_t* signer,
const char* repo_address,
const char* euc_commit,
const char* patch_content,
const char* commit_id,
const char* parent_commit_id,
const char* commit_pgp_sig,
const char* committer_name,
const char* committer_email,
const char* committer_timestamp,
int committer_tz_offset,
int is_root,
int is_root_revision
);
/**
* NIP-34: Create a pull request event (kind 1618)
*
* Points to proposed changes in a git repository via clone URLs.
*
* @param signer Signer to use for event creation
* @param repo_address Address tag value "30617:<pubkey>:<repo-id>"
* @param euc_commit Earliest unique commit ID of the repo (can be NULL)
* @param subject PR subject/title (can be NULL)
* @param content Markdown body text
* @param tip_commit Current commit ID at the tip of the PR branch (can be NULL)
* @param clone_urls Array of git clone URLs where the PR branch can be fetched
* @param clone_count Number of clone URLs
* @param branch_name Recommended branch name (can be NULL)
* @param root_patch_event_id Event ID of the root patch this revises (can be NULL)
* @param merge_base Most recent common ancestor with the target branch (can be NULL)
* @return Signed cJSON event, or NULL on error
*/
cJSON* nostr_nip34_create_pull_request(
nostr_signer_t* signer,
const char* repo_address,
const char* euc_commit,
const char* subject,
const char* content,
const char* tip_commit,
const char** clone_urls,
int clone_count,
const char* branch_name,
const char* root_patch_event_id,
const char* merge_base
);
/**
* NIP-34: Create a pull request update event (kind 1619)
*
* Updates the tip commit of an existing pull request.
*
* @param signer Signer to use for event creation
* @param repo_address Address tag value "30617:<pubkey>:<repo-id>"
* @param euc_commit Earliest unique commit ID of the repo (can be NULL)
* @param pr_event_id The pull request event ID being updated
* @param pr_author The pubkey of the pull request author
* @param tip_commit Updated commit ID at the tip of the PR branch (can be NULL)
* @param clone_urls Array of git clone URLs where the PR branch can be fetched
* @param clone_count Number of clone URLs
* @param merge_base Most recent common ancestor with the target branch (can be NULL)
* @return Signed cJSON event, or NULL on error
*/
cJSON* nostr_nip34_create_pr_update(
nostr_signer_t* signer,
const char* repo_address,
const char* euc_commit,
const char* pr_event_id,
const char* pr_author,
const char* tip_commit,
const char** clone_urls,
int clone_count,
const char* merge_base
);
/**
* NIP-34: Create an issue event (kind 1621)
*
* Bug reports, feature requests, questions or comments related to a repository.
*
* @param signer Signer to use for event creation
* @param repo_address Address tag value "30617:<pubkey>:<repo-id>"
* @param subject Issue subject/title (can be NULL)
* @param content Markdown body text
* @param labels Array of label strings (can be NULL)
* @param label_count Number of labels
* @return Signed cJSON event, or NULL on error
*/
cJSON* nostr_nip34_create_issue(
nostr_signer_t* signer,
const char* repo_address,
const char* subject,
const char* content,
const char** labels,
int label_count
);
/**
* NIP-34: Create a status event (kind 1630-1633)
*
* Sets the status of a root patch, PR, or issue.
*
* @param signer Signer to use for event creation
* @param kind Status kind: 1630 (Open), 1631 (Applied/Merged/Resolved),
* 1632 (Closed), 1633 (Draft)
* @param root_id Event ID of the root issue, PR, or original root patch
* @param repo_address Address tag value "30617:<pubkey>:<repo-id>" (can be NULL)
* @param euc_commit Earliest unique commit ID of the repo (can be NULL)
* @param content Markdown text describing the status change (can be NULL)
* @return Signed cJSON event, or NULL on error
*/
cJSON* nostr_nip34_create_status(
nostr_signer_t* signer,
int kind,
const char* root_id,
const char* repo_address,
const char* euc_commit,
const char* content
);
/**
* NIP-34: Create a user grasp list event (kind 10317)
*
* Lists grasp servers the user wishes to use for NIP-34 activity.
*
* @param signer Signer to use for event creation
* @param grasp_urls Array of grasp service websocket URLs
* @param grasp_count Number of grasp URLs
* @return Signed cJSON event, or NULL on error
*/
cJSON* nostr_nip34_create_grasp_list(
nostr_signer_t* signer,
const char** grasp_urls,
int grasp_count
);
/**
* NIP-34: Parse a nostr:// git URL into its components
*
* Supports formats:
* nostr://<naddr>
* nostr://<npub|nip05>/<identifier>
* nostr://<npub|nip05>/<relay-hint>/<identifier>
*
* @param url The nostr:// URL to parse
* @param npub_out Buffer for npub or NIP-05 address (at least 256 bytes)
* @param identifier_out Buffer for the repository identifier (at least 256 bytes)
* @param relay_hint_out Buffer for the relay hint URL (at least 512 bytes, optional, can be NULL)
* @return 0 on success, -1 on error
*/
int nostr_nip34_parse_nostr_url(
const char* url,
char* npub_out,
char* identifier_out,
char* relay_hint_out
);
#ifdef __cplusplus
}
#endif
#endif /* NOSTR_NIP034_H */
+14 -2
View File
@@ -2,10 +2,10 @@
#define NOSTR_CORE_H
// Version information (auto-updated by increment_and_push.sh)
#define VERSION "v0.6.11"
#define VERSION "v0.6.12"
#define VERSION_MAJOR 0
#define VERSION_MINOR 6
#define VERSION_PATCH 11
#define VERSION_PATCH 12
/*
* NOSTR Core Library - Complete API Reference
@@ -71,6 +71,17 @@
* - nostr_nip17_receive_dm() -> Receive and decrypt DM
* - nostr_nip17_extract_dm_relays() -> Extract relay URLs from kind 10050
*
* NIP-34 GIT STUFF:
* - nostr_nip34_create_repo_announcement() -> Create repo announcement (kind 30617)
* - nostr_nip34_create_repo_state() -> Create repo state (kind 30618)
* - nostr_nip34_create_patch() -> Create patch (kind 1617)
* - nostr_nip34_create_pull_request() -> Create pull request (kind 1618)
* - nostr_nip34_create_pr_update() -> Create PR update (kind 1619)
* - nostr_nip34_create_issue() -> Create issue (kind 1621)
* - nostr_nip34_create_status() -> Create status (kind 1630-1633)
* - nostr_nip34_create_grasp_list() -> Create grasp list (kind 10317)
* - nostr_nip34_parse_nostr_url() -> Parse nostr:// git URL
*
* NIP-42 AUTHENTICATION:
* - nostr_nip42_create_auth_event() -> Create authentication event (kind 22242)
* - nostr_nip42_verify_auth_event() -> Verify authentication event (relay-side)
@@ -187,6 +198,7 @@ extern "C" {
#include "nip017.h" // Private Direct Messages
#include "nip019.h" // Bech32 encoding (nsec/npub)
#include "nip021.h" // nostr: URI scheme
#include "nip034.h" // git stuff
#include "nip042.h" // Authentication of clients to relays
#include "nip044.h" // Encryption (modern)
#include "nip046.h" // Remote signing
+606
View File
@@ -0,0 +1,606 @@
/*
* NIP-34 Git Stuff Test Suite
* Tests repository announcement, state, patch, PR, issue, and status event creation
* Following TESTS POLICY: Shows expected vs actual values, prints entire JSON events
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "../nostr_core/nip034.h"
#include "../nostr_core/nip001.h"
#include "../nostr_core/nostr_common.h"
#include "../nostr_core/utils.h"
#include "../cjson/cJSON.h"
// Test private key (well-known test key, DO NOT use in production)
static const unsigned char test_private_key[32] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01
};
static int test_count = 0;
static int passed_tests = 0;
void print_test_header(const char* test_name) {
test_count++;
printf("\n=== TEST %d: %s ===\n", test_count, test_name);
}
void print_test_result(int passed, const char* test_name) {
if (passed) {
passed_tests++;
printf("✅ PASS: %s\n", test_name);
} else {
printf("❌ FAIL: %s\n", test_name);
}
}
void print_json_event(cJSON* event, const char* label) {
if (!event) {
printf("%s: NULL\n", label);
return;
}
char* json = cJSON_Print(event);
printf("%s:\n%s\n", label, json ? json : "NULL");
if (json) free(json);
}
// Test 1: Create a repository announcement event
int test_repo_announcement(void) {
print_test_header("Repository Announcement (kind 30617)");
nostr_signer_t* signer = nostr_signer_local(test_private_key);
if (!signer) {
printf("❌ Failed to create signer\n");
return 0;
}
const char* clone_urls[] = {
"https://laantungir.net/grasp/npub1test/c-relay.git",
"https://gitgrasp.com/npub1test/c-relay.git",
NULL
};
const char* relay_urls[] = {
"wss://relay.laantungir.net",
"wss://relay.damus.io",
NULL
};
const char* maintainers[] = {
"aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4",
NULL
};
cJSON* event = nostr_nip34_create_repo_announcement(
signer,
"c-relay",
"c-relay",
"A blazingly fast, production-ready Nostr relay",
"https://git.laantungir.net/laantungir/c-relay",
clone_urls, 2,
relay_urls, 2,
"abc123def456abc123def456abc123def456abc123def456abc123def456abc123",
maintainers, 1
);
if (!event) {
printf("❌ Failed to create repo announcement event\n");
nostr_signer_free(signer);
return 0;
}
print_json_event(event, "Repository Announcement Event");
// Validate the event
int rc = nostr_validate_event(event);
printf("Validation result: %d (expected 0 = NOSTR_SUCCESS)\n", rc);
// Check kind
cJSON* kind = cJSON_GetObjectItem(event, "kind");
int kind_val = kind ? (int)cJSON_GetNumberValue(kind) : -1;
printf("Kind: %d (expected 30617)\n", kind_val);
// Check tags
cJSON* tags = cJSON_GetObjectItem(event, "tags");
int tag_count = tags ? cJSON_GetArraySize(tags) : 0;
printf("Tag count: %d (expected >= 6)\n", tag_count);
cJSON_Delete(event);
nostr_signer_free(signer);
int passed = (rc == NOSTR_SUCCESS && kind_val == 30617 && tag_count >= 6);
print_test_result(passed, "Repository Announcement");
return passed;
}
// Test 2: Create a repository state event
int test_repo_state(void) {
print_test_header("Repository State (kind 30618)");
nostr_signer_t* signer = nostr_signer_local(test_private_key);
if (!signer) {
printf("❌ Failed to create signer\n");
return 0;
}
const char* ref_names[] = {
"refs/heads/master",
"refs/heads/develop",
"refs/tags/v1.0.0",
"HEAD",
NULL
};
const char* ref_values[] = {
"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6",
"f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1",
"0011223344550011223344550011223344550011223344550011223344550011",
"ref: refs/heads/master",
NULL
};
cJSON* event = nostr_nip34_create_repo_state(
signer,
"c-relay",
ref_names, ref_values, 4
);
if (!event) {
printf("❌ Failed to create repo state event\n");
nostr_signer_free(signer);
return 0;
}
print_json_event(event, "Repository State Event");
int rc = nostr_validate_event(event);
printf("Validation result: %d (expected 0)\n", rc);
cJSON* kind = cJSON_GetObjectItem(event, "kind");
int kind_val = kind ? (int)cJSON_GetNumberValue(kind) : -1;
printf("Kind: %d (expected 30618)\n", kind_val);
cJSON_Delete(event);
nostr_signer_free(signer);
int passed = (rc == NOSTR_SUCCESS && kind_val == 30618);
print_test_result(passed, "Repository State");
return passed;
}
// Test 3: Create a patch event
int test_patch(void) {
print_test_header("Patch (kind 1617)");
nostr_signer_t* signer = nostr_signer_local(test_private_key);
if (!signer) {
printf("❌ Failed to create signer\n");
return 0;
}
const char* patch_content =
"From 1234567890abcdef1234567890abcdef12345678 Mon Sep 17 00:00:00 2024\n"
"From: Test User <test@example.com>\n"
"Date: Mon, 1 Jan 2024 12:00:00 +0000\n"
"Subject: [PATCH] Add new feature\n"
"\n"
"This is a test patch.\n"
"---\n"
" src/main.c | 2 ++\n"
" 1 file changed, 2 insertions(+)\n"
"\n"
"diff --git a/src/main.c b/src/main.c\n"
"index abc123..def456 100644\n"
"--- a/src/main.c\n"
"+++ b/src/main.c\n"
"@@ -1,3 +1,5 @@\n"
" #include <stdio.h>\n"
"+\n"
"+// New feature\n"
" int main(void) {\n"
" return 0;\n"
" }\n";
cJSON* event = nostr_nip34_create_patch(
signer,
"30617:aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4:c-relay",
"abc123def456abc123def456abc123def456abc123def456abc123def456abc123",
patch_content,
"1234567890abcdef1234567890abcdef12345678",
"abcdef1234567890abcdef1234567890abcdef12",
"-----BEGIN PGP SIGNATURE-----\n\niQEzBAABCAAdFiEE...\n-----END PGP SIGNATURE-----",
"Test User",
"test@example.com",
"1704110400",
0,
1, // is_root
1 // is_root_revision
);
if (!event) {
printf("❌ Failed to create patch event\n");
nostr_signer_free(signer);
return 0;
}
print_json_event(event, "Patch Event");
int rc = nostr_validate_event(event);
printf("Validation result: %d (expected 0)\n", rc);
cJSON* kind = cJSON_GetObjectItem(event, "kind");
int kind_val = kind ? (int)cJSON_GetNumberValue(kind) : -1;
printf("Kind: %d (expected 1617)\n", kind_val);
cJSON_Delete(event);
nostr_signer_free(signer);
int passed = (rc == NOSTR_SUCCESS && kind_val == 1617);
print_test_result(passed, "Patch");
return passed;
}
// Test 4: Create a pull request event
int test_pull_request(void) {
print_test_header("Pull Request (kind 1618)");
nostr_signer_t* signer = nostr_signer_local(test_private_key);
if (!signer) {
printf("❌ Failed to create signer\n");
return 0;
}
const char* clone_urls[] = {
"https://github.com/user/c-relay.git",
NULL
};
cJSON* event = nostr_nip34_create_pull_request(
signer,
"30617:aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4:c-relay",
"abc123def456abc123def456abc123def456abc123def456abc123def456abc123",
"Fix critical bug in relay connection handling",
"This PR fixes a race condition in the relay connection code.\n\nChanges:\n- Added mutex locking\n- Fixed timeout handling",
"fedcba9876543210fedcba9876543210fedcba98",
clone_urls, 1,
"fix-race-condition",
NULL, // no root patch event
"abcdef1234567890abcdef1234567890abcdef12" // merge base
);
if (!event) {
printf("❌ Failed to create pull request event\n");
nostr_signer_free(signer);
return 0;
}
print_json_event(event, "Pull Request Event");
int rc = nostr_validate_event(event);
printf("Validation result: %d (expected 0)\n", rc);
cJSON* kind = cJSON_GetObjectItem(event, "kind");
int kind_val = kind ? (int)cJSON_GetNumberValue(kind) : -1;
printf("Kind: %d (expected 1618)\n", kind_val);
cJSON_Delete(event);
nostr_signer_free(signer);
int passed = (rc == NOSTR_SUCCESS && kind_val == 1618);
print_test_result(passed, "Pull Request");
return passed;
}
// Test 5: Create an issue event
int test_issue(void) {
print_test_header("Issue (kind 1621)");
nostr_signer_t* signer = nostr_signer_local(test_private_key);
if (!signer) {
printf("❌ Failed to create signer\n");
return 0;
}
const char* labels[] = {
"bug",
"high-priority",
NULL
};
cJSON* event = nostr_nip34_create_issue(
signer,
"30617:aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4:c-relay",
"Connection timeout on startup",
"When starting the relay with a large database, the connection times out after 30 seconds.\n\nSteps to reproduce:\n1. Start relay with 1M events\n2. Try to connect\n3. Observe timeout",
labels, 2
);
if (!event) {
printf("❌ Failed to create issue event\n");
nostr_signer_free(signer);
return 0;
}
print_json_event(event, "Issue Event");
int rc = nostr_validate_event(event);
printf("Validation result: %d (expected 0)\n", rc);
cJSON* kind = cJSON_GetObjectItem(event, "kind");
int kind_val = kind ? (int)cJSON_GetNumberValue(kind) : -1;
printf("Kind: %d (expected 1621)\n", kind_val);
cJSON_Delete(event);
nostr_signer_free(signer);
int passed = (rc == NOSTR_SUCCESS && kind_val == 1621);
print_test_result(passed, "Issue");
return passed;
}
// Test 6: Create a status event
int test_status(void) {
print_test_header("Status (kind 1631 - Merged)");
nostr_signer_t* signer = nostr_signer_local(test_private_key);
if (!signer) {
printf("❌ Failed to create signer\n");
return 0;
}
cJSON* event = nostr_nip34_create_status(
signer,
NOSTR_NIP34_KIND_STATUS_MERGED,
"abc123def456abc123def456abc123def456abc123def456abc123def456abc123",
"30617:aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4:c-relay",
"abc123def456abc123def456abc123def456abc123def456abc123def456abc123",
"Patch has been merged to master"
);
if (!event) {
printf("❌ Failed to create status event\n");
nostr_signer_free(signer);
return 0;
}
print_json_event(event, "Status Event (Merged)");
int rc = nostr_validate_event(event);
printf("Validation result: %d (expected 0)\n", rc);
cJSON* kind = cJSON_GetObjectItem(event, "kind");
int kind_val = kind ? (int)cJSON_GetNumberValue(kind) : -1;
printf("Kind: %d (expected 1631)\n", kind_val);
cJSON_Delete(event);
nostr_signer_free(signer);
int passed = (rc == NOSTR_SUCCESS && kind_val == 1631);
print_test_result(passed, "Status");
return passed;
}
// Test 7: Create a grasp list event
int test_grasp_list(void) {
print_test_header("Grasp List (kind 10317)");
nostr_signer_t* signer = nostr_signer_local(test_private_key);
if (!signer) {
printf("❌ Failed to create signer\n");
return 0;
}
const char* grasp_urls[] = {
"wss://grasp.laantungir.net",
"wss://gitgrasp.com",
NULL
};
cJSON* event = nostr_nip34_create_grasp_list(
signer,
grasp_urls, 2
);
if (!event) {
printf("❌ Failed to create grasp list event\n");
nostr_signer_free(signer);
return 0;
}
print_json_event(event, "Grasp List Event");
int rc = nostr_validate_event(event);
printf("Validation result: %d (expected 0)\n", rc);
cJSON* kind = cJSON_GetObjectItem(event, "kind");
int kind_val = kind ? (int)cJSON_GetNumberValue(kind) : -1;
printf("Kind: %d (expected 10317)\n", kind_val);
cJSON_Delete(event);
nostr_signer_free(signer);
int passed = (rc == NOSTR_SUCCESS && kind_val == 10317);
print_test_result(passed, "Grasp List");
return passed;
}
// Test 8: Parse nostr:// URLs
int test_parse_nostr_url(void) {
print_test_header("Parse nostr:// URLs");
int all_passed = 1;
// Test 1: nostr://<npub>/<identifier>
{
char npub[256], ident[256], relay[512];
int rc = nostr_nip34_parse_nostr_url(
"nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/c-relay",
npub, ident, relay
);
printf("Test 1 - npub/identifier:\n");
printf(" Result: %d (expected 0)\n", rc);
printf(" npub: %s\n", npub);
printf(" ident: %s\n", ident);
printf(" relay: %s\n", relay);
if (rc != 0 || strlen(npub) == 0 || strlen(ident) == 0) {
printf(" ❌ FAIL\n");
all_passed = 0;
} else {
printf(" ✅ PASS\n");
}
}
// Test 2: nostr://<npub>/<relay>/<identifier>
{
char npub[256], ident[256], relay[512];
int rc = nostr_nip34_parse_nostr_url(
"nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/relay.ngit.dev/ngit",
npub, ident, relay
);
printf("Test 2 - npub/relay/identifier:\n");
printf(" Result: %d (expected 0)\n", rc);
printf(" npub: %s\n", npub);
printf(" relay: %s\n", relay);
printf(" ident: %s\n", ident);
if (rc != 0 || strlen(npub) == 0 || strlen(relay) == 0 || strlen(ident) == 0) {
printf(" ❌ FAIL\n");
all_passed = 0;
} else {
printf(" ✅ PASS\n");
}
}
// Test 3: nostr://<nip05>/<identifier>
{
char npub[256], ident[256], relay[512];
int rc = nostr_nip34_parse_nostr_url(
"nostr://danconwaydev.com/ws%3A%2F%2Flocalhost%3A7334/my-local-only-repo",
npub, ident, relay
);
printf("Test 3 - nip05/relay/identifier (percent-encoded):\n");
printf(" Result: %d (expected 0)\n", rc);
printf(" npub: %s\n", npub);
printf(" relay: %s\n", relay);
printf(" ident: %s\n", ident);
if (rc != 0 || strlen(npub) == 0 || strlen(relay) == 0 || strcmp(ident, "my-local-only-repo") != 0) {
printf(" ❌ FAIL\n");
all_passed = 0;
} else {
printf(" ✅ PASS\n");
}
}
// Test 4: Invalid URL (no nostr:// prefix)
{
char npub[256], ident[256], relay[512];
int rc = nostr_nip34_parse_nostr_url(
"https://example.com/repo.git",
npub, ident, relay
);
printf("Test 4 - invalid URL (https):\n");
printf(" Result: %d (expected -1)\n", rc);
if (rc != -1) {
printf(" ❌ FAIL\n");
all_passed = 0;
} else {
printf(" ✅ PASS\n");
}
}
print_test_result(all_passed, "Parse nostr:// URLs");
return all_passed;
}
// Test 9: Create a PR update event
int test_pr_update(void) {
print_test_header("PR Update (kind 1619)");
nostr_signer_t* signer = nostr_signer_local(test_private_key);
if (!signer) {
printf("❌ Failed to create signer\n");
return 0;
}
const char* clone_urls[] = {
"https://github.com/user/c-relay.git",
NULL
};
cJSON* event = nostr_nip34_create_pr_update(
signer,
"30617:aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4:c-relay",
"abc123def456abc123def456abc123def456abc123def456abc123def456abc123",
"pr_event_id_1234567890abcdef",
"aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4",
"new_tip_commit_hash_1234567890abcdef",
clone_urls, 1,
"updated_merge_base_hash_1234567890"
);
if (!event) {
printf("❌ Failed to create PR update event\n");
nostr_signer_free(signer);
return 0;
}
print_json_event(event, "PR Update Event");
int rc = nostr_validate_event(event);
printf("Validation result: %d (expected 0)\n", rc);
cJSON* kind = cJSON_GetObjectItem(event, "kind");
int kind_val = kind ? (int)cJSON_GetNumberValue(kind) : -1;
printf("Kind: %d (expected 1619)\n", kind_val);
cJSON_Delete(event);
nostr_signer_free(signer);
int passed = (rc == NOSTR_SUCCESS && kind_val == 1619);
print_test_result(passed, "PR Update");
return passed;
}
int main(void) {
printf("========================================\n");
printf(" NIP-34 Git Stuff Test Suite\n");
printf("========================================\n");
// Initialize crypto
if (nostr_crypto_init() != 0) {
printf("❌ Failed to initialize crypto\n");
return 1;
}
// Run all tests
int results[9];
results[0] = test_repo_announcement();
results[1] = test_repo_state();
results[2] = test_patch();
results[3] = test_pull_request();
results[4] = test_issue();
results[5] = test_status();
results[6] = test_grasp_list();
results[7] = test_parse_nostr_url();
results[8] = test_pr_update();
// Summary
printf("\n========================================\n");
printf(" TEST SUMMARY\n");
printf("========================================\n");
printf("Total tests: %d\n", test_count);
printf("Passed: %d\n", passed_tests);
printf("Failed: %d\n", test_count - passed_tests);
nostr_crypto_cleanup();
if (passed_tests == test_count) {
printf("🎉 ALL TESTS PASSED! NIP-34 implementation is working correctly.\n");
return 0;
} else {
printf("❌ SOME TESTS FAILED!\n");
return 1;
}
}