diff --git a/angular/src/app/action/nostr-nip76-root/nostr-nip76-root.component.css b/angular/src/app/action/nostr-nip76-root/nostr-nip76-root.component.css
new file mode 100644
index 00000000..1be77b53
--- /dev/null
+++ b/angular/src/app/action/nostr-nip76-root/nostr-nip76-root.component.css
@@ -0,0 +1,3 @@
+.signing-content {
+ height: 120px;
+}
\ No newline at end of file
diff --git a/angular/src/app/action/nostr-nip76-root/nostr-nip76-root.component.html b/angular/src/app/action/nostr-nip76-root/nostr-nip76-root.component.html
new file mode 100644
index 00000000..946bcb86
--- /dev/null
+++ b/angular/src/app/action/nostr-nip76-root/nostr-nip76-root.component.html
@@ -0,0 +1,9 @@
+
+ {{ "Action.AttestContentDescription" | translate }}:
+
+
+ {{ "Action.Wallet" | translate }}: {{walletManager.activeWallet?.name}}
+ {{ "Action.Account" | translate }}: {{walletManager.activeAccount?.name}}
+ {{ "Action.DerivationPath" | translate }}: {{networkService.getDerivationPathForAccount(walletManager.activeAccount)}}
+
+
diff --git a/angular/src/app/action/nostr-nip76-root/nostr-nip76-root.component.ts b/angular/src/app/action/nostr-nip76-root/nostr-nip76-root.component.ts
new file mode 100644
index 00000000..807bd85a
--- /dev/null
+++ b/angular/src/app/action/nostr-nip76-root/nostr-nip76-root.component.ts
@@ -0,0 +1,75 @@
+import { Component, ChangeDetectorRef, ApplicationRef, NgZone, OnInit } from '@angular/core';
+import { CryptoService, UIState, NetworksService, CommunicationService, AppManager, WalletManager } from '../../services';
+import { Router } from '@angular/router';
+import { ActionService } from '../../services/action.service';
+import { TranslateService } from '@ngx-translate/core';
+import { nostrPrivateChannelAccountName } from '../../../shared/handlers/nostr-nip76-wallet-handler';
+const { v4: uuidv4 } = require('uuid');
+
+@Component({
+ selector: 'app-nostr-nip76-root',
+ templateUrl: './nostr-nip76-root.component.html',
+ styleUrls: ['./nostr-nip76-root.component.css']
+})
+export class ActionNostrNip76RootComponent implements OnInit {
+ content?: string;
+ parameters?: any;
+ expiryDate: Date;
+ callback: string;
+ result: string;
+ success?: boolean;
+ mnemonic: string;
+
+ constructor(
+ public uiState: UIState,
+ private crypto: CryptoService,
+ private router: Router,
+ private app: ApplicationRef,
+ private actionService: ActionService,
+ private ngZone: NgZone,
+ private communication: CommunicationService,
+ public networkService: NetworksService,
+ public walletManager: WalletManager,
+ private manager: AppManager,
+ private cd: ChangeDetectorRef,
+ public translate: TranslateService) {
+
+ this.actionService.status.title = 'Use an HDK Index';
+ this.actionService.status.description =
+ `Uses a separate Hierarchical Deterministic Key on the Account used for signing, indexing and encrypting Nostr Events.`;
+ this.actionService.consentType = 'regular';
+ this.actionService.permissionLevel = 'account';
+ }
+
+ ngOnDestroy(): void {
+
+ }
+
+ async ngOnInit() {
+ const accountTranslate = await this.translate.get('Account.Account').toPromise();
+ this.uiState.title = accountTranslate + ': ' + this.walletManager.activeAccount?.name;
+ this.mnemonic = this.crypto.generateMnemonic();
+ // this.createPrivateChannelAccount();
+ }
+
+ async createPrivateChannelAccount() {
+ let account = this.walletManager.activeWallet.accounts.find(x => x.name === nostrPrivateChannelAccountName);
+ if (!account) {
+ account = {
+ identifier: uuidv4(),
+ type: 'identity',
+ mode: 'normal',
+ singleAddress: true,
+ networkType: 'NOSTR',
+ name: nostrPrivateChannelAccountName,
+ index: 1776,
+ network: 1237,
+ purpose: 44,
+ purposeAddress: 340,
+ icon: 'account_circle',
+ };
+ await this.walletManager.addAccount(account, this.walletManager.activeWallet);
+ }
+ this.actionService.accountId = account.identifier;
+ }
+}
diff --git a/angular/src/app/app-routing.module.ts b/angular/src/app/app-routing.module.ts
index 8548b894..0cc851f7 100644
--- a/angular/src/app/app-routing.module.ts
+++ b/angular/src/app/app-routing.module.ts
@@ -65,6 +65,7 @@ import { ActionNostrDecryptComponent } from './action/nostr.decrypt/nostr.decryp
import { ActionTransactionSendComponent } from './action/transaction.send/transaction.send.component';
import { ActionWalletUnlockComponent } from './action/wallet.unlock/wallet.unlock.component';
import { ActionSwapsSendComponent } from './action/swaps.send/swaps.send.component';
+import { ActionNostrNip76RootComponent } from './action/nostr-nip76-root/nostr-nip76-root.component';
const routes: Routes = [
{
@@ -477,6 +478,41 @@ const routes: Routes = [
data: LoadingResolverService,
},
},
+ {
+ path: 'nostr.nip76.index',
+ component: ActionNostrNip76RootComponent,
+ resolve: {
+ data: LoadingResolverService,
+ },
+ },
+ {
+ path: 'nostr.nip76.event.create',
+ component: ActionNostrNip76RootComponent,
+ resolve: {
+ data: LoadingResolverService,
+ },
+ },
+ {
+ path: 'nostr.nip76.event.delete',
+ component: ActionNostrNip76RootComponent,
+ resolve: {
+ data: LoadingResolverService,
+ },
+ },
+ {
+ path: 'nostr.nip76.invite.read',
+ component: ActionNostrNip76RootComponent,
+ resolve: {
+ data: LoadingResolverService,
+ },
+ },
+ {
+ path: 'nostr.nip76.invite.create',
+ component: ActionNostrNip76RootComponent,
+ resolve: {
+ data: LoadingResolverService,
+ },
+ },
{
path: 'transaction.send',
component: ActionTransactionSendComponent,
diff --git a/angular/src/app/app.module.ts b/angular/src/app/app.module.ts
index ef818bb3..89d9d931 100644
--- a/angular/src/app/app.module.ts
+++ b/angular/src/app/app.module.ts
@@ -131,7 +131,7 @@ import { ActionTransactionSendComponent } from './action/transaction.send/transa
import { SendConfirmationDialog } from './action/transaction.send/send-confirmation-dialog/send-confirmation-dialog';
import { ActionWalletUnlockComponent } from './action/wallet.unlock/wallet.unlock.component';
import { ActionSwapsSendComponent } from './action/swaps.send/swaps.send.component';
-
+import { ActionNostrNip76RootComponent } from './action/nostr-nip76-root/nostr-nip76-root.component';
// required for AOT compilation
export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
return new TranslateHttpLoader(http);
@@ -216,6 +216,7 @@ export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
SendConfirmationDialog,
ActionWalletUnlockComponent,
ActionSwapsSendComponent,
+ ActionNostrNip76RootComponent
],
imports: [
BrowserModule,
diff --git a/angular/src/shared/handlers/index.ts b/angular/src/shared/handlers/index.ts
index ef73c2fa..b83aac79 100644
--- a/angular/src/shared/handlers/index.ts
+++ b/angular/src/shared/handlers/index.ts
@@ -20,6 +20,7 @@ import { SendTransactionHandler } from './send-transaction-handler';
import { AtomicSwapsKeyHandler } from './atomic-swap-key-handler';
import { AtomicSwapsSecretHandler } from './atomic-swap-secret-handler';
import { AtomicSwapsSendHandler } from './atomic-swap-send-handler';
+import { NostrNip76WalletHandler } from './nostr-nip76-wallet-handler';
// TODO: Make this more generic where the handlers are registered as form of factory.
export class Handlers {
@@ -53,6 +54,14 @@ export class Handlers {
return new NostrEncryptHandler(backgroundManager);
case 'nostr.decrypt':
return new NostrDecryptHandler(backgroundManager);
+ case 'nostr.nip76.index':
+ case 'nostr.nip76.event.create':
+ case 'nostr.nip76.event.delete':
+ case 'nostr.nip76.invite.read':
+ case 'nostr.nip76.invite.create':
+ const handler = new NostrNip76WalletHandler(backgroundManager);
+ handler.action = [action];
+ return handler;
case 'transaction.send':
return new SendTransactionHandler(backgroundManager);
case 'atomicswaps.key':
diff --git a/angular/src/shared/handlers/nostr-nip76-wallet-handler.ts b/angular/src/shared/handlers/nostr-nip76-wallet-handler.ts
new file mode 100644
index 00000000..87159090
--- /dev/null
+++ b/angular/src/shared/handlers/nostr-nip76-wallet-handler.ts
@@ -0,0 +1,143 @@
+import { sha512 } from '@noble/hashes/sha512';
+import { bytesToHex } from '@noble/hashes/utils';
+import { HDKey } from '@scure/bip32';
+import {
+ ContentDocument, getReducedKey, HDKey as Nip76HDKey, HDKIndex, HDKIndexDTO, HDKIndexType, nip19Extension,
+ NostrEventDocument, SequentialKeysetDTO, Versions, Nip76ProviderIndexArgs, walletRsvpDocumentsOffset
+} from 'animiq-nip76-tools';
+import { getPublicKey } from 'nostr-tools';
+import { BackgroundManager } from '../background-manager';
+import { SigningUtilities } from '../identity/signing-utilities';
+import { ActionPrepareResult, ActionResponse, Permission } from '../interfaces';
+import { ActionHandler, ActionState } from './action-handler';
+
+export const nostrPrivateChannelAccountName = 'Nostr Private Channels';
+
+export class NostrNip76WalletHandler implements ActionHandler {
+ action = ['nostr.nip76.index'];
+ utility = new SigningUtilities();
+
+ constructor(private backgroundManager: BackgroundManager) { }
+
+ async prepare(state: ActionState): Promise {
+ return {
+ content: [],
+ consent: true,
+ };
+ }
+
+ async getKey(permission: Permission, keyId: string): Promise {
+ const { network, node } = await this.backgroundManager.getKey(permission.walletId, permission.accountId, keyId);
+ return node as HDKey;
+ }
+
+ async getRootKeyInfo(permission: Permission, keyPage = 0)
+ : Promise<{ rootKey: Nip76HDKey, wordset: Uint32Array, documentsIndex: HDKIndex }> {
+
+ const key0 = await this.getKey(permission, `1776'/0'`);
+ const rootKey = new Nip76HDKey({ privateKey: key0.privateKey, chainCode: key0.chainCode, version: Versions.nip76API1 });
+
+ const wordsetKey = await this.getKey(permission, `1776'/1'`);
+ const wordsetHash = sha512(wordsetKey.privateKey);
+ const wordset = new Uint32Array((wordsetHash).buffer);
+
+ const key1 = getReducedKey({ root: rootKey, wordset: wordset.slice(0, 4) });
+ const key2 = getReducedKey({ root: rootKey, wordset: wordset.slice(4, 8) });
+ const documentsIndex = new HDKIndex(HDKIndexType.Sequential | HDKIndexType.Private, key1, key2, wordset.slice(8));
+
+ documentsIndex.getSequentialKeyset(0, keyPage);
+ documentsIndex.getSequentialKeyset(walletRsvpDocumentsOffset, keyPage);
+
+ return { rootKey, wordset, documentsIndex };
+ }
+
+ async execute(state: ActionState, permission: Permission): Promise {
+ const profileKey = await this.getKey(permission, permission.keyId);
+ const key = getPublicKey(profileKey.privateKey as any);
+
+ switch (state.message.request.method) {
+ case 'nostr.nip76.index': {
+ const indexArgs: Nip76ProviderIndexArgs = state.message.request.params[1];
+ const { rootKey, wordset, documentsIndex } = await this.getRootKeyInfo(permission, indexArgs.keyPage);
+ if (indexArgs.privateIndexId === null) {
+ documentsIndex.signingParent.wipePrivateData();
+ documentsIndex.encryptParent.wipePrivateData();
+ const response = { hdkIndex: documentsIndex.toJSON() };
+ return { key, response };
+ } else {
+ const keyset = documentsIndex.getDocumentKeyset(indexArgs.privateIndexId, bytesToHex(profileKey.privateKey));
+ const hdkIndex = new HDKIndex(HDKIndexType.Sequential | HDKIndexType.Private, keyset.signingKey, keyset.encryptKey);
+ hdkIndex.getSequentialKeyset(0, indexArgs.keyPage);
+ hdkIndex.signingParent.wipePrivateData();
+ hdkIndex.encryptParent.wipePrivateData();
+ const response = { hdkIndex: hdkIndex.toJSON() };
+ return { key, response };
+ }
+ }
+ case 'nostr.nip76.event.create': {
+ const serializedContent = state.message.request.params[2];
+ const kind = parseInt(serializedContent.match(/\d+/)![0]);
+ const doc = HDKIndex.getContentDocument(kind);
+ doc.deserialize(serializedContent);
+ doc.content.pubkey = key;
+ doc.docIndex = state.message.request.params[3];
+
+ let hdkIndex: HDKIndex;
+ const indexArgs: Nip76ProviderIndexArgs = state.message.request.params[1];
+ if (indexArgs.publicIndex) {
+ hdkIndex = HDKIndex.fromJSON(indexArgs.publicIndex as HDKIndexDTO);
+ } else {
+ const { rootKey, wordset, documentsIndex } = await this.getRootKeyInfo(permission);
+ if (indexArgs.privateIndexId === null) {
+ hdkIndex = documentsIndex;
+ } else {
+ const keyset = documentsIndex.getDocumentKeyset(indexArgs.privateIndexId, bytesToHex(profileKey.privateKey));
+ hdkIndex = new HDKIndex(HDKIndexType.Sequential | HDKIndexType.Private, keyset.signingKey, keyset.encryptKey);
+ }
+ }
+ const event = await hdkIndex.createEvent(doc, bytesToHex(profileKey.privateKey));
+ const response = { event };
+ return { key, response };
+ }
+ case 'nostr.nip76.event.delete': {
+ const doc = new ContentDocument();
+ doc.nostrEvent = { id: state.message.request.params[2] } as NostrEventDocument;
+ doc.docIndex = state.message.request.params[3];
+
+ let hdkIndex: HDKIndex;
+ const indexArgs: Nip76ProviderIndexArgs = state.message.request.params[1];
+ if (indexArgs.publicIndex) {
+ hdkIndex = HDKIndex.fromJSON(indexArgs.publicIndex as HDKIndexDTO);
+ } else {
+ const { rootKey, wordset, documentsIndex } = await this.getRootKeyInfo(permission);
+ if (indexArgs.privateIndexId === null) {
+ hdkIndex = documentsIndex;
+ } else {
+ const keyset = documentsIndex.getDocumentKeyset(indexArgs.privateIndexId, bytesToHex(profileKey.privateKey));
+ hdkIndex = new HDKIndex(HDKIndexType.Sequential | HDKIndexType.Private, keyset.signingKey, keyset.encryptKey);
+ }
+ }
+ const event = await hdkIndex.createDeleteEvent(doc, bytesToHex(profileKey.privateKey));
+ const response = { event };
+ return { key, response };
+ }
+ case 'nostr.nip76.invite.read': {
+ const channelPointer = state.message.request.params[0];
+ const p = await nip19Extension.decode(channelPointer, bytesToHex(profileKey.privateKey));
+ const pointer = nip19Extension.pointerToDTO(p.data as nip19Extension.PrivateChannelPointer);
+ const response = { pointer };
+ return { key, response };
+ }
+ case 'nostr.nip76.invite.create': {
+ const pointer: nip19Extension.PrivateChannelPointerDTO = state.message.request.params[0];
+ const forPubkey: string = state.message.request.params[1];
+ const channelPointer = nip19Extension.pointerFromDTO(pointer);
+ const invitation = await nip19Extension.nprivateChannelEncode(channelPointer, bytesToHex(profileKey.privateKey), forPubkey);
+ const response = { invitation };
+ return { key, response };
+ }
+ default:
+ throw new Error(`method '${state.message.request.method}' not recognized.`);
+ };
+ }
+}
diff --git a/extension/src/provider.ts b/extension/src/provider.ts
index 4616dc9d..523db892 100644
--- a/extension/src/provider.ts
+++ b/extension/src/provider.ts
@@ -1,6 +1,7 @@
import { ActionMessage, ActionRequest, ActionResponse, EventEmitter, Listener } from '../../angular/src/shared';
import { Injector, RequestArguments, Web5RequestProvider } from '@blockcore/web5-injector';
-
+import { ContentDocument, ContentTemplate, HDKIndex, nip19Extension, NostrEventDocument,
+ INostrNip76Provider, Nip76ProviderIndexArgs, Nip76ProviderIndexArgDefaults } from 'animiq-nip76-tools';
export class BlockcoreRequestProvider implements Web5RequestProvider {
name = 'Blockcore';
#requests = {};
@@ -120,7 +121,7 @@ export class BlockcoreRequestProvider implements Web5RequestProvider {
}
class NostrProvider {
- constructor(private provider: BlockcoreRequestProvider) {}
+ constructor(private provider: BlockcoreRequestProvider) { }
/** Nostr NIP-07 function: https://github.com/nostr-protocol/nips/blob/master/07.md */
async getPublicKey(): Promise {
@@ -154,10 +155,11 @@ class NostrProvider {
}
nip04 = new NostrNip04(this.provider);
+ nip76 = new NostrNip76(this.provider);
}
export class NostrNip04 {
- constructor(private provider: BlockcoreRequestProvider) {}
+ constructor(private provider: BlockcoreRequestProvider) { }
async encrypt(peer: string, plaintext: string): Promise {
const result = (await this.provider.request({
@@ -178,6 +180,67 @@ export class NostrNip04 {
}
}
+export class NostrNip76 implements INostrNip76Provider {
+ constructor(private provider: BlockcoreRequestProvider) { }
+
+ async getIndex(privateIndexId?: number, keyPage?: number): Promise {
+ const indexArgs = Nip76ProviderIndexArgDefaults;
+ if(privateIndexId) indexArgs.privateIndexId = privateIndexId;
+ if(keyPage) indexArgs.keyPage = keyPage;
+ const result = (await this.provider.request({
+ method: 'nostr.nip76.index',
+ params: [{}, indexArgs],
+ })) as any;
+ const hdkIndex = HDKIndex.fromJSON(result.response.hdkIndex);
+ return hdkIndex;
+ }
+
+ async createEvent(doc: ContentDocument): Promise {
+ const indexArgs: Nip76ProviderIndexArgs = {
+ publicIndex: doc.dkxParent.isPrivate ? undefined : doc.dkxParent.toJSON(),
+ privateIndexId: doc.dkxParent.isPrivate ? doc.dkxParent.parentDocument?.docIndex : undefined
+ };
+ const result = (await this.provider.request({
+ method: 'nostr.nip76.event.create',
+ params: [{}, indexArgs, doc.serialize(), doc.docIndex],
+ })) as any;
+
+ return result.response.event;
+ }
+
+ async createDeleteEvent(doc: ContentDocument): Promise {
+ const indexArgs: Nip76ProviderIndexArgs = {
+ publicIndex: doc.dkxParent.isPrivate ? undefined : doc.dkxParent.toJSON(),
+ privateIndexId: doc.dkxParent.isPrivate ? doc.dkxParent.parentDocument?.docIndex : undefined
+ };
+ const result = (await this.provider.request({
+ method: 'nostr.nip76.event.delete',
+ params: [{}, indexArgs, doc.nostrEvent.id, doc.docIndex],
+ })) as any;
+
+ return result.response.event;
+ }
+
+ async readInvitation(channelPointer: string): Promise {
+ const result = (await this.provider.request({
+ method: 'nostr.nip76.invite.read',
+ params: [channelPointer],
+ })) as any;
+ const pointer = nip19Extension.pointerFromDTO(result.response.pointer);
+ return pointer;
+ }
+
+ async createInvitation(pointer: nip19Extension.PrivateChannelPointer, forPubkey: string): Promise {
+ const result = (await this.provider.request({
+ method: 'nostr.nip76.invite.create',
+ params: [nip19Extension.pointerToDTO(pointer), forPubkey],
+ })) as any;
+
+ return result.response.invitation;
+ }
+
+}
+
const provider = new BlockcoreRequestProvider();
Injector.register(provider);
diff --git a/package-lock.json b/package-lock.json
index 09ea35b8..328546d1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -34,6 +34,7 @@
"@scure/bip39": "^1.1.1",
"@tbd54566975/dwn-sdk-js": "^0.0.25",
"@types/qs": "^6.9.7",
+ "animiq-nip76-tools": "file:../../animiq-nip76-tools/dist",
"async-mutex": "^0.4.0",
"axios": "0.27.2",
"axios-retry": "^3.3.1",
@@ -4851,6 +4852,11 @@
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true
},
+ "node_modules/animiq-nip76-tools": {
+ "version": "1.0.5",
+ "resolved": "file:../../animiq-nip76-tools/dist",
+ "license": "MIT"
+ },
"node_modules/ansi-align": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
@@ -22582,6 +22588,9 @@
"dev": true,
"requires": {}
},
+ "animiq-nip76-tools": {
+ "version": "1.0.5"
+ },
"ansi-align": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
diff --git a/package.json b/package.json
index 3da9ed88..71e63d92 100644
--- a/package.json
+++ b/package.json
@@ -85,6 +85,7 @@
"js-base64": "3.7.3",
"ngx-logger": "^5.0.11",
"nostr-tools": "1.7.4",
+ "animiq-nip76-tools": "file:../../animiq-nip76-tools/dist",
"qrcode": "^1.5.1",
"qs": "^6.11.0",
"rxjs": "7.5.6",