Initial support for amber

This commit is contained in:
greenart7c3
2024-01-31 08:57:21 -03:00
parent cb290c3dda
commit e839a0241b
7 changed files with 263 additions and 6 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

@@ -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()

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

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,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);
}
}

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