Merge pull request #20 from greenart7c3/main

Support for Amber
This commit is contained in:
believethehype
2024-01-31 19:08:09 +01:00
committed by GitHub
9 changed files with 299 additions and 22 deletions

View File

@@ -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"

View File

@@ -9,7 +9,6 @@ export default {
components: {Donate, Nip07, ResultsTable, Search}
}
</script>
<template>

View File

@@ -23,6 +23,7 @@ import deadnip89s from "@/components/data/deadnip89s.json";
import {data} from "autoprefixer";
import {requestProvider} from "webln";
import Newnote from "@/components/Newnote.vue";
import amberSignerService from "./android-signer/AndroidSigner";
let dvms =[]
let searching = false
@@ -41,8 +42,19 @@ const sleep = (ms) => {
async function post_note(note){
let client = store.state.client
await client.publishTextNote(note, []);
if (localStorage.getItem('nostr-key-method') === 'android-signer') {
const draft = {
content: note,
kind: 1,
pubkey: store.state.pubkey.toHex(),
tags: [],
createdAt: Date.now()
};
const eventJson = await amberSignerService.signEvent(draft);
await client.sendEvent(Event.fromJson(JSON.stringify(eventJson)));
} else {
await client.publishTextNote(note, []);
}
}
async function generate_image(message) {
@@ -64,14 +76,36 @@ async function generate_image(message) {
tags.push(Tag.parse(["i", message, "text"]))
let evt = new EventBuilder(5100, "NIP 90 Image Generation request", tags)
let res = await client.sendEventBuilder(evt)
store.commit('set_current_request_id_image', res.toHex())
console.log("IMAGE EVENT SENT: " + res.toHex())
let res;
let requestid;
if (localStorage.getItem('nostr-key-method') === 'android-signer') {
let draft = {
content: "NIP 90 Image Generation request",
kind: 5100,
pubkey: store.state.pubkey.toHex(),
tags: [
["i", message, "text"]
],
createdAt: Date.now()
};
res = await amberSignerService.signEvent(draft)
await client.sendEvent(Event.fromJson(JSON.stringify(res)))
requestid = res.id;
res = res.id;
} else {
res = await client.sendEventBuilder(evt);
requestid = res.toHex();
}
store.commit('set_current_request_id_image', requestid)
//console.log("IMAGE EVENT SENT: " + res.toHex())
//miniToastr.showMessage("Sent Request to DVMs", "Awaiting results", VueNotifications.types.warn)
searching = true
if (!store.state.imagehasEventListener){
listen()
listen()
store.commit('set_imagehasEventListener', true)
}
else{

View File

@@ -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,15 @@ 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()
@@ -91,7 +98,6 @@ export default {
catch (error){
console.log(error);
}
},
methods: {
@@ -214,6 +220,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) {
console.log(error);
}
},
async getnip89s(){
//let keys = Keys.generate()

View File

@@ -19,6 +19,7 @@ import {computed, onMounted, ref} from "vue";
import countries from "@/components/data/countries.json";
import deadnip89s from "@/components/data/deadnip89s.json";
import Nip07 from "@/components/Nip07.vue";
import amberSignerService from "./android-signer/AndroidSigner";
let items = []
let dvms =[]
@@ -92,26 +93,43 @@ async function send_search_request(msg) {
tags.push(Tag.parse(['param', 'users', JSON.stringify(users)]))
let evt = new EventBuilder(5302, "NIP 90 Search request", tags)
let res;
let requestid;
if (localStorage.getItem('nostr-key-method') === 'android-signer') {
let draft = {
content: "NIP 90 Search request",
kind: 5302,
pubkey: store.state.pubkey.toHex(),
tags: [
["i", msg, "text"],
["param", "max_results", "150"],
['param', 'users', JSON.stringify(users)]
],
createdAt: Date.now()
};
let res = await client.sendEventBuilder(evt)
let requestid = res.toHex()
res = await amberSignerService.signEvent(draft)
await client.sendEvent(Event.fromJson(JSON.stringify(res)))
requestid = res.id;
res = res.id;
} else {
res = await client.sendEventBuilder(evt)
requestid = res.toHex()
}
console.log("STORE: " +store.state.requestidSearch)
store.commit('set_current_request_id_search', requestid)
console.log("STORE AFTER: " + store.state.requestidSearch)
//miniToastr.showMessage("Sent Request to DVMs", "Awaiting results", VueNotifications.types.warn)
if (!store.state.hasEventListener){
listen()
listen()
store.commit('set_hasEventListener', true)
}
else{
console.log("Already has event listener")
}
console.log(res)
} catch (error) {
console.log(error);
}
@@ -313,7 +331,7 @@ async function listen() {
},
// Handle relay message
handleMsg: async (relayUrl, message) => {
//console.log("Received message from", relayUrl, message.asJson());
//console.log(`Received message from ${relayUrl} ${message.asJson()}`);
}
};

View File

@@ -0,0 +1,80 @@
// taken from https://github.com/hzrd149/nostrudel
import { nip19, verifySignature } from "nostr-tools";
import createDefer, { Deferred } from "./classes/deffered";
import { getPubkeyFromDecodeResult, isHexKey } from "./helpers/nip19";
import { 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) {
return `intent:${encodeURIComponent(
JSON.stringify(draft),
)}#Intent;scheme=nostrsigner;S.compressionType=none;S.returnType=event;S.type=sign_event;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): Promise<NostrEvent> {
const signedEventJson = await intentRequest(createSignEventIntent(draft));
const signedEvent = JSON.parse(signedEventJson) as NostrEvent;
if (!verifySignature(signedEvent)) throw new Error("Invalid signature");
return signedEvent;
}
const amberSignerService = {
supported: navigator.userAgent.includes("Android") && navigator.clipboard,
getPublicKey,
signEvent
};
export default amberSignerService;

View 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;
}

View File

@@ -0,0 +1,19 @@
import { getPublicKey, nip19 } from "nostr-tools";
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);
}
}

View 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;
}