diff --git a/api/serializers.py b/api/serializers.py index 18909f1c..55704854 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -267,12 +267,18 @@ class OrderDetailSerializer(serializers.ModelSerializer): maker_hash_id = serializers.CharField( required=False, help_text="The maker's robot hash" ) + maker_nostr_pubkey = serializers.CharField( + required=False, help_text="The maker's robot nostr hex pubkey" + ) taker_nick = serializers.CharField( required=False, help_text="The taker's robot hash" ) taker_hash_id = serializers.CharField( required=False, help_text="The taker's robot hash" ) + taker_nostr_pubkey = serializers.CharField( + required=False, help_text="The taker's robot nostr hex pubkey" + ) status_message = serializers.CharField( required=False, help_text="The current status of the order corresponding to the `status`", @@ -444,8 +450,10 @@ class OrderDetailSerializer(serializers.ModelSerializer): "is_seller", "maker_nick", "maker_hash_id", + "maker_nostr_pubkey", "taker_nick", "taker_hash_id", + "taker_nostr_pubkey", "status_message", "is_fiat_sent", "is_disputed", diff --git a/api/views.py b/api/views.py index 91f4fbab..31ffe73f 100644 --- a/api/views.py +++ b/api/views.py @@ -267,6 +267,7 @@ class OrderView(viewsets.ViewSet): data["maker_nick"] = str(order.maker) data["maker_hash_id"] = str(order.maker.robot.hash_id) + data["maker_nostr_pubkey"] = str(order.maker.robot.nostr_pubkey) # Add activity status of participants based on last_seen data["maker_status"] = Logics.user_activity_status(order.maker.last_login) @@ -308,6 +309,7 @@ class OrderView(viewsets.ViewSet): data["taker_nick"] = str(order.taker) if order.taker: data["taker_hash_id"] = str(order.taker.robot.hash_id) + data["taker_nostr_pubkey"] = str(order.taker.robot.nostr_pubkey) data["status_message"] = Order.Status(order.status).label data["is_fiat_sent"] = order.is_fiat_sent data["latitude"] = order.latitude diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0fe4d496..44946e3c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -34,7 +34,7 @@ "latlon-geohash": "^2.0.0", "leaflet": "^1.9.4", "light-bolt11-decoder": "^3.1.1", - "nostr-tools": "^2.10.4", + "nostr-tools": "^2.12.0", "npm": "^11.0.0", "openpgp": "^5.11.0", "react": "^18.2.0", @@ -12234,9 +12234,9 @@ } }, "node_modules/nostr-tools": { - "version": "2.10.4", - "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.10.4.tgz", - "integrity": "sha512-biU7sk+jxHgVASfobg2T5ttxOGGSt69wEVBC51sHHOEaKAAdzHBLV/I2l9Rf61UzClhliZwNouYhqIso4a3HYg==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.12.0.tgz", + "integrity": "sha512-pUWEb020gTvt1XZvTa8AKNIHWFapjsv2NKyk43Ez2nnvz6WSXsrTFE0XtkNLSRBjPn6EpxumKeNiVzLz74jNSA==", "license": "Unlicense", "dependencies": { "@noble/ciphers": "^0.5.1", diff --git a/frontend/package.json b/frontend/package.json index 70b70492..e0ca1084 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -75,7 +75,7 @@ "latlon-geohash": "^2.0.0", "leaflet": "^1.9.4", "light-bolt11-decoder": "^3.1.1", - "nostr-tools": "^2.10.4", + "nostr-tools": "^2.12.0", "npm": "^11.0.0", "openpgp": "^5.11.0", "react": "^18.2.0", diff --git a/frontend/src/components/TradeBox/EncryptedChat/EncryptedSocketChat/index.tsx b/frontend/src/components/TradeBox/EncryptedChat/EncryptedSocketChat/index.tsx index 00225d8a..2d6c2614 100644 --- a/frontend/src/components/TradeBox/EncryptedChat/EncryptedSocketChat/index.tsx +++ b/frontend/src/components/TradeBox/EncryptedChat/EncryptedSocketChat/index.tsx @@ -38,6 +38,7 @@ interface Props { setMessages: (messages: EncryptedChatMessage[]) => void; turtleMode: boolean; setTurtleMode: (state: boolean) => void; + onSendMessage: (content: string) => void; } const EncryptedSocketChat: React.FC = ({ @@ -51,6 +52,7 @@ const EncryptedSocketChat: React.FC = ({ setMessages, turtleMode, setTurtleMode, + onSendMessage, }: Props): JSX.Element => { const { t } = useTranslation(); const theme = useTheme(); @@ -248,6 +250,7 @@ const EncryptedSocketChat: React.FC = ({ } // If input string contains '#' send unencrypted and unlogged message else if (connection != null && value.substring(0, 1) === '#') { + onSendMessage(value); connection.send( JSON.stringify({ type: 'message', @@ -263,6 +266,7 @@ const EncryptedSocketChat: React.FC = ({ setValue(''); setWaitingEcho(true); setLastSent(value); + onSendMessage(value); encryptMessage(value, robot.pubKey, peerPubKey, robot.encPrivKey, slot.token) .then((encryptedMessage) => { if (connection != null) { diff --git a/frontend/src/components/TradeBox/EncryptedChat/EncryptedTurtleChat/index.tsx b/frontend/src/components/TradeBox/EncryptedChat/EncryptedTurtleChat/index.tsx index 259d4a4d..731e6c11 100644 --- a/frontend/src/components/TradeBox/EncryptedChat/EncryptedTurtleChat/index.tsx +++ b/frontend/src/components/TradeBox/EncryptedChat/EncryptedTurtleChat/index.tsx @@ -32,6 +32,7 @@ interface Props { setMessages: (messages: EncryptedChatMessage[]) => void; turtleMode: boolean; setTurtleMode: (state: boolean) => void; + onSendMessage: (content: string) => void; } const audioPath = @@ -50,6 +51,7 @@ const EncryptedTurtleChat: React.FC = ({ setMessages, setTurtleMode, turtleMode, + onSendMessage, }: Props): JSX.Element => { const { t } = useTranslation(); const theme = useTheme(); @@ -198,6 +200,7 @@ const EncryptedTurtleChat: React.FC = ({ const { url, basePath } = federation .getCoordinator(garage.getSlot()?.activeOrder?.shortAlias ?? '') .getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl); + onSendMessage(value); apiClient .post( url + basePath, @@ -226,6 +229,7 @@ const EncryptedTurtleChat: React.FC = ({ else if (value !== '' && Boolean(robot?.pubKey)) { setWaitingEcho(true); setLastSent(value); + onSendMessage(value); encryptMessage(value, robot?.pubKey, peerPubKey ?? '', robot?.encPrivKey, slot?.token) .then((encryptedMessage) => { const { url, basePath } = federation diff --git a/frontend/src/components/TradeBox/EncryptedChat/index.tsx b/frontend/src/components/TradeBox/EncryptedChat/index.tsx index fe2203c7..8bb4bcbb 100644 --- a/frontend/src/components/TradeBox/EncryptedChat/index.tsx +++ b/frontend/src/components/TradeBox/EncryptedChat/index.tsx @@ -1,7 +1,13 @@ -import React, { useState } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import { type Order, type Robot } from '../../../models'; import EncryptedSocketChat from './EncryptedSocketChat'; import EncryptedTurtleChat from './EncryptedTurtleChat'; +import { nip17 } from 'nostr-tools'; +import { GarageContext, type UseGarageStoreType } from '../../../contexts/GarageContext'; +import { + FederationContext, + type UseFederationStoreType, +} from '../../../contexts/FederationContext'; interface Props { order: Order; @@ -36,11 +42,35 @@ const EncryptedChat: React.FC = ({ status, }: Props): JSX.Element => { const [turtleMode, setTurtleMode] = useState(false); + const { garage } = useContext(GarageContext); + const { federation } = useContext(FederationContext); + + useEffect(() => { + const coordinator = federation.getCoordinator(order.shortAlias); + federation.roboPool.connect([coordinator.getRelayUrl()]); + }, []); + + const onSendMessage = (content: string): void => { + const slot = garage.getSlot(); + const coordinator = federation.getCoordinator(order.shortAlias); + + if (!slot?.nostrSecKey) return; + + const recipient = { + publicKey: order.is_maker ? order.taker_nostr_pubkey : order.maker_nostr_pubkey, + relayUrl: coordinator.getRelayUrl(), + }; + + const wrappedEvent = nip17.wrapEvent(slot?.nostrSecKey, recipient, content); + + federation.roboPool.sendEvent(wrappedEvent); + }; return turtleMode ? ( = ({ status={status} messages={messages} setMessages={setMessages} + onSendMessage={onSendMessage} order={order} takerNick={order.taker_nick} takerHashId={order.taker_hash_id} diff --git a/frontend/src/models/Coordinator.model.ts b/frontend/src/models/Coordinator.model.ts index 17127b32..fa4ccb0e 100644 --- a/frontend/src/models/Coordinator.model.ts +++ b/frontend/src/models/Coordinator.model.ts @@ -303,6 +303,10 @@ export class Coordinator { return { url: String(this[network][origin]), basePath: '' }; } }; + + getRelayUrl = (): string => { + return `ws://${this.url.replace(/^https?:\/\//, '')}/nostr`; + }; } export default Coordinator; diff --git a/frontend/src/models/Federation.model.ts b/frontend/src/models/Federation.model.ts index efb597ea..cb9fa2eb 100644 --- a/frontend/src/models/Federation.model.ts +++ b/frontend/src/models/Federation.model.ts @@ -65,7 +65,7 @@ export class Federation { if (tesnetHost) settings.network = 'testnet'; this.connection = null; - this.roboPool = new RoboPool(settings, origin); + this.roboPool = new RoboPool(settings, Object.values(this.coordinators)); } private coordinators: Record; diff --git a/frontend/src/models/Order.model.ts b/frontend/src/models/Order.model.ts index d140f04f..7a5e9dfd 100644 --- a/frontend/src/models/Order.model.ts +++ b/frontend/src/models/Order.model.ts @@ -93,8 +93,10 @@ class Order { is_seller: boolean = false; maker_nick: string = ''; maker_hash_id: string = ''; + maker_nostr_pubkey: string = ''; taker_nick: string = ''; taker_hash_id: string = ''; + taker_nostr_pubkey: string = ''; status_message: string = ''; is_fiat_sent: boolean = false; is_disputed: boolean = false; diff --git a/frontend/src/models/Robot.model.ts b/frontend/src/models/Robot.model.ts index c16af9dc..8e4c1541 100644 --- a/frontend/src/models/Robot.model.ts +++ b/frontend/src/models/Robot.model.ts @@ -75,6 +75,7 @@ class Robot { last_login: data.last_login, pubKey: data.public_key, encPrivKey: data.encrypted_private_key, + nostrPubKey: data.nostr_pubkey, }); }) .catch((e) => { diff --git a/frontend/src/services/RoboPool/index.ts b/frontend/src/services/RoboPool/index.ts index 9c5c487f..264052dd 100644 --- a/frontend/src/services/RoboPool/index.ts +++ b/frontend/src/services/RoboPool/index.ts @@ -1,5 +1,5 @@ import { type Event } from 'nostr-tools'; -import { type Settings } from '../../models'; +import { type Coordinator, type Settings } from '../../models'; import defaultFederation from '../../../static/federation.json'; import { websocketClient, type WebsocketConnection, WebsocketState } from '../Websocket'; import thirdParties from '../../../static/thirdparties.json'; @@ -10,19 +10,12 @@ interface RoboPoolEvents { } class RoboPool { - constructor(settings: Settings, origin: string) { + constructor(settings: Settings, coordinators: Coordinator[]) { this.network = settings.network ?? 'mainnet'; this.relays = []; - const federationRelays = Object.values(defaultFederation) - .map((coord) => { - const url: string = coord[this.network]?.[settings.selfhostedClient ? 'onion' : origin]; + const federationRelays = coordinators.map((coord) => coord.getRelayUrl()); - if (!url) return undefined; - - return `ws://${url.replace(/^https?:\/\//, '')}/nostr`; - }) - .filter((item) => item !== undefined); if (settings.host) { const hostNostr = `ws://${settings.host.replace(/^https?:\/\//, '')}/nostr`; if (federationRelays.includes(hostNostr)) { @@ -44,8 +37,8 @@ class RoboPool { public webSockets: Record = {}; private readonly messageHandlers: Array<(url: string, event: MessageEvent) => void> = []; - connect = (): void => { - this.relays.forEach((url: string) => { + connect = (relays: string[] = this.relays): void => { + relays.forEach((url: string) => { if (Object.keys(this.webSockets).find((wUrl) => wUrl === url)) return; this.webSockets[url] = null;