mirror of
https://github.com/believethehype/nostrdvm.git
synced 2025-11-18 22:06:48 +01:00
Initial support for amber
This commit is contained in:
@@ -5,15 +5,18 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "run-p type-check \"build-only {@}\" --",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"type-check": "vue-tsc --build --force"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rust-nostr/nostr-sdk": "^0.10.0",
|
"@rust-nostr/nostr-sdk": "^0.10.0",
|
||||||
"@vueuse/core": "^10.7.2",
|
"@vueuse/core": "^10.7.2",
|
||||||
|
"bech32": "^2.0.0",
|
||||||
"bootstrap": "^5.3.2",
|
"bootstrap": "^5.3.2",
|
||||||
"daisyui": "^4.6.0",
|
"daisyui": "^4.6.0",
|
||||||
"mini-toastr": "^0.8.1",
|
"mini-toastr": "^0.8.1",
|
||||||
|
"nostr-tools": "^1.17.0",
|
||||||
"vue": "^3.4.15",
|
"vue": "^3.4.15",
|
||||||
"vue-notifications": "^1.0.2",
|
"vue-notifications": "^1.0.2",
|
||||||
"vue3-easy-data-table": "^1.5.47",
|
"vue3-easy-data-table": "^1.5.47",
|
||||||
@@ -26,8 +29,10 @@
|
|||||||
"postcss": "^8.4.33",
|
"postcss": "^8.4.33",
|
||||||
"sass": "^1.70.0",
|
"sass": "^1.70.0",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
|
"typescript": "~5.3.0",
|
||||||
"vite": "^5.0.10",
|
"vite": "^5.0.10",
|
||||||
"vue-router": "^4.2.5"
|
"vue-router": "^4.2.5",
|
||||||
|
"vue-tsc": "^1.8.27"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rollup/rollup-linux-x64-gnu": "4.6.1"
|
"@rollup/rollup-linux-x64-gnu": "4.6.1"
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export default {
|
|||||||
components: {Donate, Nip07, ResultsTable, Search}
|
components: {Donate, Nip07, ResultsTable, Search}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -34,6 +34,9 @@
|
|||||||
<h3 className="card-title">Nip07 Login</h3>
|
<h3 className="card-title">Nip07 Login</h3>
|
||||||
<p>Use a Browser Nip07 Extension like getalby or nos2x to login</p>
|
<p>Use a Browser Nip07 Extension like getalby or nos2x to login</p>
|
||||||
<button className="btn" @click="sign_in_nip07()">Browser Extension</button>
|
<button className="btn" @click="sign_in_nip07()">Browser Extension</button>
|
||||||
|
<template v-if="supports_android_signer">
|
||||||
|
<button className="btn" @click="sign_in_amber()">Amber Sign in</button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -51,13 +54,14 @@ import {
|
|||||||
Filter,
|
Filter,
|
||||||
initLogger,
|
initLogger,
|
||||||
LogLevel,
|
LogLevel,
|
||||||
Timestamp, Keys, NostrDatabase, ClientBuilder, ClientZapper, Alphabet, SingleLetterTag, Options, Duration
|
Timestamp, Keys, NostrDatabase, ClientBuilder, ClientZapper, Alphabet, SingleLetterTag, Options, Duration, PublicKey
|
||||||
} from "@rust-nostr/nostr-sdk";
|
} from "@rust-nostr/nostr-sdk";
|
||||||
import VueNotifications from "vue-notifications";
|
import VueNotifications from "vue-notifications";
|
||||||
import store from '../store';
|
import store from '../store';
|
||||||
import Nip89 from "@/components/Nip89.vue";
|
import Nip89 from "@/components/Nip89.vue";
|
||||||
import miniToastr from "mini-toastr";
|
import miniToastr from "mini-toastr";
|
||||||
import deadnip89s from "@/components/data/deadnip89s.json";
|
import deadnip89s from "@/components/data/deadnip89s.json";
|
||||||
|
import amberSignerService from "./android-signer/AndroidSigner";
|
||||||
import {useDark, useToggle} from "@vueuse/core";
|
import {useDark, useToggle} from "@vueuse/core";
|
||||||
const isDark = useDark();
|
const isDark = useDark();
|
||||||
//const toggleDark = useToggle(isDark);
|
//const toggleDark = useToggle(isDark);
|
||||||
@@ -72,12 +76,16 @@ export default {
|
|||||||
current_user: "",
|
current_user: "",
|
||||||
avatar: "",
|
avatar: "",
|
||||||
signer: "",
|
signer: "",
|
||||||
|
supports_android_signer: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
try{
|
try{
|
||||||
|
if (amberSignerService.supported) {
|
||||||
|
this.supports_android_signer = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (localStorage.getItem('nostr-key-method') === 'nip07')
|
if (localStorage.getItem('nostr-key-method') === 'nip07')
|
||||||
{
|
{
|
||||||
await this.sign_in_nip07()
|
await this.sign_in_nip07()
|
||||||
@@ -214,6 +222,47 @@ export default {
|
|||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async sign_in_amber() {
|
||||||
|
try {
|
||||||
|
|
||||||
|
await loadWasmAsync();
|
||||||
|
|
||||||
|
if(logger){
|
||||||
|
try {
|
||||||
|
initLogger(LogLevel.debug());
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!amberSignerService.supported) {
|
||||||
|
alert("android signer not supported")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hexKey = await amberSignerService.getPublicKey();
|
||||||
|
let publicKey = PublicKey.fromHex(hexKey);
|
||||||
|
let keys = Keys.fromPublicKey(publicKey)
|
||||||
|
this.signer = ClientSigner.keys(keys)
|
||||||
|
let opts = new Options().waitForSend(false).connectionTimeout(Duration.fromSecs(5));
|
||||||
|
let client = new ClientBuilder().signer(this.signer).opts(opts).build()
|
||||||
|
for (const relay of store.state.relays){
|
||||||
|
await client.addRelay(relay);
|
||||||
|
}
|
||||||
|
await client.connect();
|
||||||
|
store.commit('set_client', client)
|
||||||
|
store.commit('set_pubkey', publicKey)
|
||||||
|
store.commit('set_hasEventListener', false)
|
||||||
|
localStorage.setItem('nostr-key-method', "android-signer")
|
||||||
|
localStorage.setItem('nostr-key', "")
|
||||||
|
await this.get_user_info(publicKey)
|
||||||
|
|
||||||
|
miniToastr.showMessage("Login successful!", "Logged in as " + publicKey.toHex(), VueNotifications.types.success)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
alert(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
async getnip89s(){
|
async getnip89s(){
|
||||||
|
|
||||||
//let keys = Keys.generate()
|
//let keys = Keys.generate()
|
||||||
|
|||||||
106
ui/noogle/src/components/android-signer/AndroidSigner.ts
Normal file
106
ui/noogle/src/components/android-signer/AndroidSigner.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import { getEventHash, nip19, verifySignature } from "nostr-tools";
|
||||||
|
import createDefer, { Deferred } from "./classes/deffered";
|
||||||
|
import { getPubkeyFromDecodeResult, isHex, isHexKey } from "./helpers/nip19";
|
||||||
|
import { DraftNostrEvent, NostrEvent } from "./types/nostr-event";
|
||||||
|
|
||||||
|
export function createGetPublicKeyIntent() {
|
||||||
|
return `intent:#Intent;scheme=nostrsigner;S.compressionType=none;S.returnType=signature;S.type=get_public_key;end`;
|
||||||
|
}
|
||||||
|
export function createSignEventIntent(draft: DraftNostrEvent) {
|
||||||
|
return `intent:${encodeURIComponent(
|
||||||
|
JSON.stringify(draft),
|
||||||
|
)}#Intent;scheme=nostrsigner;S.compressionType=none;S.returnType=signature;S.type=sign_event;end`;
|
||||||
|
}
|
||||||
|
export function createNip04EncryptIntent(pubkey: string, plainText: string) {
|
||||||
|
return `intent:${encodeURIComponent(
|
||||||
|
plainText,
|
||||||
|
)}#Intent;scheme=nostrsigner;S.pubKey=${pubkey};S.compressionType=none;S.returnType=signature;S.type=nip04_encrypt;end`;
|
||||||
|
}
|
||||||
|
export function createNip04DecryptIntent(pubkey: string, data: string) {
|
||||||
|
return `intent:${encodeURIComponent(
|
||||||
|
data,
|
||||||
|
)}#Intent;scheme=nostrsigner;S.pubKey=${pubkey};S.compressionType=none;S.returnType=signature;S.type=nip04_decrypt;end`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pendingRequest: Deferred<string> | null = null;
|
||||||
|
|
||||||
|
function rejectPending() {
|
||||||
|
if (pendingRequest) {
|
||||||
|
pendingRequest.reject("Canceled");
|
||||||
|
pendingRequest = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onVisibilityChange() {
|
||||||
|
if (document.visibilityState === "visible") {
|
||||||
|
if (!pendingRequest || !navigator.clipboard) return;
|
||||||
|
|
||||||
|
// read the result from the clipboard
|
||||||
|
setTimeout(() => {
|
||||||
|
navigator.clipboard
|
||||||
|
.readText()
|
||||||
|
.then((result) => pendingRequest?.resolve(result))
|
||||||
|
.catch((e) => pendingRequest?.reject(e));
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener("visibilitychange", onVisibilityChange);
|
||||||
|
|
||||||
|
async function intentRequest(intent: string) {
|
||||||
|
rejectPending();
|
||||||
|
const request = createDefer<string>();
|
||||||
|
window.open(intent, "_blank");
|
||||||
|
// NOTE: wait 500ms before setting the pending request since the visibilitychange event fires as soon as window.open is called
|
||||||
|
setTimeout(() => {
|
||||||
|
pendingRequest = request;
|
||||||
|
}, 500);
|
||||||
|
const result = await request;
|
||||||
|
if (result.length === 0) throw new Error("Empty clipboard");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPublicKey() {
|
||||||
|
const result = await intentRequest(createGetPublicKeyIntent());
|
||||||
|
if (isHexKey(result)) return result;
|
||||||
|
else if (result.startsWith("npub") || result.startsWith("nprofile")) {
|
||||||
|
const decode = nip19.decode(result);
|
||||||
|
const pubkey = getPubkeyFromDecodeResult(decode);
|
||||||
|
if (!pubkey) throw new Error("Expected npub from clipboard");
|
||||||
|
return pubkey;
|
||||||
|
}
|
||||||
|
throw new Error("Expected clipboard to have pubkey");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signEvent(draft: DraftNostrEvent & { pubkey: string }): Promise<NostrEvent> {
|
||||||
|
const draftWithId = { ...draft, id: draft.id || getEventHash(draft) };
|
||||||
|
const sig = await intentRequest(createSignEventIntent(draftWithId));
|
||||||
|
if (!isHex(sig)) throw new Error("Expected hex signature");
|
||||||
|
|
||||||
|
const event: NostrEvent = { ...draftWithId, sig };
|
||||||
|
if (!verifySignature(event)) throw new Error("Invalid signature");
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function nip04Encrypt(pubkey: string, plaintext: string): Promise<string> {
|
||||||
|
const data = await intentRequest(createNip04EncryptIntent(pubkey, plaintext));
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
async function nip04Decrypt(pubkey: string, data: string): Promise<string> {
|
||||||
|
const plaintext = await intentRequest(createNip04DecryptIntent(pubkey, data));
|
||||||
|
return plaintext;
|
||||||
|
}
|
||||||
|
|
||||||
|
const amberSignerService = {
|
||||||
|
supported: navigator.userAgent.includes("Android") && navigator.clipboard,
|
||||||
|
getPublicKey,
|
||||||
|
signEvent,
|
||||||
|
nip04Encrypt,
|
||||||
|
nip04Decrypt,
|
||||||
|
};
|
||||||
|
|
||||||
|
// if (import.meta.env.DEV) {
|
||||||
|
// // @ts-ignore
|
||||||
|
// window.amberSignerService = amberSignerService;
|
||||||
|
// }
|
||||||
|
|
||||||
|
export default amberSignerService;
|
||||||
21
ui/noogle/src/components/android-signer/classes/deffered.ts
Normal file
21
ui/noogle/src/components/android-signer/classes/deffered.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
export type Deferred<T> = Promise<T> & {
|
||||||
|
resolve: (value?: T | PromiseLike<T>) => void;
|
||||||
|
reject: (reason?: any) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function createDefer<T>() {
|
||||||
|
let _resolve: (value?: T | PromiseLike<T>) => void;
|
||||||
|
let _reject: (reason?: any) => void;
|
||||||
|
const promise = new Promise<T>((resolve, reject) => {
|
||||||
|
// @ts-ignore
|
||||||
|
_resolve = resolve;
|
||||||
|
_reject = reject;
|
||||||
|
}) as Deferred<T>;
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
promise.resolve = _resolve;
|
||||||
|
// @ts-ignore
|
||||||
|
promise.reject = _reject;
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
23
ui/noogle/src/components/android-signer/helpers/nip19.ts
Normal file
23
ui/noogle/src/components/android-signer/helpers/nip19.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { getPublicKey, nip19 } from "nostr-tools";
|
||||||
|
|
||||||
|
export function isHex(str?: string) {
|
||||||
|
if (str?.match(/^[0-9a-f]+$/i)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
export function isHexKey(key?: string) {
|
||||||
|
if (key?.toLowerCase()?.match(/^[0-9a-f]{64}$/)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPubkeyFromDecodeResult(result?: nip19.DecodeResult) {
|
||||||
|
if (!result) return;
|
||||||
|
switch (result.type) {
|
||||||
|
case "naddr":
|
||||||
|
case "nprofile":
|
||||||
|
return result.data.pubkey;
|
||||||
|
case "npub":
|
||||||
|
return result.data;
|
||||||
|
case "nsec":
|
||||||
|
return getPublicKey(result.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
54
ui/noogle/src/components/android-signer/types/nostr-event.ts
Normal file
54
ui/noogle/src/components/android-signer/types/nostr-event.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
export type ETag = ["e", string] | ["e", string, string] | ["e", string, string, string];
|
||||||
|
export type ATag = ["a", string] | ["a", string, string];
|
||||||
|
export type PTag = ["p", string] | ["p", string, string] | ["p", string, string, string];
|
||||||
|
export type RTag = ["r", string] | ["r", string, string];
|
||||||
|
export type DTag = ["d"] | ["d", string];
|
||||||
|
export type EmojiTag = ["emoji", string, string];
|
||||||
|
export type Tag = string[] | ETag | PTag | RTag | DTag | ATag;
|
||||||
|
|
||||||
|
export type NostrEvent = {
|
||||||
|
id: string;
|
||||||
|
pubkey: string;
|
||||||
|
created_at: number;
|
||||||
|
kind: number;
|
||||||
|
tags: Tag[];
|
||||||
|
content: string;
|
||||||
|
sig: string;
|
||||||
|
};
|
||||||
|
export type CountResponse = {
|
||||||
|
count: number;
|
||||||
|
approximate?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DraftNostrEvent = Omit<NostrEvent, "pubkey" | "id" | "sig"> & { pubkey?: string; id?: string };
|
||||||
|
|
||||||
|
export type RawIncomingEvent = ["EVENT", string, NostrEvent];
|
||||||
|
export type RawIncomingNotice = ["NOTICE", string];
|
||||||
|
export type RawIncomingCount = ["COUNT", string, CountResponse];
|
||||||
|
export type RawIncomingEOSE = ["EOSE", string];
|
||||||
|
export type RawIncomingCommandResult = ["OK", string, boolean, string];
|
||||||
|
export type RawIncomingNostrEvent =
|
||||||
|
| RawIncomingEvent
|
||||||
|
| RawIncomingNotice
|
||||||
|
| RawIncomingCount
|
||||||
|
| RawIncomingEOSE
|
||||||
|
| RawIncomingCommandResult;
|
||||||
|
|
||||||
|
export function isETag(tag: Tag): tag is ETag {
|
||||||
|
return tag[0] === "e" && tag[1] !== undefined;
|
||||||
|
}
|
||||||
|
export function isPTag(tag: Tag): tag is PTag {
|
||||||
|
return tag[0] === "p" && tag[1] !== undefined;
|
||||||
|
}
|
||||||
|
export function isRTag(tag: Tag): tag is RTag {
|
||||||
|
return tag[0] === "r" && tag[1] !== undefined;
|
||||||
|
}
|
||||||
|
export function isDTag(tag: Tag): tag is DTag {
|
||||||
|
return tag[0] === "d";
|
||||||
|
}
|
||||||
|
export function isATag(tag: Tag): tag is ATag {
|
||||||
|
return tag[0] === "a" && tag[1] !== undefined;
|
||||||
|
}
|
||||||
|
export function isEmojiTag(tag: Tag): tag is EmojiTag {
|
||||||
|
return tag[0] === "emoji" && tag[1] !== undefined && tag[2] !== undefined;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user