mirror of
https://github.com/believethehype/nostrdvm.git
synced 2025-09-28 21:53:33 +02:00
Initial support for amber
This commit is contained in:
@@ -5,15 +5,18 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"preview": "vite preview",
|
||||
"type-check": "vue-tsc --build --force"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rust-nostr/nostr-sdk": "^0.10.0",
|
||||
"@vueuse/core": "^10.7.2",
|
||||
"bech32": "^2.0.0",
|
||||
"bootstrap": "^5.3.2",
|
||||
"daisyui": "^4.6.0",
|
||||
"mini-toastr": "^0.8.1",
|
||||
"nostr-tools": "^1.17.0",
|
||||
"vue": "^3.4.15",
|
||||
"vue-notifications": "^1.0.2",
|
||||
"vue3-easy-data-table": "^1.5.47",
|
||||
@@ -26,8 +29,10 @@
|
||||
"postcss": "^8.4.33",
|
||||
"sass": "^1.70.0",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "~5.3.0",
|
||||
"vite": "^5.0.10",
|
||||
"vue-router": "^4.2.5"
|
||||
"vue-router": "^4.2.5",
|
||||
"vue-tsc": "^1.8.27"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-linux-x64-gnu": "4.6.1"
|
||||
|
@@ -9,7 +9,6 @@ export default {
|
||||
components: {Donate, Nip07, ResultsTable, Search}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@@ -34,6 +34,9 @@
|
||||
<h3 className="card-title">Nip07 Login</h3>
|
||||
<p>Use a Browser Nip07 Extension like getalby or nos2x to login</p>
|
||||
<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>
|
||||
@@ -51,13 +54,14 @@ import {
|
||||
Filter,
|
||||
initLogger,
|
||||
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";
|
||||
import VueNotifications from "vue-notifications";
|
||||
import store from '../store';
|
||||
import Nip89 from "@/components/Nip89.vue";
|
||||
import miniToastr from "mini-toastr";
|
||||
import deadnip89s from "@/components/data/deadnip89s.json";
|
||||
import amberSignerService from "./android-signer/AndroidSigner";
|
||||
import {useDark, useToggle} from "@vueuse/core";
|
||||
const isDark = useDark();
|
||||
//const toggleDark = useToggle(isDark);
|
||||
@@ -72,12 +76,16 @@ export default {
|
||||
current_user: "",
|
||||
avatar: "",
|
||||
signer: "",
|
||||
|
||||
supports_android_signer: false,
|
||||
};
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
try{
|
||||
if (amberSignerService.supported) {
|
||||
this.supports_android_signer = true;
|
||||
}
|
||||
|
||||
if (localStorage.getItem('nostr-key-method') === 'nip07')
|
||||
{
|
||||
await this.sign_in_nip07()
|
||||
@@ -214,6 +222,47 @@ export default {
|
||||
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(){
|
||||
|
||||
//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