import {Instance, SnapshotOut, types, flow} from 'mobx-state-tree' import { Metadata } from 'nostr-tools/kinds' import {KeyChain, MinibitsClient, NostrClient, NostrUnsignedEvent, WalletTask} from '../services' import {log} from '../services/logService' import { Err } from '../utils/AppError' import { getRandomUsername } from '../utils/usernames' import { getRootStore } from './helpers/getRootStore' export type WalletProfile = { pubkey: string walletId: string name: string nip05: string picture: string lud16?: string | null device?: string | null isOwnProfile: boolean } export type WalletProfileRecord = { id: number pubkey: string walletId: string name: string nip05: string lud16?: string | null device?: string | null avatar: string createdAt: string } export const WalletProfileStoreModel = types .model('WalletProfileStore', { pubkey: types.optional(types.string, ''), walletId: types.optional(types.string, ''), name: types.optional(types.string, ''), nip05: types.optional(types.string, ''), picture: types.optional(types.string, ''), lud16: types.maybe(types.maybeNull(types.string)), device: types.maybe(types.maybeNull(types.string)), isOwnProfile: types.optional(types.boolean, false), }) .actions(self => ({ publishToRelays: flow(function* publishToRelays() { try { const {pubkey, name, picture, nip05, lud16} = self // announce to minibits relay + default public relays with replaceable event const profileEvent: NostrUnsignedEvent = { kind: Metadata, pubkey, tags: [], content: JSON.stringify({ name, picture, nip05, lud16, }), created_at: Math.floor(Date.now() / 1000) } const rootStore = getRootStore(self) const {relaysStore} = rootStore // new wallet profile has not yet the relays if(relaysStore.allUrls.length === 0) { // saves default relays and creates subscription for incoming nostr messages WalletTask.receiveEventsFromRelaysQueue() } const relaysToPublish: string[] = relaysStore.allUrls log.debug('[publishToRelays]', 'Publish profile to relays', {profileEvent, relaysToPublish}) const publishedEvent: Event | undefined = yield NostrClient.publish( profileEvent, relaysToPublish ) return publishedEvent } catch (e: any) { log.error(e.name, e.message) return false // silent } }) })) .actions(self => ({ hydrate: flow(function* hydrate(profileRecord: WalletProfileRecord) { const {name, nip05, lud16, avatar, pubkey, walletId} = profileRecord self.pubkey = pubkey self.walletId = walletId self.name = name // default name is set on server side, equals walletId self.nip05 = nip05 // default is name@minibits.cash set on server side self.lud16 = lud16 // equals to nip05 for all @minibits.cash addresses, set on server side self.picture = avatar // default picture is set on server side yield self.publishToRelays() }) })) .actions(self => ({ create: flow(function* create(publicKey: string, walletId: string, seedHash: string) { let profileRecord: WalletProfileRecord log.trace('[create]', {seedHash, publicKey, walletId}) try { // creates new profile. If all params equal existing one, it is returned profileRecord = yield MinibitsClient.createWalletProfile(publicKey, walletId, seedHash) self.hydrate(profileRecord) log.info('[create]', 'Wallet profile saved in WalletProfileStore', {self}) return self } catch (e: any) { // Unlikely we might hit the same walletId so we retry with another one if(e.name === Err.ALREADY_EXISTS_ERROR) { // recreate walletId + default name const name = getRandomUsername() // attempt to create new unique profile again profileRecord = yield MinibitsClient.createWalletProfile(publicKey, name, seedHash) log.error('[create]', 'Profile reset executed to resolve duplicate walletId on the server.', {caller: 'create', walletId, newWalletId: name}) self.hydrate(profileRecord) return } throw e } }), updateName: flow(function* updateName(name: string) { let profileRecord: WalletProfileRecord = yield MinibitsClient.updateWalletProfile( self.pubkey, { name, } ) self.hydrate(profileRecord) log.debug('[updateName]', 'Wallet name updated in the WalletProfileStore', {self}) return self }), updatePicture: flow(function* updatePicture(picture: string) { let profileRecord: WalletProfileRecord = yield MinibitsClient.updateWalletProfile( self.pubkey, { avatar: picture, // this is png in base64 } ) self.picture = profileRecord.avatar + '?r=' + Math.floor(Math.random() * 100) // force refresh as image URL stays the same const publishedEvent = yield self.publishToRelays() log.debug('[updatePicture]', 'Wallet picture updated in the WalletProfileStore', {self, publishedEvent}) return self }), updateNip05: flow(function* updateNip05(newPubkey: string, name: string, nip05: string, lud16: string, picture: string, isOwnProfile: boolean) { log.trace('[updateNip05]', {currentPubkey: self.pubkey, newPubkey}) let profileRecord: WalletProfileRecord = yield MinibitsClient.updateWalletProfileNip05( self.pubkey, { newPubkey, nip05, lud16, name, avatar: picture } ) log.trace('[updateNip05]', 'profileRecord', {profileRecord}) self.hydrate(profileRecord) log.info('[updateNip05]', 'Wallet nip05 updated in the WalletProfileStore', {self}) return self }), recover: flow(function* recover(publicKey: string, walletId: string, seedHash: string) { let profileRecord: WalletProfileRecord = yield MinibitsClient.recoverProfile( publicKey, walletId, seedHash ) self.hydrate(profileRecord) log.info('[recover]', 'Wallet profile recovered in WalletProfileStore', {self}) return self }), setDevice: flow(function* setDevice(device: string) { try { if(!self.pubkey) { // skip call for new installs without a profile return } yield MinibitsClient.updateDeviceToken(self.pubkey, {deviceToken: device}) self.device = device } catch (e: any) { log.error('[setDevice]', e.message) } }) })) .views(self => ({ get npub() { return NostrClient.getNpubkey(self.pubkey) }, })) export interface WalletProfileStore extends Instance {} export interface WalletProfileStoreSnapshot extends SnapshotOut {}