using System.Text; using Nostr.Client.Messages; namespace NostrRelay; /// /// Simple fixed length nostr event storage /// public static class NostrBuf { public static byte[] Encode(NostrEvent ev) { var buf = new byte[CalculateSize(ev)]; var span = buf.AsSpan(); span[0] = 0x01; // version 1 Convert.FromHexString(ev.Id!).CopyTo(span[1..33]); Convert.FromHexString(ev.Pubkey!).CopyTo(span[33..65]); Convert.FromHexString(ev.Sig!).CopyTo(span[65..129]); BitConverter.GetBytes((ulong)ToUnixTime(ev.CreatedAt!.Value)).CopyTo(span[129..137]); BitConverter.GetBytes((uint)ev.Kind).CopyTo(span[137..141]); var contentBytes = Encoding.UTF8.GetBytes(ev.Content!); BitConverter.GetBytes((uint)contentBytes.Length).CopyTo(span[141..145]); var pos = 145; contentBytes.CopyTo(span[pos..(pos += contentBytes.Length)]); BitConverter.GetBytes((ushort)ev.Tags!.Count).CopyTo(span[pos..(pos += 2)]); foreach (var tag in ev.Tags) { var tagKey = Encoding.UTF8.GetBytes(tag.TagIdentifier); span[pos++] = (byte)tagKey.Length; tagKey.CopyTo(span[pos..(pos += tagKey.Length)]); span[pos++] = (byte)tag.AdditionalData.Length; foreach (var tagAdd in tag.AdditionalData) { var tagAddBytes = Encoding.UTF8.GetBytes(tagAdd); BitConverter.GetBytes((ushort)tagAddBytes.Length).CopyTo(span[pos..(pos += 2)]); tagAddBytes.CopyTo(span[pos..(pos += tagAddBytes.Length)]); } } return buf; } public static NostrEvent? Decode(Span data) { var version = data[0]; if (version != 0x01) throw new Exception("Version not supported"); var id = Convert.ToHexString(data[1..33]).ToLower(); var pubkey = Convert.ToHexString(data[33..65]).ToLower(); var sig = Convert.ToHexString(data[65..129]).ToLower(); var createdAt = BitConverter.ToUInt64(data[129..137]); var kind = BitConverter.ToUInt32(data[137..141]); var contentLen = BitConverter.ToUInt32(data[141..145]); var pos = 145; var content = Encoding.UTF8.GetString(data[pos..(pos += (int)contentLen)]); var nTags = BitConverter.ToUInt16(data[pos..(pos += 2)]); var tags = new List(); for (var x = 0; x < nTags; x++) { var keyLen = data[pos++]; var keyString = Encoding.UTF8.GetString(data[pos..(pos += keyLen)]); var nElements = data[pos++]; var elms = new string[nElements]; for (var y = 0; y < nElements; y++) { var elmLen = BitConverter.ToUInt16(data[pos..(pos += 2)]); var elmString = Encoding.UTF8.GetString(data[pos..(pos += elmLen)]); elms[y] = elmString; } tags.Add(new(keyString, elms)); } return new NostrEvent() { Id = id, Pubkey = pubkey, Sig = sig, CreatedAt = DateTimeOffset.FromUnixTimeSeconds((long)createdAt).UtcDateTime, Kind = (NostrKind)kind, Content = content, Tags = new NostrEventTags(tags) }; } public static long CalculateSize(NostrEvent ev) { const long fixedPart = 1 // version byte + 32 // id + 32 // pubkey + 64 // sig + 8 // created_at (uint64) + 4 // kind (uint32) + 4 // len(content) (uint32) + 2; // len(tags) (uint16) var variableLength = Encoding.UTF8.GetByteCount(ev.Content!) + ev.Tags!.Sum(a => 1 + // 1 byte for length of first tag element (key) Encoding.UTF8.GetByteCount(a.TagIdentifier) + // length of the tag key a.AdditionalData.Length + // 1 byte - number of elements a.AdditionalData.Sum(b => Encoding.UTF8.GetByteCount(b)) + // length of the content of the items in the tag (a.AdditionalData.Length * 2)); // 2 bytes per tag element for length prefix return fixedPart + variableLength; } private static long ToUnixTime(DateTime dt) { return dt.ToUniversalTime().Ticks / 10000000L - 62135596800L; } }