diff --git a/lib/login.dart b/lib/login.dart index d856ad5..297d9e9 100644 --- a/lib/login.dart +++ b/lib/login.dart @@ -6,6 +6,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:ndk/domain_layer/entities/account.dart'; import 'package:ndk/shared/nips/nip01/bip340.dart'; import 'package:ndk/shared/nips/nip19/nip19.dart'; +import 'package:zap_stream_flutter/utils.dart'; class LoginAccount { final AccountType type; @@ -15,12 +16,15 @@ class LoginAccount { LoginAccount._({required this.type, required this.pubkey, this.privateKey}); static LoginAccount nip19(String key) { - final keyData = Nip19.decode(key); + final keyData = bech32ToHex(key); final pubkey = Nip19.isKey("nsec", key) ? Bip340.getPublicKey(keyData) : keyData; final privateKey = Nip19.isKey("npub", key) ? null : keyData; return LoginAccount._( - type: Nip19.isKey("npub", key) ? AccountType.publicKey : AccountType.privateKey, + type: + Nip19.isKey("npub", key) + ? AccountType.publicKey + : AccountType.privateKey, pubkey: pubkey, privateKey: privateKey, ); @@ -46,6 +50,13 @@ class LoginAccount { static LoginAccount? fromJson(Map json) { if (json.length > 2 && json.containsKey("pubKey")) { + if ((json["pubKey"] as String).length != 64) { + throw "Invalid pubkey, length != 64"; + } + if (json.containsKey("privateKey") && + (json["privateKey"] as String).length != 64) { + throw "Invalid privateKey, length != 64"; + } return LoginAccount._( type: AccountType.values.firstWhere( (v) => v.toString().endsWith(json["type"] as String), diff --git a/lib/pages/login.dart b/lib/pages/login.dart index 544a6ab..96add64 100644 --- a/lib/pages/login.dart +++ b/lib/pages/login.dart @@ -1,10 +1,10 @@ import 'package:amberflutter/amberflutter.dart'; import 'package:flutter/widgets.dart'; import 'package:go_router/go_router.dart'; -import 'package:ndk/shared/nips/nip19/nip19.dart'; import 'package:zap_stream_flutter/login.dart'; import 'package:zap_stream_flutter/main.dart'; import 'package:zap_stream_flutter/theme.dart'; +import 'package:zap_stream_flutter/utils.dart'; import 'package:zap_stream_flutter/widgets/button.dart'; class LoginPage extends StatelessWidget { @@ -24,7 +24,7 @@ class LoginPage extends StatelessWidget { final amber = Amberflutter(); final result = await amber.getPublicKey(); if (result['signature'] != null) { - final key = Nip19.decode(result['signature']); + final key = bech32ToHex(result['signature']); loginData.value = LoginAccount.externalPublicKeyHex(key); ctx.go("/"); } diff --git a/lib/pages/login_input.dart b/lib/pages/login_input.dart index 6255831..6be6c5a 100644 --- a/lib/pages/login_input.dart +++ b/lib/pages/login_input.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:ndk/shared/nips/nip19/nip19.dart'; import 'package:zap_stream_flutter/login.dart'; import 'package:zap_stream_flutter/main.dart'; import 'package:zap_stream_flutter/theme.dart'; +import 'package:zap_stream_flutter/utils.dart'; import 'package:zap_stream_flutter/widgets/button.dart'; class LoginInputPage extends StatefulWidget { @@ -30,7 +30,7 @@ class _LoginInputPage extends State { "Login", onTap: () async { try { - final keyData = Nip19.decode(_controller.text); + final keyData = bech32ToHex(_controller.text); if (keyData.isNotEmpty) { loginData.value = LoginAccount.nip19(_controller.text); context.go("/"); diff --git a/lib/pages/profile.dart b/lib/pages/profile.dart index f0eea5b..2b4553e 100644 --- a/lib/pages/profile.dart +++ b/lib/pages/profile.dart @@ -2,11 +2,11 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/widgets.dart'; import 'package:go_router/go_router.dart'; import 'package:ndk/ndk.dart'; -import 'package:ndk/shared/nips/nip19/nip19.dart'; import 'package:zap_stream_flutter/imgproxy.dart'; import 'package:zap_stream_flutter/main.dart'; import 'package:zap_stream_flutter/rx_filter.dart'; import 'package:zap_stream_flutter/theme.dart'; +import 'package:zap_stream_flutter/utils.dart'; import 'package:zap_stream_flutter/widgets/avatar.dart'; import 'package:zap_stream_flutter/widgets/button.dart'; import 'package:zap_stream_flutter/widgets/header.dart'; @@ -20,7 +20,7 @@ class ProfilePage extends StatelessWidget { @override Widget build(BuildContext context) { - final hexPubkey = Nip19.decode(pubkey); + final hexPubkey = bech32ToHex(pubkey); return ProfileLoaderWidget(hexPubkey, (ctx, state) { final profile = state.data ?? Metadata(pubKey: hexPubkey); return SingleChildScrollView( diff --git a/lib/utils.dart b/lib/utils.dart index 7a3b7bd..3b5d6f2 100644 --- a/lib/utils.dart +++ b/lib/utils.dart @@ -1,5 +1,8 @@ +import 'package:bech32/bech32.dart'; import 'package:collection/collection.dart'; +import 'package:convert/convert.dart'; import 'package:ndk/ndk.dart'; +import 'package:ndk/shared/nips/nip19/nip19.dart'; /// Container class over event and stream info class StreamEvent { @@ -238,3 +241,56 @@ String zapSum(List zaps) { .fold(0, (acc, v) => acc + (v.amountSats ?? 0)); return formatSats(total); } + +String bech32ToHex(String bech32) { + final decoder = Bech32Decoder(); + final data = decoder.convert(bech32, 10_000); + final data8bit = Nip19.convertBits(data.data, 5, 8, false); + if (data.hrp == "nevent" || data.hrp == "naddr" || data.hrp == "nprofile") { + final tlv = parseTLV(data8bit); + return hex.encode(tlv.firstWhere((v) => v.type == 0).value); + } else { + return hex.encode(data8bit); + } +} + +class TLV { + final int type; + final int length; + final List value; + + TLV(this.type, this.length, this.value); +} + +List parseTLV(List data) { + List result = []; + int index = 0; + + while (index < data.length) { + // Check if we have enough bytes for type and length + if (index + 2 > data.length) { + throw FormatException('Incomplete TLV data'); + } + + // Read type (1 byte) + int type = data[index]; + index++; + + // Read length (1 byte) + int length = data[index]; + index++; + + // Check if we have enough bytes for value + if (index + length > data.length) { + throw FormatException('TLV value length exceeds available data'); + } + + // Read value + List value = data.sublist(index, index + length); + index += length; + + result.add(TLV(type, length, value)); + } + + return result; +} diff --git a/lib/widgets/nostr_text.dart b/lib/widgets/nostr_text.dart index a5250c2..f3a189f 100644 --- a/lib/widgets/nostr_text.dart +++ b/lib/widgets/nostr_text.dart @@ -5,6 +5,7 @@ import 'package:ndk/ndk.dart'; import 'package:ndk/shared/nips/nip19/nip19.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:zap_stream_flutter/theme.dart'; +import 'package:zap_stream_flutter/utils.dart'; import 'package:zap_stream_flutter/widgets/profile.dart'; /// Converts a nostr note text containing links @@ -62,10 +63,15 @@ InlineSpan _buildProfileOrNoteSpan(String word) { cleanedWord.startsWith('note') || cleanedWord.startsWith('nevent'); if (isProfile) { - return _inlineMention(Nip19.decode(cleanedWord)); + final hexKey = bech32ToHex(cleanedWord); + if (hexKey.isNotEmpty) { + return _inlineMention(hexKey); + } else { + return TextSpan(text: "@$cleanedWord"); + } } if (isNote) { - final eventId = Nip19.decode(cleanedWord); + final eventId = bech32ToHex(cleanedWord); return TextSpan(text: eventId, style: TextStyle(color: PRIMARY_1)); } else { return TextSpan(text: word); diff --git a/pubspec.lock b/pubspec.lock index 27d3d6f..1bf0b13 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -26,7 +26,7 @@ packages: source: hosted version: "2.12.0" bech32: - dependency: transitive + dependency: "direct main" description: name: bech32 sha256: "156cbace936f7720c79a79d16a03efad343b1ef17106716e04b8b8e39f99f7f7" diff --git a/pubspec.yaml b/pubspec.yaml index e141019..df2a133 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,7 @@ dependencies: chewie: ^1.11.3 image_picker: ^1.1.2 emoji_picker_flutter: ^4.3.0 + bech32: ^0.2.2 dependency_overrides: ndk: