chore(base): delete nostr-metadata-service + retire webapp-side kind-0 broadcast paths #82
9 changed files with 55 additions and 308 deletions
|
|
@ -144,9 +144,6 @@ export const SERVICE_TOKENS = {
|
|||
SUBMISSION_SERVICE: Symbol('submissionService'),
|
||||
LINK_PREVIEW_SERVICE: Symbol('linkPreviewService'),
|
||||
|
||||
// Nostr metadata services
|
||||
NOSTR_METADATA_SERVICE: Symbol('nostrMetadataService'),
|
||||
|
||||
// Nostr transport (kind-21000 RPC over relays — LNbits backend)
|
||||
NOSTR_TRANSPORT_SERVICE: Symbol('nostrTransportService'),
|
||||
|
||||
|
|
|
|||
|
|
@ -112,12 +112,29 @@ export class LnbitsAPI extends BaseService {
|
|||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
// Try to surface FastAPI's `{"detail": "..."}` shape; fall back to raw
|
||||
// body for non-JSON errors. Without this, every backend error renders
|
||||
// as a generic "API request failed: <status>" and you can't distinguish
|
||||
// "wrong endpoint" from "expired token" from "validation failure".
|
||||
let detail: string = errorText
|
||||
try {
|
||||
const parsed = JSON.parse(errorText)
|
||||
if (parsed && typeof parsed.detail === 'string') {
|
||||
detail = parsed.detail
|
||||
} else if (parsed && Array.isArray(parsed.detail)) {
|
||||
// pydantic ValidationError: take the first msg
|
||||
detail = parsed.detail[0]?.msg ?? errorText
|
||||
}
|
||||
} catch {
|
||||
// body wasn't JSON; keep the raw text in `detail`
|
||||
}
|
||||
console.error('LNBits API Error:', {
|
||||
endpoint,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
errorText
|
||||
detail,
|
||||
})
|
||||
throw new Error(`API request failed: ${response.status} ${response.statusText}`)
|
||||
throw new Error(`LNbits ${endpoint} ${response.status}: ${detail || response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
|
@ -186,8 +203,12 @@ export class LnbitsAPI extends BaseService {
|
|||
}
|
||||
|
||||
async updateProfile(data: Partial<User>): Promise<User> {
|
||||
return this.request<User>('/auth/update', {
|
||||
method: 'PUT',
|
||||
// aiolabs/lnbits PR #26 (gap-fill 869f67c3) wired
|
||||
// _publish_nostr_metadata_event into PATCH /api/v1/auth
|
||||
// (auth_api.py:546). The legacy PUT /auth/update route does not
|
||||
// exist on the post-cascade server.
|
||||
return this.request<User>('/auth', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,18 +32,24 @@ export function useActivityDetail(activityId: string) {
|
|||
isLoading.value = true
|
||||
error.value = null
|
||||
|
||||
// Subscribe and wait for this specific event
|
||||
// Scope both the subscription and the one-shot query to this
|
||||
// activity's d-tag. Without this scope, the query asks every
|
||||
// relay for every kind-31922/31923 event and races a 5s timeout
|
||||
// to find ours — on a cold page refresh that race is often lost
|
||||
// even when the activity is reachable.
|
||||
const detailFilters = { dTags: [activityId] }
|
||||
|
||||
unsubscribe = nostrService.subscribeToCalendarEvents(
|
||||
(incoming) => {
|
||||
store.upsertActivity(incoming)
|
||||
if (incoming.id === activityId) {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
},
|
||||
detailFilters
|
||||
)
|
||||
|
||||
// Also do a one-shot query
|
||||
const results = await nostrService.queryCalendarEvents()
|
||||
const results = await nostrService.queryCalendarEvents(detailFilters)
|
||||
store.upsertActivities(results)
|
||||
|
||||
// If we still don't have it after query, stop loading
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ export interface CalendarEventFilters {
|
|||
hashtags?: string[]
|
||||
/** Filter by geohash prefix (NIP-52 'g' tag) */
|
||||
geohash?: string
|
||||
/** Filter by NIP-52 'd' tag — scopes the query to specific parameterized-replaceable events */
|
||||
dTags?: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -168,6 +170,7 @@ export class ActivitiesNostrService extends BaseService {
|
|||
if (filters?.authors?.length) filter.authors = filters.authors
|
||||
if (filters?.hashtags?.length) filter['#t'] = filters.hashtags
|
||||
if (filters?.geohash) filter['#g'] = [filters.geohash]
|
||||
if (filters?.dTags?.length) filter['#d'] = filters.dTags
|
||||
|
||||
return [filter]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,7 @@
|
|||
import { ref, computed } from 'vue'
|
||||
import { BaseService } from '@/core/base/BaseService'
|
||||
import { eventBus } from '@/core/event-bus'
|
||||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||
import type { LoginCredentials, RegisterData, User } from '@/lib/api/lnbits'
|
||||
import type { NostrMetadataService } from '../nostr/nostr-metadata-service'
|
||||
import { getPendingAuthToken, removePendingAuthToken } from '@/lib/config/lnbits'
|
||||
|
||||
export class AuthService extends BaseService {
|
||||
|
|
@ -114,9 +112,6 @@ export class AuthService extends BaseService {
|
|||
|
||||
eventBus.emit('auth:login', { user: userData }, 'auth-service')
|
||||
|
||||
// Auto-broadcast Nostr metadata on login
|
||||
this.broadcastNostrMetadata()
|
||||
|
||||
} catch (error) {
|
||||
const err = this.handleError(error, 'login')
|
||||
eventBus.emit('auth:login-failed', { error: err }, 'auth-service')
|
||||
|
|
@ -138,9 +133,6 @@ export class AuthService extends BaseService {
|
|||
|
||||
eventBus.emit('auth:login', { user: userData }, 'auth-service')
|
||||
|
||||
// Auto-broadcast Nostr metadata on registration
|
||||
this.broadcastNostrMetadata()
|
||||
|
||||
} catch (error) {
|
||||
const err = this.handleError(error, 'register')
|
||||
eventBus.emit('auth:login-failed', { error: err }, 'auth-service')
|
||||
|
|
@ -195,10 +187,9 @@ export class AuthService extends BaseService {
|
|||
prvkey: this.user.value?.prvkey || updatedUser.prvkey
|
||||
}
|
||||
|
||||
// Auto-broadcast Nostr metadata when profile is updated
|
||||
// Note: ProfileSettings component will also manually broadcast,
|
||||
// but this ensures metadata stays in sync even if updated elsewhere
|
||||
this.broadcastNostrMetadata()
|
||||
// Kind-0 metadata is published server-side by lnbits's PATCH /auth handler
|
||||
// (aiolabs/lnbits commit 869f67c3) once the cascade is deployed. The webapp
|
||||
// no longer maintains its own broadcast path.
|
||||
|
||||
} catch (error) {
|
||||
const err = this.handleError(error, 'updateProfile')
|
||||
|
|
@ -208,26 +199,6 @@ export class AuthService extends BaseService {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast user metadata to Nostr relays (NIP-01 kind 0)
|
||||
* Called automatically on login, registration, and profile updates
|
||||
*/
|
||||
private async broadcastNostrMetadata(): Promise<void> {
|
||||
try {
|
||||
const metadataService = injectService<NostrMetadataService>(SERVICE_TOKENS.NOSTR_METADATA_SERVICE)
|
||||
if (metadataService && this.user.value?.pubkey) {
|
||||
// Broadcast in background - don't block login/update
|
||||
metadataService.publishMetadata().catch(error => {
|
||||
console.warn('Failed to broadcast Nostr metadata:', error)
|
||||
// Don't throw - this is a non-critical background operation
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
// If service isn't available yet, silently skip
|
||||
console.debug('Nostr metadata service not yet available')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup when service is disposed
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -122,32 +122,17 @@
|
|||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-col sm:flex-row gap-3">
|
||||
<Button
|
||||
type="submit"
|
||||
:disabled="isUpdating || !isFormValid"
|
||||
class="flex-1"
|
||||
class="w-full"
|
||||
>
|
||||
<span v-if="isUpdating">Updating...</span>
|
||||
<span v-else>Update Profile</span>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
:disabled="isBroadcasting"
|
||||
@click="broadcastMetadata"
|
||||
class="flex-1"
|
||||
>
|
||||
<Radio class="mr-2 h-4 w-4" :class="{ 'animate-pulse': isBroadcasting }" />
|
||||
<span v-if="isBroadcasting">Broadcasting...</span>
|
||||
<span v-else>Broadcast to Nostr</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Your profile is automatically broadcast to Nostr when you update it or log in.
|
||||
Use the "Broadcast to Nostr" button to manually re-broadcast your profile.
|
||||
Your profile is broadcast to Nostr automatically when you save changes.
|
||||
</p>
|
||||
</form>
|
||||
|
||||
|
|
@ -189,7 +174,7 @@ import * as z from 'zod'
|
|||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { User, Zap, Hash, Radio } from 'lucide-vue-next'
|
||||
import { User, Zap, Hash } from 'lucide-vue-next'
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
|
|
@ -215,19 +200,16 @@ import { useAuth } from '@/composables/useAuthService'
|
|||
import { useRouter } from 'vue-router'
|
||||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||
import type { ImageUploadService } from '../services/ImageUploadService'
|
||||
import type { NostrMetadataService } from '../nostr/nostr-metadata-service'
|
||||
import { useToast } from '@/core/composables/useToast'
|
||||
|
||||
// Services
|
||||
const { user, updateProfile, logout } = useAuth()
|
||||
const router = useRouter()
|
||||
const imageService = injectService<ImageUploadService>(SERVICE_TOKENS.IMAGE_UPLOAD_SERVICE)
|
||||
const metadataService = injectService<NostrMetadataService>(SERVICE_TOKENS.NOSTR_METADATA_SERVICE)
|
||||
const toast = useToast()
|
||||
|
||||
// Local state
|
||||
const isUpdating = ref(false)
|
||||
const isBroadcasting = ref(false)
|
||||
const updateError = ref<string | null>(null)
|
||||
const updateSuccess = ref(false)
|
||||
const uploadedPicture = ref<any[]>([])
|
||||
|
|
@ -323,18 +305,12 @@ const updateUserProfile = async (formData: any) => {
|
|||
}
|
||||
}
|
||||
|
||||
// Update profile via AuthService (which updates LNbits)
|
||||
// Update profile via AuthService (which updates LNbits).
|
||||
// Kind-0 metadata publishing happens server-side as part of the
|
||||
// PATCH /api/v1/auth handler (aiolabs/lnbits 869f67c3).
|
||||
await updateProfile(updateData)
|
||||
|
||||
// Broadcast to Nostr automatically
|
||||
try {
|
||||
await metadataService.publishMetadata()
|
||||
toast.success('Profile updated and broadcast to Nostr!')
|
||||
} catch (nostrError) {
|
||||
console.error('Failed to broadcast to Nostr:', nostrError)
|
||||
toast.warning('Profile updated, but failed to broadcast to Nostr')
|
||||
}
|
||||
|
||||
toast.success('Profile updated!')
|
||||
updateSuccess.value = true
|
||||
|
||||
// Clear success message after 3 seconds
|
||||
|
|
@ -352,22 +328,6 @@ const updateUserProfile = async (formData: any) => {
|
|||
}
|
||||
}
|
||||
|
||||
// Manually broadcast metadata to Nostr
|
||||
const broadcastMetadata = async () => {
|
||||
isBroadcasting.value = true
|
||||
|
||||
try {
|
||||
const result = await metadataService.publishMetadata()
|
||||
toast.success(`Profile broadcast to ${result.success}/${result.total} relays!`)
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to broadcast metadata'
|
||||
console.error('Error broadcasting metadata:', error)
|
||||
toast.error(`Failed to broadcast: ${errorMessage}`)
|
||||
} finally {
|
||||
isBroadcasting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Log out + redirect to /login on this app's origin.
|
||||
const onLogout = async () => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||
import type { NostrMetadataService, NostrMetadata } from '../nostr/nostr-metadata-service'
|
||||
|
||||
/**
|
||||
* Composable for accessing Nostr metadata service
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const { publishMetadata, getMetadata } = useNostrMetadata()
|
||||
*
|
||||
* // Get current metadata
|
||||
* const metadata = getMetadata()
|
||||
*
|
||||
* // Publish metadata to Nostr relays
|
||||
* await publishMetadata()
|
||||
* ```
|
||||
*/
|
||||
export function useNostrMetadata() {
|
||||
const metadataService = injectService<NostrMetadataService>(SERVICE_TOKENS.NOSTR_METADATA_SERVICE)
|
||||
|
||||
/**
|
||||
* Publish user metadata to Nostr relays (NIP-01 kind 0)
|
||||
*/
|
||||
const publishMetadata = async (): Promise<{ success: number; total: number }> => {
|
||||
return await metadataService.publishMetadata()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current user's Nostr metadata
|
||||
*/
|
||||
const getMetadata = (): NostrMetadata => {
|
||||
return metadataService.getMetadata()
|
||||
}
|
||||
|
||||
return {
|
||||
publishMetadata,
|
||||
getMetadata
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ import type { App } from 'vue'
|
|||
import type { ModulePlugin } from '@/core/types'
|
||||
import { container, SERVICE_TOKENS } from '@/core/di-container'
|
||||
import { relayHub } from './nostr/relay-hub'
|
||||
import { NostrMetadataService } from './nostr/nostr-metadata-service'
|
||||
import { ProfileService } from './nostr/ProfileService'
|
||||
import { ReactionService } from './nostr/ReactionService'
|
||||
import { NostrTransportService } from './services/NostrTransportService'
|
||||
|
|
@ -30,7 +29,6 @@ import ProfileSettings from './components/ProfileSettings.vue'
|
|||
const invoiceService = new InvoiceService()
|
||||
const lnbitsAPI = new LnbitsAPI()
|
||||
const imageUploadService = new ImageUploadService()
|
||||
const nostrMetadataService = new NostrMetadataService()
|
||||
const profileService = new ProfileService()
|
||||
const reactionService = new ReactionService()
|
||||
const nostrTransportService = new NostrTransportService()
|
||||
|
|
@ -48,7 +46,6 @@ export const baseModule: ModulePlugin = {
|
|||
|
||||
// Register core Nostr services
|
||||
container.provide(SERVICE_TOKENS.RELAY_HUB, relayHub)
|
||||
container.provide(SERVICE_TOKENS.NOSTR_METADATA_SERVICE, nostrMetadataService)
|
||||
|
||||
// Register auth service
|
||||
container.provide(SERVICE_TOKENS.AUTH_SERVICE, auth)
|
||||
|
|
@ -113,10 +110,6 @@ export const baseModule: ModulePlugin = {
|
|||
waitForDependencies: true, // ImageUploadService depends on ToastService
|
||||
maxRetries: 3
|
||||
})
|
||||
await nostrMetadataService.initialize({
|
||||
waitForDependencies: true, // NostrMetadataService depends on AuthService and RelayHub
|
||||
maxRetries: 3
|
||||
})
|
||||
await profileService.initialize({
|
||||
waitForDependencies: true, // ProfileService depends on RelayHub
|
||||
maxRetries: 3
|
||||
|
|
@ -145,7 +138,6 @@ export const baseModule: ModulePlugin = {
|
|||
await storageService.dispose()
|
||||
await toastService.dispose()
|
||||
await imageUploadService.dispose()
|
||||
await nostrMetadataService.dispose()
|
||||
await profileService.dispose()
|
||||
await reactionService.dispose()
|
||||
await nostrTransportService.dispose()
|
||||
|
|
@ -156,7 +148,6 @@ export const baseModule: ModulePlugin = {
|
|||
container.remove(SERVICE_TOKENS.LNBITS_API)
|
||||
container.remove(SERVICE_TOKENS.INVOICE_SERVICE)
|
||||
container.remove(SERVICE_TOKENS.IMAGE_UPLOAD_SERVICE)
|
||||
container.remove(SERVICE_TOKENS.NOSTR_METADATA_SERVICE)
|
||||
container.remove(SERVICE_TOKENS.PROFILE_SERVICE)
|
||||
container.remove(SERVICE_TOKENS.REACTION_SERVICE)
|
||||
|
||||
|
|
@ -173,7 +164,6 @@ export const baseModule: ModulePlugin = {
|
|||
invoiceService,
|
||||
pwaService,
|
||||
imageUploadService,
|
||||
nostrMetadataService,
|
||||
profileService,
|
||||
reactionService
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,162 +0,0 @@
|
|||
import { BaseService } from '@/core/base/BaseService'
|
||||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||
import { finalizeEvent, type EventTemplate } from 'nostr-tools'
|
||||
import type { AuthService } from '@/modules/base/auth/auth-service'
|
||||
import type { RelayHub } from '@/modules/base/nostr/relay-hub'
|
||||
|
||||
/**
|
||||
* Nostr User Metadata (NIP-01 kind 0)
|
||||
* https://github.com/nostr-protocol/nips/blob/master/01.md
|
||||
*/
|
||||
export interface NostrMetadata {
|
||||
name?: string // Display name (from username)
|
||||
display_name?: string // Alternative display name
|
||||
about?: string // Bio/description
|
||||
picture?: string // Profile picture URL
|
||||
banner?: string // Profile banner URL
|
||||
nip05?: string // NIP-05 identifier (username@domain)
|
||||
lud16?: string // Lightning Address (same as nip05)
|
||||
website?: string // Personal website
|
||||
}
|
||||
|
||||
/**
|
||||
* Service for publishing and managing Nostr user metadata (NIP-01 kind 0)
|
||||
*
|
||||
* This service handles:
|
||||
* - Publishing user profile metadata to Nostr relays
|
||||
* - Syncing LNbits user data with Nostr profile
|
||||
* - Auto-broadcasting metadata on login and profile updates
|
||||
*/
|
||||
export class NostrMetadataService extends BaseService {
|
||||
protected readonly metadata = {
|
||||
name: 'NostrMetadataService',
|
||||
version: '1.0.0',
|
||||
dependencies: ['AuthService', 'RelayHub']
|
||||
}
|
||||
|
||||
protected authService: AuthService | null = null
|
||||
protected relayHub: RelayHub | null = null
|
||||
|
||||
protected async onInitialize(): Promise<void> {
|
||||
console.log('NostrMetadataService: Starting initialization...')
|
||||
|
||||
this.authService = injectService<AuthService>(SERVICE_TOKENS.AUTH_SERVICE)
|
||||
this.relayHub = injectService<RelayHub>(SERVICE_TOKENS.RELAY_HUB)
|
||||
|
||||
if (!this.authService) {
|
||||
throw new Error('AuthService not available')
|
||||
}
|
||||
|
||||
if (!this.relayHub) {
|
||||
throw new Error('RelayHub service not available')
|
||||
}
|
||||
|
||||
console.log('NostrMetadataService: Initialization complete')
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Nostr metadata from LNbits user data
|
||||
*/
|
||||
private buildMetadata(): NostrMetadata {
|
||||
const user = this.authService?.user.value
|
||||
if (!user) {
|
||||
throw new Error('No authenticated user')
|
||||
}
|
||||
|
||||
const lightningDomain = import.meta.env.VITE_LIGHTNING_DOMAIN || window.location.hostname
|
||||
const username = user.username || user.id.slice(0, 8)
|
||||
|
||||
const metadata: NostrMetadata = {
|
||||
name: username,
|
||||
nip05: `${username}@${lightningDomain}`,
|
||||
lud16: `${username}@${lightningDomain}`
|
||||
}
|
||||
|
||||
// Add optional fields from user.extra if they exist
|
||||
if (user.extra?.display_name) {
|
||||
metadata.display_name = user.extra.display_name
|
||||
}
|
||||
|
||||
if (user.extra?.picture) {
|
||||
metadata.picture = user.extra.picture
|
||||
}
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish user metadata to Nostr relays (NIP-01 kind 0)
|
||||
*
|
||||
* This creates a replaceable event that updates the user's profile.
|
||||
* Only the latest kind 0 event for a given pubkey is kept by relays.
|
||||
*/
|
||||
async publishMetadata(): Promise<{ success: number; total: number }> {
|
||||
if (!this.authService?.isAuthenticated.value) {
|
||||
throw new Error('Must be authenticated to publish metadata')
|
||||
}
|
||||
|
||||
if (!this.relayHub?.isConnected.value) {
|
||||
throw new Error('Not connected to relays')
|
||||
}
|
||||
|
||||
const user = this.authService.user.value
|
||||
if (!user?.prvkey) {
|
||||
throw new Error('User private key not available')
|
||||
}
|
||||
|
||||
try {
|
||||
const metadata = this.buildMetadata()
|
||||
|
||||
console.log('📤 Publishing Nostr metadata (kind 0):', metadata)
|
||||
|
||||
// Create kind 0 event (user metadata)
|
||||
// Content is JSON-stringified metadata
|
||||
const eventTemplate: EventTemplate = {
|
||||
kind: 0,
|
||||
content: JSON.stringify(metadata),
|
||||
tags: [],
|
||||
created_at: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
|
||||
// Sign the event
|
||||
const privkeyBytes = this.hexToUint8Array(user.prvkey)
|
||||
const signedEvent = finalizeEvent(eventTemplate, privkeyBytes)
|
||||
|
||||
console.log('✅ Metadata event signed:', signedEvent.id)
|
||||
console.log('📋 Full signed event:', JSON.stringify(signedEvent, null, 2))
|
||||
|
||||
// Publish to all connected relays
|
||||
const result = await this.relayHub.publishEvent(signedEvent)
|
||||
|
||||
console.log(`✅ Metadata published to ${result.success}/${result.total} relays`)
|
||||
|
||||
return result
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to publish metadata:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current user's Nostr metadata
|
||||
*/
|
||||
getMetadata(): NostrMetadata {
|
||||
return this.buildMetadata()
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to convert hex string to Uint8Array
|
||||
*/
|
||||
private hexToUint8Array(hex: string): Uint8Array {
|
||||
const bytes = new Uint8Array(hex.length / 2)
|
||||
for (let i = 0; i < hex.length; i += 2) {
|
||||
bytes[i / 2] = parseInt(hex.substr(i, 2), 16)
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
protected async onDestroy(): Promise<void> {
|
||||
// Cleanup if needed
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue