feat: nip55

This commit is contained in:
kieran 2024-12-06 11:59:15 +00:00
parent 892b00810d
commit 5338f3acab
No known key found for this signature in database
GPG Key ID: DE71CEB3925BE941
13 changed files with 141 additions and 17 deletions

View File

@ -38,6 +38,7 @@ Snort supports the following NIP's:
- [x] NIP-50: Search - [x] NIP-50: Search
- [x] NIP-51: Lists - [x] NIP-51: Lists
- [x] NIP-53: Live Events - [x] NIP-53: Live Events
- [x] NIP-55: Android signer application
- [x] NIP-57: Zaps - [x] NIP-57: Zaps
- [x] NIP-58: Badges - [x] NIP-58: Badges
- [x] NIP-59: Gift Wrap - [x] NIP-59: Gift Wrap

View File

@ -105,6 +105,7 @@
"@types/webtorrent": "^0.109.3", "@types/webtorrent": "^0.109.3",
"@typescript-eslint/eslint-plugin": "^6.1.0", "@typescript-eslint/eslint-plugin": "^6.1.0",
"@typescript-eslint/parser": "^6.1.0", "@typescript-eslint/parser": "^6.1.0",
"@vitejs/plugin-basic-ssl": "^1.2.0",
"@vitejs/plugin-react": "^4.2.0", "@vitejs/plugin-react": "^4.2.0",
"@webbtc/webln-types": "^3.0.0", "@webbtc/webln-types": "^3.0.0",
"@webscopeio/react-textarea-autocomplete": "^4.9.2", "@webscopeio/react-textarea-autocomplete": "^4.9.2",

View File

@ -1,5 +1,4 @@
import { unwrap } from "@snort/shared"; import { Nip7Signer, Nip55Signer, NotEncrypted } from "@snort/system";
import { NotEncrypted } from "@snort/system";
import { SnortContext } from "@snort/system-react"; import { SnortContext } from "@snort/system-react";
import classNames from "classnames"; import classNames from "classnames";
import { FormEvent, useContext, useState } from "react"; import { FormEvent, useContext, useState } from "react";
@ -16,6 +15,8 @@ import { NewUserState } from ".";
const NSEC_NPUB_REGEX = /(nsec1|npub1)[a-zA-Z0-9]{20,65}/gi; const NSEC_NPUB_REGEX = /(nsec1|npub1)[a-zA-Z0-9]{20,65}/gi;
const signer = new Nip55Signer();
export function SignIn() { export function SignIn() {
const navigate = useNavigate(); const navigate = useNavigate();
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
@ -25,15 +26,23 @@ export function SignIn() {
const loginHandler = useLoginHandler(); const loginHandler = useLoginHandler();
const hasNip7 = "nostr" in window; const hasNip7 = "nostr" in window;
const hasNip55 = true;
async function doNip07Login() { async function doNip07Login() {
/*const relays = const signer = new Nip7Signer();
"getRelays" in unwrap(window.nostr) ? await unwrap(window.nostr?.getRelays).call(window.nostr) : undefined;*/ const pubKey = await signer.getPubKey();
const pubKey = await unwrap(window.nostr).getPublicKey();
LoginStore.loginWithPubkey(pubKey, LoginSessionType.Nip7); LoginStore.loginWithPubkey(pubKey, LoginSessionType.Nip7);
trackEvent("Login", { type: "NIP7" }); trackEvent("Login", { type: "NIP7" });
navigate("/"); navigate("/");
} }
async function doNip55Login() {
const pubKey = await signer.getPubKey();
LoginStore.loginWithPubkey(pubKey, LoginSessionType.Nip55);
trackEvent("Login", { type: "NIP55" });
navigate("/");
}
async function onSubmit(e) { async function onSubmit(e) {
e.preventDefault(); e.preventDefault();
doLogin(key); doLogin(key);
@ -69,7 +78,7 @@ export function SignIn() {
} }
}; };
const nip7Login = hasNip7 && !useKey; const signerExtLogin = (hasNip7 || hasNip55) && !useKey;
return ( return (
<div className="flex flex-col g24"> <div className="flex flex-col g24">
<img src={CONFIG.icon} width={48} height={48} className="br mr-auto ml-auto" /> <img src={CONFIG.icon} width={48} height={48} className="br mr-auto ml-auto" />
@ -77,10 +86,10 @@ export function SignIn() {
<h1> <h1>
<FormattedMessage defaultMessage="Sign In" /> <FormattedMessage defaultMessage="Sign In" />
</h1> </h1>
{nip7Login && <FormattedMessage defaultMessage="Use a nostr signer extension to sign in" />} {signerExtLogin && <FormattedMessage defaultMessage="Use a nostr signer extension to sign in" />}
</div> </div>
<div className={classNames("flex flex-col g16", { "items-center": nip7Login })}> <div className={classNames("flex flex-col g16", { "items-center": signerExtLogin })}>
{hasNip7 && !useKey && ( {signerExtLogin && (
<> <>
<AsyncButton onClick={doNip07Login}> <AsyncButton onClick={doNip07Login}>
<div className="circle bg-warning p12 text-white"> <div className="circle bg-warning p12 text-white">
@ -88,6 +97,12 @@ export function SignIn() {
</div> </div>
<FormattedMessage defaultMessage="Sign in with Nostr Extension" /> <FormattedMessage defaultMessage="Sign in with Nostr Extension" />
</AsyncButton> </AsyncButton>
<AsyncButton onClick={doNip55Login}>
<div className="circle bg-warning p12 text-white">
<Icon name="key" />
</div>
<FormattedMessage defaultMessage="Sign in with Android signer" />
</AsyncButton>
<Link to="" className="highlight"> <Link to="" className="highlight">
<FormattedMessage defaultMessage="Supported Extensions" /> <FormattedMessage defaultMessage="Supported Extensions" />
</Link> </Link>
@ -96,13 +111,12 @@ export function SignIn() {
</AsyncButton> </AsyncButton>
</> </>
)} )}
{(!hasNip7 || useKey) && ( {(!signerExtLogin || useKey) && (
<form onSubmit={onSubmit} className="flex flex-col gap-4"> <form onSubmit={onSubmit} className="flex flex-col gap-4">
<input <input
type="text" type="text"
placeholder={formatMessage({ placeholder={formatMessage({
defaultMessage: "nsec, npub, nip-05, hex, mnemonic", defaultMessage: "nsec, npub, nip-05, hex, mnemonic",
id: "X7xU8J",
})} })}
value={key} value={key}
onChange={onChange} onChange={onChange}

View File

@ -7,6 +7,7 @@ import {
KeyStorage, KeyStorage,
Nip7Signer, Nip7Signer,
Nip46Signer, Nip46Signer,
Nip55Signer,
PrivateKeySigner, PrivateKeySigner,
RelaySettings, RelaySettings,
SystemInterface, SystemInterface,
@ -157,5 +158,8 @@ export function createPublisher(l: LoginSession) {
case LoginSessionType.Nip7: { case LoginSessionType.Nip7: {
return new EventPublisher(new Nip7Signer(), unwrap(l.publicKey)); return new EventPublisher(new Nip7Signer(), unwrap(l.publicKey));
} }
case LoginSessionType.Nip55: {
return new EventPublisher(new Nip55Signer(), unwrap(l.publicKey));
}
} }
} }

View File

@ -18,6 +18,7 @@ export const enum LoginSessionType {
Nip7 = "nip7", Nip7 = "nip7",
Nip46 = "nip46", Nip46 = "nip46",
Nip7os = "nip7_os", Nip7os = "nip7_os",
Nip55 = "nip55",
} }
export interface SnortAppData { export interface SnortAppData {

View File

@ -876,6 +876,9 @@
"J2Q92B": { "J2Q92B": {
"defaultMessage": "Emoji sets" "defaultMessage": "Emoji sets"
}, },
"J6N9xl": {
"defaultMessage": "Sign in with Android signer"
},
"JCIgkj": { "JCIgkj": {
"defaultMessage": "Username" "defaultMessage": "Username"
}, },

View File

@ -290,6 +290,7 @@
"J1iLmb": "Notifications Not Allowed", "J1iLmb": "Notifications Not Allowed",
"J2HeQ+": "Use commas to separate words e.g. word1, word2, word3", "J2HeQ+": "Use commas to separate words e.g. word1, word2, word3",
"J2Q92B": "Emoji sets", "J2Q92B": "Emoji sets",
"J6N9xl": "Sign in with Android signer",
"JCIgkj": "Username", "JCIgkj": "Username",
"JGrt9q": "Send sats to {name}", "JGrt9q": "Send sats to {name}",
"JHEHCk": "Zaps ({n})", "JHEHCk": "Zaps ({n})",

View File

@ -1,3 +1,4 @@
import basicSsl from "@vitejs/plugin-basic-ssl";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import appConfig from "config"; import appConfig from "config";
import { visualizer } from "rollup-plugin-visualizer"; import { visualizer } from "rollup-plugin-visualizer";
@ -7,6 +8,7 @@ import { vitePluginVersionMark } from "vite-plugin-version-mark";
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
basicSsl(),
react({ react({
jsxImportSource: "@welldone-software/why-did-you-render", jsxImportSource: "@welldone-software/why-did-you-render",
babel: { babel: {

View File

@ -1,6 +1,6 @@
{ {
"name": "@snort/system-react", "name": "@snort/system-react",
"version": "1.5.7", "version": "1.6.0",
"description": "React hooks for @snort/system", "description": "React hooks for @snort/system",
"main": "dist/index.js", "main": "dist/index.js",
"module": "src/index.ts", "module": "src/index.ts",
@ -17,7 +17,7 @@
], ],
"dependencies": { "dependencies": {
"@snort/shared": "^1.0.17", "@snort/shared": "^1.0.17",
"@snort/system": "^1.5.7", "@snort/system": "^1.6.0",
"react": "^18.2.0" "react": "^18.2.0"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@snort/system", "name": "@snort/system",
"version": "1.5.7", "version": "1.6.0",
"description": "Snort nostr system package", "description": "Snort nostr system package",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",

View File

@ -0,0 +1,86 @@
import debug from "debug";
import { NostrEvent, NotSignedNostrEvent } from "../nostr";
import { EventSigner } from "../signer";
import { v4 as uuid } from "uuid";
import { bech32ToHex } from "@snort/shared";
export class Nip55Signer implements EventSigner {
#log = debug("NIP-55");
#queue: Array<{ id: string; resolve: (o: any) => void; reject: () => void }> = [];
init(): Promise<void> {
// nothing here
return Promise.resolve();
}
async getPubKey() {
let pk = await this.#call("get_public_key", "signature");
if (pk.startsWith("npub")) {
pk = bech32ToHex(pk);
}
return pk;
}
async nip4Encrypt(content: string, key: string) {
return await this.#call("nip04_encrypt", "signature", content, new Map([["pubkey", key]]));
}
async nip4Decrypt(content: string, otherKey: string) {
return await this.#call("nip04_decrypt", "signature", content, new Map([["pubkey", otherKey]]));
}
async nip44Encrypt(content: string, key: string) {
return await this.#call("nip44_encrypt", "signature", content, new Map([["pubkey", key]]));
}
async nip44Decrypt(content: string, otherKey: string) {
return await this.#call("nip44_decrypt", "signature", content, new Map([["pubkey", otherKey]]));
}
async sign(ev: NostrEvent | NotSignedNostrEvent) {
const evRet = await this.#call("sign_event", "event", ev);
return JSON.parse(evRet);
}
get supports(): string[] {
return ["nip04", "nip44"];
}
#call(
method: string,
returnType: string,
obj?: NostrEvent | NotSignedNostrEvent | string,
otherParams?: Map<string, string>,
) {
const id = uuid();
const objString = typeof obj === "string" ? obj : obj != undefined ? JSON.stringify(obj) : undefined;
const params = new URLSearchParams();
params.append("compressionType", "none");
params.append("returnType", returnType);
params.append("type", method);
if (otherParams) {
for (const [k, v] of otherParams) {
params.append(k, v);
}
}
return new Promise<string>((resolve, reject) => {
const t = setInterval(async () => {
if (document.hasFocus()) {
const text = await navigator.clipboard.readText();
if (text) {
this.#log("Response: %s", text);
await navigator.clipboard.writeText("");
resolve(text);
clearInterval(t);
}
}
}, 500);
const dst = `nostrsigner:${objString ?? ""}?${params.toString()}`;
this.#log("Sending command %s, %s", id, dst);
globalThis.location.href = dst;
});
}
}

View File

@ -33,6 +33,7 @@ export * from "./impl/nip10";
export * from "./impl/nip44"; export * from "./impl/nip44";
export * from "./impl/nip46"; export * from "./impl/nip46";
export * from "./impl/nip57"; export * from "./impl/nip57";
export * from "./impl/nip55";
export * from "./cache/index"; export * from "./cache/index";
export * from "./cache/user-relays"; export * from "./cache/user-relays";

View File

@ -4724,6 +4724,7 @@ __metadata:
"@typescript-eslint/eslint-plugin": "npm:^6.1.0" "@typescript-eslint/eslint-plugin": "npm:^6.1.0"
"@typescript-eslint/parser": "npm:^6.1.0" "@typescript-eslint/parser": "npm:^6.1.0"
"@uidotdev/usehooks": "npm:^2.4.1" "@uidotdev/usehooks": "npm:^2.4.1"
"@vitejs/plugin-basic-ssl": "npm:^1.2.0"
"@vitejs/plugin-react": "npm:^4.2.0" "@vitejs/plugin-react": "npm:^4.2.0"
"@void-cat/api": "npm:^1.0.12" "@void-cat/api": "npm:^1.0.12"
"@webbtc/webln-types": "npm:^3.0.0" "@webbtc/webln-types": "npm:^3.0.0"
@ -4823,7 +4824,7 @@ __metadata:
resolution: "@snort/system-react@workspace:packages/system-react" resolution: "@snort/system-react@workspace:packages/system-react"
dependencies: dependencies:
"@snort/shared": "npm:^1.0.17" "@snort/shared": "npm:^1.0.17"
"@snort/system": "npm:^1.5.6" "@snort/system": "npm:^1.5.7"
"@types/react": "npm:^18.2.14" "@types/react": "npm:^18.2.14"
react: "npm:^18.2.0" react: "npm:^18.2.0"
typescript: "npm:^5.2.2" typescript: "npm:^5.2.2"
@ -4858,7 +4859,7 @@ __metadata:
languageName: unknown languageName: unknown
linkType: soft linkType: soft
"@snort/system@npm:^1.0.21, @snort/system@npm:^1.2.11, @snort/system@npm:^1.5.2, @snort/system@npm:^1.5.6, @snort/system@workspace:*, @snort/system@workspace:packages/system": "@snort/system@npm:^1.0.21, @snort/system@npm:^1.2.11, @snort/system@npm:^1.5.2, @snort/system@npm:^1.5.7, @snort/system@workspace:*, @snort/system@workspace:packages/system":
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@snort/system@workspace:packages/system" resolution: "@snort/system@workspace:packages/system"
dependencies: dependencies:
@ -4898,7 +4899,7 @@ __metadata:
"@lightninglabs/lnc-web": "npm:^0.3.1-alpha" "@lightninglabs/lnc-web": "npm:^0.3.1-alpha"
"@scure/base": "npm:^1.1.6" "@scure/base": "npm:^1.1.6"
"@snort/shared": "npm:^1.0.17" "@snort/shared": "npm:^1.0.17"
"@snort/system": "npm:^1.5.6" "@snort/system": "npm:^1.5.7"
"@types/debug": "npm:^4.1.12" "@types/debug": "npm:^4.1.12"
"@webbtc/webln-types": "npm:^3.0.0" "@webbtc/webln-types": "npm:^3.0.0"
debug: "npm:^4.3.4" debug: "npm:^4.3.4"
@ -6011,6 +6012,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@vitejs/plugin-basic-ssl@npm:^1.2.0":
version: 1.2.0
resolution: "@vitejs/plugin-basic-ssl@npm:1.2.0"
peerDependencies:
vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0
checksum: 10/34def4ab7838901f1f51bbff21fefac5f56346331f4ff1a8a3059d68e4cd43b62e1581a5ad39971d5d908343ee3217e782a0b506d97e0653c3373e27757ecedf
languageName: node
linkType: hard
"@vitejs/plugin-react@npm:^4.2.0": "@vitejs/plugin-react@npm:^4.2.0":
version: 4.2.0 version: 4.2.0
resolution: "@vitejs/plugin-react@npm:4.2.0" resolution: "@vitejs/plugin-react@npm:4.2.0"