Compare commits
	
		
			2 Commits
		
	
	
		
			36a1cad977
			...
			81f60a8c38
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 81f60a8c38 | |||
| 99521fe8c7 | 
							
								
								
									
										3
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "cSpell.words": ["Nostr"]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										127
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										127
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -29,13 +29,15 @@
 | 
				
			|||||||
        "gatsby-source-filesystem": "^3.3.0",
 | 
					        "gatsby-source-filesystem": "^3.3.0",
 | 
				
			||||||
        "gatsby-transformer-remark": "^4.0.0",
 | 
					        "gatsby-transformer-remark": "^4.0.0",
 | 
				
			||||||
        "gatsby-transformer-sharp": "^3.3.0",
 | 
					        "gatsby-transformer-sharp": "^3.3.0",
 | 
				
			||||||
 | 
					        "nostr-tools": "^2.7.0",
 | 
				
			||||||
        "react": "^17.0.1",
 | 
					        "react": "^17.0.1",
 | 
				
			||||||
        "react-dom": "^17.0.1",
 | 
					        "react-dom": "^17.0.1",
 | 
				
			||||||
        "react-helmet": "^6.1.0",
 | 
					        "react-helmet": "^6.1.0",
 | 
				
			||||||
        "react-icons": "^4.2.0",
 | 
					        "react-icons": "^4.2.0",
 | 
				
			||||||
        "react-share": "^4.4.0",
 | 
					        "react-share": "^4.4.0",
 | 
				
			||||||
        "react-use-flexsearch": "^0.1.1",
 | 
					        "react-use-flexsearch": "^0.1.1",
 | 
				
			||||||
        "styled-components": "^5.2.3"
 | 
					        "styled-components": "^5.2.3",
 | 
				
			||||||
 | 
					        "tseep": "^1.2.1"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "devDependencies": {
 | 
					      "devDependencies": {
 | 
				
			||||||
        "@popperjs/core": "^2.9.2",
 | 
					        "@popperjs/core": "^2.9.2",
 | 
				
			||||||
@@ -3114,6 +3116,47 @@
 | 
				
			|||||||
        "eslint-scope": "5.1.1"
 | 
					        "eslint-scope": "5.1.1"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@noble/ciphers": {
 | 
				
			||||||
 | 
					      "version": "0.5.3",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==",
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "url": "https://paulmillr.com/funding/"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@noble/curves": {
 | 
				
			||||||
 | 
					      "version": "1.2.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "@noble/hashes": "1.3.2"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "url": "https://paulmillr.com/funding/"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@noble/curves/node_modules/@noble/hashes": {
 | 
				
			||||||
 | 
					      "version": "1.3.2",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==",
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">= 16"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "url": "https://paulmillr.com/funding/"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@noble/hashes": {
 | 
				
			||||||
 | 
					      "version": "1.3.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">= 16"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "url": "https://paulmillr.com/funding/"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/@nodelib/fs.scandir": {
 | 
					    "node_modules/@nodelib/fs.scandir": {
 | 
				
			||||||
      "version": "2.1.5",
 | 
					      "version": "2.1.5",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
 | 
				
			||||||
@@ -3215,6 +3258,53 @@
 | 
				
			|||||||
        "url": "https://opencollective.com/popperjs"
 | 
					        "url": "https://opencollective.com/popperjs"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@scure/base": {
 | 
				
			||||||
 | 
					      "version": "1.1.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==",
 | 
				
			||||||
 | 
					      "funding": [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "type": "individual",
 | 
				
			||||||
 | 
					          "url": "https://paulmillr.com/funding/"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@scure/bip32": {
 | 
				
			||||||
 | 
					      "version": "1.3.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "@noble/curves": "~1.1.0",
 | 
				
			||||||
 | 
					        "@noble/hashes": "~1.3.1",
 | 
				
			||||||
 | 
					        "@scure/base": "~1.1.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "url": "https://paulmillr.com/funding/"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@scure/bip32/node_modules/@noble/curves": {
 | 
				
			||||||
 | 
					      "version": "1.1.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "@noble/hashes": "1.3.1"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "url": "https://paulmillr.com/funding/"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@scure/bip39": {
 | 
				
			||||||
 | 
					      "version": "1.2.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "@noble/hashes": "~1.3.0",
 | 
				
			||||||
 | 
					        "@scure/base": "~1.1.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "url": "https://paulmillr.com/funding/"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/@sideway/address": {
 | 
					    "node_modules/@sideway/address": {
 | 
				
			||||||
      "version": "4.1.5",
 | 
					      "version": "4.1.5",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
 | 
				
			||||||
@@ -15854,6 +15944,36 @@
 | 
				
			|||||||
        "url": "https://github.com/sponsors/sindresorhus"
 | 
					        "url": "https://github.com/sponsors/sindresorhus"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/nostr-tools": {
 | 
				
			||||||
 | 
					      "version": "2.7.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.7.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-jJoL2J1CBiKDxaXZww27nY/Wsuxzx7AULxmGKFce4sskDu1tohNyfnzYQ8BvDyvkstU8kNZUAXPL32tre33uig==",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "@noble/ciphers": "^0.5.1",
 | 
				
			||||||
 | 
					        "@noble/curves": "1.2.0",
 | 
				
			||||||
 | 
					        "@noble/hashes": "1.3.1",
 | 
				
			||||||
 | 
					        "@scure/base": "1.1.1",
 | 
				
			||||||
 | 
					        "@scure/bip32": "1.3.1",
 | 
				
			||||||
 | 
					        "@scure/bip39": "1.2.1"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "optionalDependencies": {
 | 
				
			||||||
 | 
					        "nostr-wasm": "v0.1.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "peerDependencies": {
 | 
				
			||||||
 | 
					        "typescript": ">=5.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "peerDependenciesMeta": {
 | 
				
			||||||
 | 
					        "typescript": {
 | 
				
			||||||
 | 
					          "optional": true
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/nostr-wasm": {
 | 
				
			||||||
 | 
					      "version": "0.1.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==",
 | 
				
			||||||
 | 
					      "optional": true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/not": {
 | 
					    "node_modules/not": {
 | 
				
			||||||
      "version": "0.1.0",
 | 
					      "version": "0.1.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/not/-/not-0.1.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/not/-/not-0.1.0.tgz",
 | 
				
			||||||
@@ -21030,6 +21150,11 @@
 | 
				
			|||||||
        "json5": "lib/cli.js"
 | 
					        "json5": "lib/cli.js"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/tseep": {
 | 
				
			||||||
 | 
					      "version": "1.2.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/tseep/-/tseep-1.2.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-VFnsNcPGC4qFJ1nxbIPSjTmtRZOhlqLmtwRqtLVos8mbRHki8HO9cy9Z1e89EiWyxFmq6LBviI9TQjijxw/mEw=="
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/tslib": {
 | 
					    "node_modules/tslib": {
 | 
				
			||||||
      "version": "1.14.1",
 | 
					      "version": "1.14.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -38,13 +38,15 @@
 | 
				
			|||||||
    "gatsby-source-filesystem": "^3.3.0",
 | 
					    "gatsby-source-filesystem": "^3.3.0",
 | 
				
			||||||
    "gatsby-transformer-remark": "^4.0.0",
 | 
					    "gatsby-transformer-remark": "^4.0.0",
 | 
				
			||||||
    "gatsby-transformer-sharp": "^3.3.0",
 | 
					    "gatsby-transformer-sharp": "^3.3.0",
 | 
				
			||||||
 | 
					    "nostr-tools": "^2.7.0",
 | 
				
			||||||
    "react": "^17.0.1",
 | 
					    "react": "^17.0.1",
 | 
				
			||||||
    "react-dom": "^17.0.1",
 | 
					    "react-dom": "^17.0.1",
 | 
				
			||||||
    "react-helmet": "^6.1.0",
 | 
					    "react-helmet": "^6.1.0",
 | 
				
			||||||
    "react-icons": "^4.2.0",
 | 
					    "react-icons": "^4.2.0",
 | 
				
			||||||
    "react-share": "^4.4.0",
 | 
					    "react-share": "^4.4.0",
 | 
				
			||||||
    "react-use-flexsearch": "^0.1.1",
 | 
					    "react-use-flexsearch": "^0.1.1",
 | 
				
			||||||
    "styled-components": "^5.2.3"
 | 
					    "styled-components": "^5.2.3",
 | 
				
			||||||
 | 
					    "tseep": "^1.2.1"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
    "@popperjs/core": "^2.9.2",
 | 
					    "@popperjs/core": "^2.9.2",
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										205
									
								
								src/controllers/NostrController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										205
									
								
								src/controllers/NostrController.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,205 @@
 | 
				
			|||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Event,
 | 
				
			||||||
 | 
					  EventTemplate,
 | 
				
			||||||
 | 
					  SimplePool,
 | 
				
			||||||
 | 
					  UnsignedEvent,
 | 
				
			||||||
 | 
					  finalizeEvent,
 | 
				
			||||||
 | 
					  nip04,
 | 
				
			||||||
 | 
					  nip19,
 | 
				
			||||||
 | 
					  verifyEvent,
 | 
				
			||||||
 | 
					  generateSecretKey,
 | 
				
			||||||
 | 
					  getPublicKey
 | 
				
			||||||
 | 
					} from 'nostr-tools'
 | 
				
			||||||
 | 
					import { EventEmitter } from 'tseep'
 | 
				
			||||||
 | 
					import { SignedEvent, Keys } from '../types'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class NostrController extends EventEmitter {
 | 
				
			||||||
 | 
					  private static instance: NostrController
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private generatedKeys: Keys | undefined
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private constructor() {
 | 
				
			||||||
 | 
					    super()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.generatedKeys = this.generateKeys()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public static getInstance(): NostrController {
 | 
				
			||||||
 | 
					    if (!NostrController.instance) {
 | 
				
			||||||
 | 
					      NostrController.instance = new NostrController()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return NostrController.instance
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Function will publish provided event to the provided relays
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  publishEvent = async (event: Event, relays: string[]) => {
 | 
				
			||||||
 | 
					    const simplePool = new SimplePool()
 | 
				
			||||||
 | 
					    const promises = simplePool.publish(relays, event)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const results = await Promise.allSettled(promises)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const publishedRelays: string[] = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    results.forEach((result, index) => {
 | 
				
			||||||
 | 
					      if (result.status === 'fulfilled') publishedRelays.push(relays[index])
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (publishedRelays.length === 0) {
 | 
				
			||||||
 | 
					      const failedPublishes: any[] = []
 | 
				
			||||||
 | 
					      const fallbackRejectionReason =
 | 
				
			||||||
 | 
					        'Attempt to publish an event has been rejected with unknown reason.'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      results.forEach((res, index) => {
 | 
				
			||||||
 | 
					        if (res.status === 'rejected') {
 | 
				
			||||||
 | 
					          failedPublishes.push({
 | 
				
			||||||
 | 
					            relay: relays[index],
 | 
				
			||||||
 | 
					            error: res.reason
 | 
				
			||||||
 | 
					              ? res.reason.message || fallbackRejectionReason
 | 
				
			||||||
 | 
					              : fallbackRejectionReason
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      throw failedPublishes
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return publishedRelays
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Signs an event with private key (if it is present in local storage) or
 | 
				
			||||||
 | 
					   * with browser extension (if it is present) or
 | 
				
			||||||
 | 
					   * with nSecBunker instance.
 | 
				
			||||||
 | 
					   * @param event - unsigned nostr event.
 | 
				
			||||||
 | 
					   * @returns - a promised that is resolved with signed nostr event.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  signEvent = async (
 | 
				
			||||||
 | 
					    event: UnsignedEvent | EventTemplate
 | 
				
			||||||
 | 
					  ): Promise<SignedEvent> => {
 | 
				
			||||||
 | 
					    if (!this.generatedKeys) {
 | 
				
			||||||
 | 
					      throw new Error(`Private & public key pair is not found.`)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { private: nsec } = this.generatedKeys
 | 
				
			||||||
 | 
					    const privateKey = nip19.decode(nsec).data as Uint8Array
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const signedEvent = finalizeEvent(event, privateKey)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.verifySignedEvent(signedEvent)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return Promise.resolve(signedEvent)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  nip04Encrypt = async (receiver: string, content: string) => {
 | 
				
			||||||
 | 
					    if (!this.generatedKeys) {
 | 
				
			||||||
 | 
					      throw new Error(`Private & public key pair is not found.`)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { private: nsec } = this.generatedKeys
 | 
				
			||||||
 | 
					    const privateKey = nip19.decode(nsec).data as Uint8Array
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const encrypted = await nip04.encrypt(privateKey, receiver, content)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return encrypted
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Sends a Direct Message (DM) to a recipient, encrypting the content and handling authentication.
 | 
				
			||||||
 | 
					   * @param fileUrl The URL of the encrypted zip file to be included in the DM.
 | 
				
			||||||
 | 
					   * @param encryptionKey The encryption key used to decrypt the zip file to be included in the DM.
 | 
				
			||||||
 | 
					   * @param pubkey The public key of the recipient.
 | 
				
			||||||
 | 
					   * @param isSigner Boolean indicating whether the recipient is a signer or viewer.
 | 
				
			||||||
 | 
					   * @param setAuthUrl Function to set the authentication URL in the component state.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  sendDM = async (pubkey: string, message: string) => {
 | 
				
			||||||
 | 
					    // Set up timeout promise to handle encryption timeout
 | 
				
			||||||
 | 
					    const timeoutPromise = new Promise<never>((_, reject) => {
 | 
				
			||||||
 | 
					      setTimeout(() => {
 | 
				
			||||||
 | 
					        reject(new Error('Timeout occurred'))
 | 
				
			||||||
 | 
					      }, 60000) // Timeout duration = 60 seconds
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Encrypt the DM content, with timeout
 | 
				
			||||||
 | 
					    const encrypted = await Promise.race([
 | 
				
			||||||
 | 
					      this.nip04Encrypt(this.npubToHex(pubkey) as string, message),
 | 
				
			||||||
 | 
					      timeoutPromise
 | 
				
			||||||
 | 
					    ])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Return if encryption failed
 | 
				
			||||||
 | 
					    if (!encrypted) throw new Error('Message was not encrypted.')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Construct event metadata for the DM
 | 
				
			||||||
 | 
					    const event: EventTemplate = {
 | 
				
			||||||
 | 
					      kind: 4, // DM event type
 | 
				
			||||||
 | 
					      content: encrypted, // Encrypted DM content
 | 
				
			||||||
 | 
					      created_at: Math.floor(Date.now() / 1000), // Current timestamp
 | 
				
			||||||
 | 
					      tags: [['p', this.npubToHex(pubkey) as string]] // Tag with recipient's public key
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Sign the DM event
 | 
				
			||||||
 | 
					    const signedEvent = await this.signEvent(event)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Return if event signing failed
 | 
				
			||||||
 | 
					    if (!signedEvent) throw new Error('Message was not signed.')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // These relay will be used to send a DM. Recipient has to read from these relays to receive a DM.
 | 
				
			||||||
 | 
					    const relays = [
 | 
				
			||||||
 | 
					      'wss://relay.damus.io/',
 | 
				
			||||||
 | 
					      'wss://nos.lol/',
 | 
				
			||||||
 | 
					      'wss://relay.snort.social'
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Publish the signed DM event to the recipient's read relays
 | 
				
			||||||
 | 
					    return await this.publishEvent(signedEvent, relays)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * @param hexKey hex private or public key
 | 
				
			||||||
 | 
					   * @returns whether or not is key valid
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  validateHex = (hexKey: string) => {
 | 
				
			||||||
 | 
					    return hexKey.match(/^[a-f0-9]{64}$/)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * NPUB provided - it will convert NPUB to HEX
 | 
				
			||||||
 | 
					   * HEX provided - it will return HEX
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param pubKey in NPUB, HEX format
 | 
				
			||||||
 | 
					   * @returns HEX format
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  npubToHex = (pubKey: string): string | null => {
 | 
				
			||||||
 | 
					    // If key is NPUB
 | 
				
			||||||
 | 
					    if (pubKey.startsWith('npub1')) {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        return nip19.decode(pubKey).data as string
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        return null
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // valid hex key
 | 
				
			||||||
 | 
					    if (this.validateHex(pubKey)) return pubKey
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Not a valid hex key
 | 
				
			||||||
 | 
					    return null
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  generateKeys = (): Keys => {
 | 
				
			||||||
 | 
					    const nsec = generateSecretKey()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return { private: nip19.nsecEncode(nsec), public: getPublicKey(nsec) }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  verifySignedEvent = (event: SignedEvent) => {
 | 
				
			||||||
 | 
					    const isGood = verifyEvent(event)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!isGood) {
 | 
				
			||||||
 | 
					      throw new Error(
 | 
				
			||||||
 | 
					        'Signed event did not pass verification. Check sig, id and pubkey.'
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1
									
								
								src/controllers/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/controllers/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					export * from './NostrController'
 | 
				
			||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
import { PageProps, Link, graphql } from 'gatsby'
 | 
					import { PageProps, Link, graphql } from 'gatsby'
 | 
				
			||||||
import React from 'react'
 | 
					import React, { useState } from 'react'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import Layout from '../components/layout'
 | 
					import Layout from '../components/layout'
 | 
				
			||||||
import Seo from '../components/seo'
 | 
					import Seo from '../components/seo'
 | 
				
			||||||
@@ -20,6 +20,7 @@ import {
 | 
				
			|||||||
import contactBg from '../images/contact_bg.jpg'
 | 
					import contactBg from '../images/contact_bg.jpg'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import '../styledComponents/contact.css'
 | 
					import '../styledComponents/contact.css'
 | 
				
			||||||
 | 
					import { NostrController } from '../controllers'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type DataProps = {
 | 
					type DataProps = {
 | 
				
			||||||
  site: {
 | 
					  site: {
 | 
				
			||||||
@@ -32,6 +33,17 @@ type DataProps = {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Contact: React.FC<PageProps<DataProps>> = ({ data, location }) => {
 | 
					const Contact: React.FC<PageProps<DataProps>> = ({ data, location }) => {
 | 
				
			||||||
 | 
					  const nostrController = NostrController.getInstance()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [name, setName] = useState<string>()
 | 
				
			||||||
 | 
					  const [email, setEmail] = useState<string>()
 | 
				
			||||||
 | 
					  const [subject, setSubject] = useState<string>()
 | 
				
			||||||
 | 
					  const [message, setMessage] = useState<string>()
 | 
				
			||||||
 | 
					  const [notification, setNotification] = useState<string>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const getBorderStyle = (value: string | undefined) =>
 | 
				
			||||||
 | 
					    value === undefined ? {} : value ? {} : { border: '1px solid red' }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Layout
 | 
					    <Layout
 | 
				
			||||||
      location={location}
 | 
					      location={location}
 | 
				
			||||||
@@ -52,8 +64,35 @@ const Contact: React.FC<PageProps<DataProps>> = ({ data, location }) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            <form
 | 
					            <form
 | 
				
			||||||
              className="kwes-form"
 | 
					              className="kwes-form"
 | 
				
			||||||
              method="POST"
 | 
					              onSubmit={async (evt) => {
 | 
				
			||||||
              action="https://kwes.io/api/foreign/forms/mxKuyK4lxZWnG2WNH3ga"
 | 
					                evt.preventDefault()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (name && email && subject && message) {
 | 
				
			||||||
 | 
					                  const res = await nostrController
 | 
				
			||||||
 | 
					                    .sendDM(
 | 
				
			||||||
 | 
					                      'npub1dc0000002dtkw7et06sztc9nvk79r6yju8gk69sr88rgrg0e8cvsnptgyv',
 | 
				
			||||||
 | 
					                      `Name: ${name}
 | 
				
			||||||
 | 
					Email: ${email}
 | 
				
			||||||
 | 
					Subject: ${subject}
 | 
				
			||||||
 | 
					Message: ${message}`
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    .catch((err) => {
 | 
				
			||||||
 | 
					                      setNotification(
 | 
				
			||||||
 | 
					                        `Something went wrong. Please check the console for more information. Please try one more time.`
 | 
				
			||||||
 | 
					                      )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                      console.log(`Sending message error: `, err)
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  if (res && res.length) {
 | 
				
			||||||
 | 
					                    setNotification(`Message sent. We'll contact you shortly.`)
 | 
				
			||||||
 | 
					                  } else {
 | 
				
			||||||
 | 
					                    setNotification(
 | 
				
			||||||
 | 
					                      `Something went wrong. Please try one more time.`
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <div className="mb-3">
 | 
					              <div className="mb-3">
 | 
				
			||||||
                <StyledLabel htmlFor="name" className="form-label">
 | 
					                <StyledLabel htmlFor="name" className="form-label">
 | 
				
			||||||
@@ -65,6 +104,11 @@ const Contact: React.FC<PageProps<DataProps>> = ({ data, location }) => {
 | 
				
			|||||||
                  id="name"
 | 
					                  id="name"
 | 
				
			||||||
                  name="name"
 | 
					                  name="name"
 | 
				
			||||||
                  rules="required|max:50"
 | 
					                  rules="required|max:50"
 | 
				
			||||||
 | 
					                  onChange={(evt) => {
 | 
				
			||||||
 | 
					                    setName(evt.target.value)
 | 
				
			||||||
 | 
					                    setNotification(undefined)
 | 
				
			||||||
 | 
					                  }}
 | 
				
			||||||
 | 
					                  style={getBorderStyle(name)}
 | 
				
			||||||
                />
 | 
					                />
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
              <div className="mb-3">
 | 
					              <div className="mb-3">
 | 
				
			||||||
@@ -78,6 +122,11 @@ const Contact: React.FC<PageProps<DataProps>> = ({ data, location }) => {
 | 
				
			|||||||
                  name="email"
 | 
					                  name="email"
 | 
				
			||||||
                  rules="required|email"
 | 
					                  rules="required|email"
 | 
				
			||||||
                  aria-describedby="emailHelp"
 | 
					                  aria-describedby="emailHelp"
 | 
				
			||||||
 | 
					                  onChange={(evt) => {
 | 
				
			||||||
 | 
					                    setEmail(evt.target.value)
 | 
				
			||||||
 | 
					                    setNotification(undefined)
 | 
				
			||||||
 | 
					                  }}
 | 
				
			||||||
 | 
					                  style={getBorderStyle(email)}
 | 
				
			||||||
                />
 | 
					                />
 | 
				
			||||||
                <div id="emailHelp" className="form-text">
 | 
					                <div id="emailHelp" className="form-text">
 | 
				
			||||||
                  We'll never share your email with anyone else.
 | 
					                  We'll never share your email with anyone else.
 | 
				
			||||||
@@ -93,6 +142,11 @@ const Contact: React.FC<PageProps<DataProps>> = ({ data, location }) => {
 | 
				
			|||||||
                  id="subject"
 | 
					                  id="subject"
 | 
				
			||||||
                  name="subject"
 | 
					                  name="subject"
 | 
				
			||||||
                  rules="required|max:50"
 | 
					                  rules="required|max:50"
 | 
				
			||||||
 | 
					                  onChange={(evt) => {
 | 
				
			||||||
 | 
					                    setSubject(evt.target.value)
 | 
				
			||||||
 | 
					                    setNotification(undefined)
 | 
				
			||||||
 | 
					                  }}
 | 
				
			||||||
 | 
					                  style={getBorderStyle(subject)}
 | 
				
			||||||
                />
 | 
					                />
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
              <div className="mb-3">
 | 
					              <div className="mb-3">
 | 
				
			||||||
@@ -105,12 +159,18 @@ const Contact: React.FC<PageProps<DataProps>> = ({ data, location }) => {
 | 
				
			|||||||
                  name="message"
 | 
					                  name="message"
 | 
				
			||||||
                  rows="5"
 | 
					                  rows="5"
 | 
				
			||||||
                  rules="required|max:200"
 | 
					                  rules="required|max:200"
 | 
				
			||||||
 | 
					                  onChange={(evt) => {
 | 
				
			||||||
 | 
					                    setMessage(evt.target.value)
 | 
				
			||||||
 | 
					                    setNotification(undefined)
 | 
				
			||||||
 | 
					                  }}
 | 
				
			||||||
 | 
					                  style={getBorderStyle(message)}
 | 
				
			||||||
                ></textarea>
 | 
					                ></textarea>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
              <div className="mb-3">
 | 
					              <div className="mb-3">
 | 
				
			||||||
                <SolidButton theme="dark">Submit</SolidButton>
 | 
					                <SolidButton theme="dark">Submit</SolidButton>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
            </form>
 | 
					            </form>
 | 
				
			||||||
 | 
					            {notification && <span>{notification}</span>}
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <div className="col-md-6">
 | 
					          <div className="col-md-6">
 | 
				
			||||||
            <ContactBackground src={contactBg} info="Book a Demo" />
 | 
					            <ContactBackground src={contactBg} info="Book a Demo" />
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1
									
								
								src/types/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/types/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					export * from './nostr'
 | 
				
			||||||
							
								
								
									
										14
									
								
								src/types/nostr.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/types/nostr.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					export interface SignedEvent {
 | 
				
			||||||
 | 
					  kind: number
 | 
				
			||||||
 | 
					  tags: string[][]
 | 
				
			||||||
 | 
					  content: string
 | 
				
			||||||
 | 
					  created_at: number
 | 
				
			||||||
 | 
					  pubkey: string
 | 
				
			||||||
 | 
					  id: string
 | 
				
			||||||
 | 
					  sig: string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface Keys {
 | 
				
			||||||
 | 
					  private: string
 | 
				
			||||||
 | 
					  public: string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user