feat: nwc

This commit is contained in:
Kieran 2023-05-08 15:36:43 +01:00
parent 7baa85ca11
commit 9d67da3b6f
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
9 changed files with 272 additions and 18 deletions

View File

@ -0,0 +1,13 @@
import IconProps from "./IconProps";
export default function NostrIcon(props: IconProps) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 116.446 84.924" {...props}>
<path
fill="currentColor"
clip-rule="evenodd"
d="m35.805 39.467c1.512-1.608 5.559-0.682 6.96-2.4-0.595-1.9-4.07-4.608-4.319-6.96-0.112-1.057 0.563-1.379 0.96-2.64 0.243-0.775 0.004-1.643 0.239-2.16 0.681-1.492 2.526-2.548 2.88-4.08-1.356-6.734 4.686-8.103 8.641-10.32 4.301 0.146 9.927-1.066 13.68 0.96 0.113 0.754-0.646 0.634-0.72 1.2 0.339 0.541 1.563 0.197 1.439 1.2-1.327 1.862-4.511-0.112-5.52 1.68 0.646 0.634 1.735 0.824 2.4 1.44v2.64c-0.708 0.172-1.486 0.274-1.921 0.72 1.552 3.67-5.669 2.291-3.359 6 1.339-0.021 4.954-0.144 6.72-1.2 2.784-1.665 2.711-6.367 5.521-8.159 0.691-0.029 1.57 0.131 1.92-0.24 1.151-2.775 3.98-5.438 8.88-5.76 2.746-0.182 8.349-1.87 10.8 0.239 1.465 1.262 0.81 3.268 2.16 4.561 0.988 0.451 2.105 0.774 2.16 2.16 0.267 1.202-1.834 1.31-0.48 2.159-0.962 1.039-1.811 2.19-3.12 2.881-0.113 1.153 1.554 0.526 1.44 1.68-0.802 1.122-1.209 3.907-2.641 3.6-0.806 0.247-0.373-0.746-0.479-1.199-0.89 0.295-1.405 0.67-2.16 0-0.26 0.78-0.709 1.371-1.2 1.92 1.643 1.478 4.003 2.237 5.521 3.84 3.235-1.359 7.077-5.149 10.8-1.92 0.188 0.988-0.368 1.231-0.24 2.16 0.896 0.774 0.978-0.801 1.92-0.721 1.06 0.062 1.265 0.976 2.16 1.2 0.185 0.904-0.293 1.147-0.24 1.92 0.473 0.889 2.352 0.368 2.881 1.2 0.555 2.155-1.012 2.188-0.961 3.84 1.031 0.388 1.998-1.142 3.601-0.96 0.884 1.517 0.381 4.419 2.16 5.04 0.628 3.104-2.561 3.75-4.32 2.4-0.444 0.436-0.312 1.448-0.72 1.92-1.188 0.147-1.536-0.545-2.4-0.721-0.799 1.563 1.617 1.889 0.72 3.601-1.775-0.463-2.337 1.205-3.359 2.16-1.136-0.064-1.352-1.049-2.16-1.44-0.217 0.423-0.884 0.396-0.96 0.96-0.752 0.804 1.801 1.3 0.72 2.4-1.513 2.06-3.329-1.013-5.76 0-0.55-0.57-1.208-1.032-1.44-1.92-2.051 0.131-3.084-0.756-4.319-1.44-3.303-0.538-4.311 1.677-7.44 0.96 0.216 2.23 3.326 2.419 5.28 2.16 2.783 2.896 3.368 7.992 6.72 10.32 0.458-3.125 4.479 6.161 9.12 10.319 3.707-0.149 6.219 0.33 8.16 1.44 0.042 1.242-2.057 0.343-2.64 0.96 1.246 2.751 4.993-0.816 6.96-0.24-0.479 6.364-12.435 7.859-14.881 2.16-6.689-3.79-9.293-11.666-15.119-16.32-2.059-0.502-3.208-1.912-4.801-2.88-5.372 0.134-10.436 0.287-13.92-1.92-2.16 1.263-3.17 4.747-6 5.521-2.923 0.798-5.911-0.139-8.16 1.92-7.446 1.033-14.465 2.494-19.68 5.76-1.237 0.412-2.52-0.162-3.12 0.479 0.48 2.32 1.668 3.934 1.92 6.48-0.519 0.761-0.962 1.598-1.92 1.92 0.095 1.746 2.833 0.848 3.12 2.4-4.069 1.981-6.507-1.59-7.92-3.841 0.508-4.2-0.333-9.392 2.16-11.52 1.205-1.029 2.837-0.545 4.32-1.68 4.366 0.4 8.705-2.869 12.96-3.84 4.858-1.109 9.547-1.108 11.279-5.28-1.414-1.656-3.291-0.841-5.52-1.44-1.111-0.299-1.463-1.133-2.4-1.68-0.562-0.328-1.474-0.334-2.16-0.72-2.196-1.234-3.287-3.257-6.239-3.841-1.489-0.294-2.832-0.085-4.08-0.479-7.656-2.422-10.618-10.302-13.2-18.24-0.314-3.445-0.995-6.524-1.92-9.359-0.827-8.533-7.048-11.673-13.68-14.4-2.024-0.184-3.309 0.372-5.28 0.24-0.977-0.784-2.486-1.034-2.16-3.12 1.78-0.307 3.603-1.558 5.52-0.96 1.04-0.164 1.452-1.567 2.636-2.16 1.045-0.523 3.934-0.583 5.52-1.92 0.24-0.202 4.291-0.067 4.561 0 2.813 0.7 2.876 4.102 5.04 5.76-1.263 4.763 2.796 8.095 3.6 12.24 0.192 0.99-0.095 1.896 0 2.88 0.472 4.913 2.428 11.467 4.8 14.88 0.998 1.438 2.397 2.623 4.078 3.6z"
fill-rule="evenodd"></path>
</svg>
);
}

View File

@ -5,7 +5,9 @@ import { RouteObject, useNavigate } from "react-router-dom";
import BlueWallet from "Icons/BlueWallet";
import ConnectLNC from "Pages/settings/wallet/LNC";
import ConnectLNDHub from "./wallet/LNDHub";
import ConnectLNDHub from "Pages/settings/wallet/LNDHub";
import ConnectNostrWallet from "Pages/settings/wallet/NWC";
import NostrIcon from "Icons/Nostrich";
const WalletSettings = () => {
const navigate = useNavigate();
@ -19,12 +21,14 @@ const WalletSettings = () => {
<img src={LndLogo} width={100} />
<h3 className="f-end">LND with LNC</h3>
</div>
{
<div className="card" onClick={() => navigate("/settings/wallet/lndhub")}>
<BlueWallet width={100} height={100} />
<h3 className="f-end">LNDHub</h3>
</div>
}
<div className="card" onClick={() => navigate("/settings/wallet/lndhub")}>
<BlueWallet width={100} height={100} />
<h3 className="f-end">LNDHub</h3>
</div>
<div className="card" onClick={() => navigate("/settings/wallet/nwc")}>
<NostrIcon width={100} height={100} />
<h3 className="f-end">Nostr Wallet Connect</h3>
</div>
</div>
</>
);
@ -45,4 +49,8 @@ export const WalletSettingsRoutes = [
path: "/settings/wallet/lndhub",
element: <ConnectLNDHub />,
},
{
path: "/settings/wallet/nwc",
element: <ConnectNostrWallet />,
},
] as Array<RouteObject>;

View File

@ -0,0 +1,70 @@
import { useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { v4 as uuid } from "uuid";
import AsyncButton from "Element/AsyncButton";
import { unwrap } from "Util";
import { WalletConfig, WalletKind, Wallets } from "Wallet";
import { useNavigate } from "react-router-dom";
import { NostrConnectWallet } from "Wallet/NostrWalletConnect";
const ConnectNostrWallet = () => {
const navigate = useNavigate();
const { formatMessage } = useIntl();
const [config, setConfig] = useState<string>();
const [error, setError] = useState<string>();
async function tryConnect(config: string) {
try {
const connection = new NostrConnectWallet(config);
await connection.login();
const info = await connection.getInfo();
const newWallet = {
id: uuid(),
kind: WalletKind.NWC,
active: true,
info,
data: config,
} as WalletConfig;
Wallets.add(newWallet);
navigate("/wallet");
} catch (e) {
if (e instanceof Error) {
setError((e as Error).message);
} else {
setError(
formatMessage({
defaultMessage: "Unknown error",
})
);
}
}
}
return (
<>
<h4>
<FormattedMessage defaultMessage="Enter Nostr Wallet Connect config" />
</h4>
<div className="flex">
<div className="f-grow mr10">
<input
type="text"
placeholder="nostr+walletconnect:<pubkey>?relay=<relay>&secret=<secret>"
className="w-max"
value={config}
onChange={e => setConfig(e.target.value)}
/>
</div>
<AsyncButton onClick={() => tryConnect(unwrap(config))} disabled={!config}>
<FormattedMessage defaultMessage="Connect" />
</AsyncButton>
</div>
{error && <b className="error p10">{error}</b>}
</>
);
};
export default ConnectNostrWallet;

View File

@ -1,6 +1,6 @@
import { EventKind, HexKey, NostrPrefix, RawEvent } from "@snort/nostr";
import { HashtagRegex } from "Const";
import { parseNostrLink, unixNow } from "Util";
import { getPublicKey, parseNostrLink, unixNow } from "Util";
import { EventExt } from "./EventExt";
export class EventBuilder {
@ -68,7 +68,7 @@ export class EventBuilder {
* @param pk Private key to sign event with
*/
async buildAndSign(pk: HexKey) {
const ev = this.build();
const ev = this.pubKey(getPublicKey(pk)).build();
await EventExt.sign(ev, pk);
return ev;
}

View File

@ -22,6 +22,10 @@ export const sha256 = (str: string | Uint8Array): u256 => {
return secp.utils.bytesToHex(hash(str));
};
export function getPublicKey(privKey: HexKey) {
return secp.utils.bytesToHex(secp.schnorr.getPublicKey(privKey));
}
export async function openFile(): Promise<File | undefined> {
return new Promise(resolve => {
const elm = document.createElement("input");

View File

@ -0,0 +1,146 @@
import { Connection, RawEvent } from "@snort/nostr";
import { EventBuilder } from "System";
import { EventExt } from "System/EventExt";
import { LNWallet, WalletError, WalletErrorCode, WalletInfo, WalletInvoice } from "Wallet";
interface WalletConnectConfig {
relayUrl: string;
walletPubkey: string;
secret: string;
}
interface QueueObj {
resolve: (o: string) => void;
reject: (e: Error) => void;
}
export class NostrConnectWallet implements LNWallet {
#config: WalletConnectConfig;
#conn?: Connection;
#commandQueue: Map<string, QueueObj>;
constructor(cfg: string) {
this.#config = NostrConnectWallet.parseConfigUrl(cfg);
this.#commandQueue = new Map();
}
static parseConfigUrl(url: string) {
const uri = new URL(url.replace("nostrwalletconnect://", "http://").replace("nostr+walletconnect://", "http://"));
return {
relayUrl: uri.searchParams.get("relay"),
walletPubkey: uri.host,
secret: uri.searchParams.get("secret"),
} as WalletConnectConfig;
}
isReady(): boolean {
return true;
}
async getInfo() {
await this.login();
return await new Promise<WalletInfo>((resolve, reject) => {
this.#commandQueue.set("info", {
resolve: (o: string) => {
resolve({
alias: "NWC",
chains: o.split(" "),
} as WalletInfo);
},
reject,
});
this.#conn?.QueueReq(["REQ", "info", { kinds: [13194], limit: 1 }], () => {
// ignored
});
});
}
async login() {
if (this.#conn) return true;
return await new Promise<boolean>(resolve => {
this.#conn = new Connection(this.#config.relayUrl, { read: true, write: true });
this.#conn.OnConnected = () => resolve(true);
this.#conn.OnEvent = (s, e) => {
this.#onReply(s, e);
};
this.#conn.Connect();
});
}
async close() {
this.#conn?.Close();
return true;
}
async getBalance() {
return 0;
}
createInvoice() {
return Promise.reject(new WalletError(WalletErrorCode.GeneralError, "Not implemented"));
}
async payInvoice(pr: string) {
await this.login();
return await this.#rpc<WalletInvoice>("pay_invoice", {
invoice: pr,
});
}
getInvoices() {
return Promise.resolve([]);
}
async #onReply(sub: string, e: RawEvent) {
if (sub === "info") {
const pending = this.#commandQueue.get("info");
if (!pending) {
throw new WalletError(WalletErrorCode.GeneralError, "No pending info command found");
}
pending.resolve(e.content);
this.#commandQueue.delete("info");
return;
}
if (e.kind !== 23195) {
throw new WalletError(WalletErrorCode.GeneralError, "Unknown event kind");
}
const replyTo = e.tags.find(a => a[0] === "e");
if (!replyTo) {
throw new WalletError(WalletErrorCode.GeneralError, "Missing e-tag in command response");
}
const pending = this.#commandQueue.get(replyTo[1]);
if (!pending) {
throw new WalletError(WalletErrorCode.GeneralError, "No pending command found");
}
const body = JSON.parse(await EventExt.decryptData(e.content, this.#config.secret, this.#config.walletPubkey));
pending.resolve(body);
this.#commandQueue.delete(replyTo[1]);
}
async #rpc<T>(method: string, params: Record<string, string>) {
if (!this.#conn) throw new WalletError(WalletErrorCode.GeneralError, "Not implemented");
const payload = JSON.stringify({
method,
params,
});
const eb = new EventBuilder();
eb.kind(23194)
.content(await EventExt.encryptData(payload, this.#config.walletPubkey, this.#config.secret))
.tag(["p", this.#config.walletPubkey]);
const evCommand = await eb.buildAndSign(this.#config.secret);
await this.#conn.SendAsync(evCommand);
/*return await new Promise<T>((resolve, reject) => {
this.#commandQueue.set(evCommand.id, {
resolve, reject
})
})*/
return {} as T;
}
}

View File

@ -3,12 +3,14 @@ import { useSyncExternalStore } from "react";
import { decodeInvoice, unwrap } from "Util";
import { LNCWallet } from "./LNCWallet";
import LNDHubWallet from "./LNDHub";
import { NostrConnectWallet } from "./NostrWalletConnect";
import { setupWebLNWalletConfig, WebLNWallet } from "./WebLN";
export enum WalletKind {
LNDHub = 1,
LNC = 2,
WebLN = 3,
NWC = 4,
}
export enum WalletErrorCode {
@ -246,6 +248,9 @@ export class WalletStore {
case WalletKind.LNDHub: {
return new LNDHubWallet(unwrap(cfg.data));
}
case WalletKind.NWC: {
return new NostrConnectWallet(unwrap(cfg.data));
}
}
}
}

View File

@ -57,6 +57,9 @@
"1Mo59U": {
"defaultMessage": "Are you sure you want to remove this note from bookmarks?"
},
"1R43+L": {
"defaultMessage": "Enter Nostr Wallet Connect config"
},
"1c4YST": {
"defaultMessage": "Connected to: {node} 🎉"
},
@ -307,15 +310,15 @@
"Ebl/B2": {
"defaultMessage": "Translate to {lang}"
},
"EcZF24": {
"defaultMessage": "Custom Relays"
},
"EcglP9": {
"defaultMessage": "Key"
},
"EnCOBJ": {
"defaultMessage": "Buy"
},
"EqRgFp": {
"defaultMessage": "Some clients will only allow zaps on your notes"
},
"Eqjl5K": {
"defaultMessage": "Only Snort and our integration partner identifier gives you a colorful domain name, but you are welcome to use other services too."
},
@ -724,6 +727,9 @@
"c35bj2": {
"defaultMessage": "If you have an enquiry about your NIP-05 order please DM {link}"
},
"c3g2hL": {
"defaultMessage": "Broadcast Again"
},
"cPIKU2": {
"defaultMessage": "Following"
},
@ -1056,6 +1062,9 @@
"tOdNiY": {
"defaultMessage": "Dark"
},
"th5lxp": {
"defaultMessage": "Send note to a subset of your write relays"
},
"thnRpU": {
"defaultMessage": "Getting NIP-05 verified can help:"
},
@ -1080,9 +1089,6 @@
"ut+2Cd": {
"defaultMessage": "Get a partner identifier"
},
"v48Uwm": {
"defaultMessage": "OnlyZaps"
},
"vOKedj": {
"defaultMessage": "{n,plural,=1{& {n} other} other{& {n} others}}"
},

View File

@ -18,6 +18,7 @@
"0yO7wF": "{n} secs",
"1A7TZk": "What is Snort and how does it work?",
"1Mo59U": "Are you sure you want to remove this note from bookmarks?",
"1R43+L": "Enter Nostr Wallet Connect config",
"1c4YST": "Connected to: {node} 🎉",
"1iQ8GN": "Toggle Preview",
"1nYUGC": "{n} Following",
@ -100,9 +101,9 @@
"EPYwm7": "Your private key is your password. If you lose this key, you will lose access to your account! Copy it and keep it in a safe place. There is no way to reset your private key.",
"EWyQH5": "Global",
"Ebl/B2": "Translate to {lang}",
"EcZF24": "Custom Relays",
"EcglP9": "Key",
"EnCOBJ": "Buy",
"EqRgFp": "Some clients will only allow zaps on your notes",
"Eqjl5K": "Only Snort and our integration partner identifier gives you a colorful domain name, but you are welcome to use other services too.",
"F+B3x1": "We have also partnered with nostrplebs.com to give you more options",
"F3l7xL": "Add Account",
@ -237,6 +238,7 @@
"bxv59V": "Just now",
"c+oiJe": "Install Extension",
"c35bj2": "If you have an enquiry about your NIP-05 order please DM {link}",
"c3g2hL": "Broadcast Again",
"cPIKU2": "Following",
"cQfLWb": "URL..",
"cWx9t8": "Mute all",
@ -346,6 +348,7 @@
"sWnYKw": "Snort is designed to have a similar experience to Twitter.",
"svOoEH": "Name-squatting and impersonation is not allowed. Snort and our partners reserve the right to terminate your handle (not your account - nobody can take that away) for violating this rule.",
"tOdNiY": "Dark",
"th5lxp": "Send note to a subset of your write relays",
"thnRpU": "Getting NIP-05 verified can help:",
"ttxS0b": "Supporter Badge",
"u/vOPu": "Paid",
@ -354,7 +357,6 @@
"uSV4Ti": "Reposts need to be manually confirmed",
"usAvMr": "Edit Profile",
"ut+2Cd": "Get a partner identifier",
"v48Uwm": "OnlyZaps",
"vOKedj": "{n,plural,=1{& {n} other} other{& {n} others}}",
"vU71Ez": "Paying with {wallet}",
"vZ4quW": "NIP-05 is a DNS based verification spec which helps to validate you as a real user.",
@ -388,4 +390,4 @@
"zjJZBd": "You're ready!",
"zonsdq": "Failed to load LNURL service",
"zvCDao": "Automatically show latest notes"
}
}