From ed0f315b1a7f8d759d855cc775df3252dafee71e Mon Sep 17 00:00:00 2001
From: Sandwich <299465+dskvr@users.noreply.github.com>
Date: Wed, 8 Jan 2025 12:54:05 +0700
Subject: [PATCH] validate NIP-11 against schema

---
 .../partials/relay-single/RelayNip11.svelte   | 38 ++++++++++
 .../src/lib/services/Nip05Service/index.ts    |  7 +-
 .../services/SchemaValidationService/index.ts | 18 ++---
 .../schemavalidation.worker.ts                | 73 +++++++++----------
 .../relays/[protocol]/[...relay]/+page.svelte | 14 +++-
 libraries/schemata-js-ajv/src/index.ts        |  6 +-
 libraries/schemata/nips/nip-11/schema.yaml    | 33 +--------
 7 files changed, 100 insertions(+), 89 deletions(-)
 create mode 100644 apps/gui/src/lib/components/partials/relay-single/RelayNip11.svelte

diff --git a/apps/gui/src/lib/components/partials/relay-single/RelayNip11.svelte b/apps/gui/src/lib/components/partials/relay-single/RelayNip11.svelte
new file mode 100644
index 00000000..814b118a
--- /dev/null
+++ b/apps/gui/src/lib/components/partials/relay-single/RelayNip11.svelte
@@ -0,0 +1,38 @@
+<script lang="ts">
+    import { Nip11 } from '@nostrwatch/nip66/models';
+	import { onMount } from 'svelte';
+	import { derived, writable, type Readable, type Writable } from 'svelte/store';
+    import { SchemaValidationService, type SchemaValidationServiceResponse } from '$lib/services/SchemaValidationService';
+
+    export let nip11: Writable<Nip11>
+
+    const schemaValidationService = new SchemaValidationService();
+
+    const validationResult: Writable<SchemaValidationServiceResponse | null> = writable(null);
+
+    const nip11Valid: Readable<boolean> = derived(validationResult, $validationResult => {
+        return $validationResult?.status === 'success' && $validationResult?.result?.valid === true;
+    });
+
+    onMount(() => {
+        schemaValidationService.validateNip11($nip11?.json, $nip11?.hash).then( (result: SchemaValidationServiceResponse) => {
+            validationResult.set(result);
+        })
+    });
+</script>
+
+{#if $nip11Valid}
+    <div class="bg-green-500 text-white p-4 rounded-lg">
+        <p class="text-lg font-bold">NIP-11 has no issues</p>
+    </div>
+{:else}
+    <div class="bg-red-500 text-white p-4 rounded-lg">
+        <p class="text-lg font-bold">NIP-11 requires attentions</p>
+    </div>
+{/if}
+
+{#if $validationResult} 
+    <pre class="py-6 px-8 bg-white/5 rounded-lg">{JSON.stringify($validationResult, null, 4)}</pre>
+{/if}
+
+<pre class="py-6 px-8 bg-white/5 rounded-lg">{JSON.stringify($nip11?.json, null, 4)}</pre>
\ No newline at end of file
diff --git a/apps/gui/src/lib/services/Nip05Service/index.ts b/apps/gui/src/lib/services/Nip05Service/index.ts
index d0697972..71a05c42 100644
--- a/apps/gui/src/lib/services/Nip05Service/index.ts
+++ b/apps/gui/src/lib/services/Nip05Service/index.ts
@@ -16,10 +16,9 @@ export class Nip05Service {
     }
 
     find(pubkey: string, nip05: Nip05): INip05Result | undefined {
+        const $nip05s: INip05Map = get(nip05s)
         const key = generateNip05MapKey(pubkey, nip05)
-        let n05s = get(nip05s)
-        let result = n05s.get(key)
-        return result
+        return $nip05s.get(key)
     }
     
     async check(pubkey: string, nip05: Nip05): Promise<INip05Result> {
@@ -27,7 +26,7 @@ export class Nip05Service {
         this.worker.postMessage({ pubkey, nip05 })
         let result: INip05Result | undefined;
         while(!result){
-            result = get(nip05s).get(key)
+            result = (get(nip05s) as INip05Map).get(key)
             await new Promise( resolve => setTimeout( resolve, 200 ))
         }
         return result;
diff --git a/apps/gui/src/lib/services/SchemaValidationService/index.ts b/apps/gui/src/lib/services/SchemaValidationService/index.ts
index 707f1189..1f573548 100644
--- a/apps/gui/src/lib/services/SchemaValidationService/index.ts
+++ b/apps/gui/src/lib/services/SchemaValidationService/index.ts
@@ -1,4 +1,4 @@
-import { deterministicHash } from '@nostrwatch/nip66/utils/hash';
+import { deterministicHash } from '@nostrwatch/nip66/utils';
 import type { NostrEvent } from 'nostr-tools';
 import { get } from 'svelte/store';
 import { EventEmitter } from 'tseep' 
@@ -41,7 +41,7 @@ export class SchemaValidationService {
             const timeout = setTimeout( () => reject({ status: 'error', hash, error: 'Request timed out, worker may have been terminated.' }), 5000)
             this.emitter.once(this.emitterKey(hash), (response: SchemaValidationServiceResponse) => {
                 clearTimeout(timeout)
-                const { result, error } = response;
+                const { error } = response;
                 if(error) {
                     reject(response)
                 }
@@ -52,6 +52,11 @@ export class SchemaValidationService {
         })
     }
 
+    private onmessage(message: MessageEvent<SchemaValidationServiceResponse>){
+        const { hash } = message.data;
+        this.emitter.emit(this.emitterKey(hash), message.data)
+    }
+
     async validate(request: SchemaValidationServiceRequest, hash?: string): Promise<SchemaValidationServiceResponse> {
         hash = hash ?? deterministicHash(request.json)
         this._subIds.add(hash)
@@ -59,12 +64,12 @@ export class SchemaValidationService {
         return this.respond(hash)
     }
     
-    async validateNip11(nip11: string): Promise<SchemaValidationServiceResponse> {
+    async validateNip11(nip11: any, hash?: string): Promise<SchemaValidationServiceResponse> {
         const request: SchemaValidationServiceRequest = { 
             type: 'nip11',
             json: nip11
         }
-        return this.validate(request)
+        return this.validate(request, hash)
     }
 
     async validateMessage(json: string, subject: string, slug: string): Promise<SchemaValidationServiceResponse> {
@@ -89,9 +94,4 @@ export class SchemaValidationService {
         }
         return this.validate(request, hash)
     }
-
-    private onmessage(message: MessageEvent<SchemaValidationServiceResponse>){
-        const { hash } = message.data;
-        this.emitter.emit(this.emitterKey(hash), message.data)
-    }
 }
\ No newline at end of file
diff --git a/apps/gui/src/lib/services/SchemaValidationService/schemavalidation.worker.ts b/apps/gui/src/lib/services/SchemaValidationService/schemavalidation.worker.ts
index 528cbc99..6fd29a1d 100644
--- a/apps/gui/src/lib/services/SchemaValidationService/schemavalidation.worker.ts
+++ b/apps/gui/src/lib/services/SchemaValidationService/schemavalidation.worker.ts
@@ -1,42 +1,41 @@
-import { deterministicHash } from '@nostrwatch/nip66/utils/hash';
-import { type SchemaValidatorResult } from '@nostrwatch/schemata-js-ajv'
+import { deterministicHash } from '@nostrwatch/nip66/utils';
+import { validateNip11, validateMessage, validateNote, type SchemaValidatorResult } from '@nostrwatch/schemata-js-ajv'
 
 self.onmessage = ({ data }) => {
-    import('@nostrwatch/schemata-js-ajv').then( ({validateNip11, validateMessage, validateNote}) => {
-        const { json, type, subject, slug } = data as any;
-        let { hash } = data as any;
-        let result: SchemaValidatorResult;
-        let error: string = '';
-        if(!hash) {
-            hash = deterministicHash(json)
+    console.log('schema validation worker recieved data', data)
+    const { json, type, subject, slug } = data as any;
+    let { hash } = data as any;
+    let result: SchemaValidatorResult;
+    let error: string = '';
+    if(!hash) {
+        hash = deterministicHash(json)
+    }
+    if(type === 'nip11') {
+        result = validateNip11(json)
+    }
+    else if(type === 'message') {
+        if(subject && slug) {
+            result = validateMessage(json, subject, slug)
         }
-        if(type === 'nip11') {
-            result = validateNip11(json)
+        else {
+            error = 'Both subject and slug are required for message validation (for example subject as "relay" and slug as "ok" for the NIP-01 "OK" message.)'
         }
-        else if(type === 'message') {
-            if(subject && slug) {
-                result = validateMessage(json, subject, slug)
-            }
-            else {
-                error = 'Both subject and slug are required for message validation (for example subject "relay" and slug "ok"'
-            }
-        }
-        else if(type === 'note') {
-            result = validateNote(json)
-        }
-        if(result) {
-            self.postMessage({
-                status: 'success',
-                hash,
-                result
-            })
-        }
-        else if(error){
-            self.postMessage({
-                status: 'error',
-                hash,
-                error
-            })
-        }
-    });
+    }
+    else if(type === 'note') {
+        result = validateNote(json)
+    }
+    if(result) {
+        self.postMessage({
+            status: 'success',
+            hash,
+            result
+        })
+    }
+    else if(error){
+        self.postMessage({
+            status: 'error',
+            hash,
+            error
+        })
+    }
 }
\ No newline at end of file
diff --git a/apps/gui/src/routes/relays/[protocol]/[...relay]/+page.svelte b/apps/gui/src/routes/relays/[protocol]/[...relay]/+page.svelte
index 9e78088b..5f0c1a54 100644
--- a/apps/gui/src/routes/relays/[protocol]/[...relay]/+page.svelte
+++ b/apps/gui/src/routes/relays/[protocol]/[...relay]/+page.svelte
@@ -36,6 +36,7 @@
   let CardSpeed: typeof import('$lib/components/partials/relay-single/cards/CardSpeed.svelte').default | null = null;
   let CardNips: typeof import('$lib/components/partials/relay-single/cards/CardNips.svelte').default | null = null;
   let RelayAudits: typeof import('$lib/components/partials/relay-single/RelayAudits.svelte').default | null = null;
+  let RelayNip11: typeof import('$lib/components/partials/relay-single/RelayNip11.svelte').default | null = null;
 
   const loadComponent = async (importFunc: () => Promise<any>, setter: (component: any) => void) => {
       try {
@@ -47,11 +48,11 @@
   };
 
   const loadComponents = () => {
+      loadComponent(() => import('svelte-bricks'), (comp) => Masonry = comp);
       loadComponent(() => import('$lib/components/partials/ProfileCompact.svelte'), (comp) => ProfileCompact = comp);
       loadComponent(() => import('$lib/components/partials/relay-single/RelayChecks.svelte'), (comp) => RelayChecks = comp);
       loadComponent(() => import('$lib/components/partials/relay-single/OperatorFeed.svelte'), (comp) => OperatorFeed = comp);
       loadComponent(() => import('$lib/components/ui/tabs'), (comp) => Tabs = comp);
-      loadComponent(() => import('svelte-bricks'), (comp) => Masonry = comp);
       loadComponent(() => import('$lib/components/partials/relay-single/cards/CardChecks.svelte'), (comp) => CardChecks = comp);
       loadComponent(() => import('$lib/components/partials/relay-single/cards/CardFees.svelte'), (comp) => CardFees = comp);
       loadComponent(() => import('$lib/components/partials/relay-single/cards/CardInsights.svelte'), (comp) => CardInsights = comp);
@@ -63,6 +64,7 @@
       loadComponent(() => import('$lib/components/partials/relay-single/cards/CardSpeed.svelte'), (comp) => CardSpeed = comp);
       loadComponent(() => import('$lib/components/partials/relay-single/cards/CardNips.svelte'), (comp) => CardNips = comp);
       loadComponent(() => import('$lib/components/partials/relay-single/RelayAudits.svelte'), (comp) => RelayAudits = comp);
+      loadComponent(() => import('$lib/components/partials/relay-single/RelayNip11.svelte'), (comp) => RelayNip11 = comp);
   };
 
   doBootstrap.set(false);
@@ -389,19 +391,25 @@
             </Tabs.Content>
           {/if}
 
-          {#if Tabs && RelayChecks}
+          {#if Tabs && RelayChecks && $relayAggregate}
             <Tabs.Content value="checks" class="py-6">
               <RelayChecks relay={relayUrl} monitors={$monitors} checks={$checksrelay} aggregate={$relayAggregate} />
             </Tabs.Content>
           {/if}
 
+
+          {#if Tabs && RelayNip11 && $nip11Ready}
           <Tabs.Content value="nip11">
             <!-- {$nip11s.get(relayUrl)?.length ?? 0} NIP-11s from NIP-66 events [{$nip11s.get(relayUrl)?.[0] ? true : false}] <br /> -->
             <!-- {#if $nip11sLocal?.get(relayUrl)}
               NIP-11 found locally <br />
             {/if} -->
-            <pre class="py-6 px-8 bg-white/5 rounded-lg">{JSON.stringify($nip11?.json, null, 4)}</pre>
+            <!-- <pre class="py-6 px-8 bg-white/5 rounded-lg">
+              {JSON.stringify($nip11?.json, null, 4)}
+            </pre> -->
+            <RelayNip11 {nip11} />
           </Tabs.Content>
+          {/if}
 
           
           <Tabs.Content value="operator-feed">
diff --git a/libraries/schemata-js-ajv/src/index.ts b/libraries/schemata-js-ajv/src/index.ts
index a3df212d..fc6c8e02 100644
--- a/libraries/schemata-js-ajv/src/index.ts
+++ b/libraries/schemata-js-ajv/src/index.ts
@@ -1,12 +1,8 @@
 import { Ajv, type ErrorObject } from 'ajv';
-import addErrors from 'ajv-errors'
 
 import * as NostrSchemata from '@nostrwatch/schemata'
 import { type NostrEvent } from 'nostr-tools'
 
-const ajv = new Ajv({ allErrors: true });
-addErrors(ajv);
-
 type NostrSchemataType = typeof NostrSchemata;
 
 const defaultResult: SchemaValidatorResult = { valid: false, errors: [], warnings: [] }
@@ -18,6 +14,8 @@ export type SchemaValidatorResult = {
 }
 
 const validate = (schema: any, data: any): SchemaValidatorResult => {
+    const ajv = new Ajv({ allErrors: true });
+    ajv.addKeyword("errorMessage")
     const result = structuredClone(defaultResult)
     const validate = ajv.compile(schema);
     const valid = validate(data);
diff --git a/libraries/schemata/nips/nip-11/schema.yaml b/libraries/schemata/nips/nip-11/schema.yaml
index 0a603df0..6f937c81 100644
--- a/libraries/schemata/nips/nip-11/schema.yaml
+++ b/libraries/schemata/nips/nip-11/schema.yaml
@@ -84,20 +84,6 @@ properties:
         type: number
       created_at_upper_limit:
         type: number
-    required:
-    - max_message_length
-    - max_subscriptions
-    - max_filters
-    - max_limit
-    - max_subid_length
-    - max_event_tags
-    - max_content_length
-    - min_pow_difficulty
-    - auth_required
-    - payment_required
-    - restricted_writes
-    - created_at_lower_limit
-    - created_at_upper_limit
   payments_url:
     type: string
     pattern: "^https?://"
@@ -111,23 +97,6 @@ properties:
       publication:
         $ref: "#/$defs/fee"
     additionalProperties: false
-required:
-- name
-- description
-- pubkey
-- contact
-- supported_nips
-- software
-- version
-- retention
-- relay_country
-- icon
-- language_tags
-- tags
-- posting_policy
-- limitation
-- payments_url
-- fees
 allOf:
 - if:
     properties:
@@ -207,4 +176,4 @@ allOf:
     - kinds
     - count
     - time
-additionalProperties: false
+additionalProperties: false
\ No newline at end of file