import { Authenticator } from '@smontero/nostr-ual' import { Event } from 'nostr-tools' import { Balance } from './Balance' import { BaseOwnedSigner, PolicyPathSelector, Trx, Policy, FinalizeTrxResponse, BasicTrxDetails, TrxDetails, Utxo, PolicyPathsResult } from './types' import { BitcoinUtil, Wallet } from './interfaces' import { PaginationOpts, TimeUtil, fromNostrDate, toPublished } from '../util' import { generateUiMetadata, UIMetadata, Key } from '../util/GenerateUiMetadata' import { LabeledUtxo, PublishedLabel, PublishedOwnedSigner, PublishedProofOfReserveProposal, PublishedSharedSigner, PublishedSpendingProposal } from '../types' import { type Store } from '../service' export class PublishedPolicy { id: string name: string description: string descriptor: string createdAt: Date sharedKeyAuth: Authenticator nostrPublicKeys: string[] lastSyncTime?: Date generatedUiMetadata?: UIMetadata vaultData?: string private wallet: Wallet private syncTimeGap: number private syncPromise?: Promise private getSharedSigners: (publicKeys?: string | string[]) => Promise private getOwnedSigners: () => Promise private toMiniscript: (descriptor: string) => string private getProposalsByPolicyId: (policy_ids: string[] | string, paginationOpts: PaginationOpts) => Promise>> private getLabelsByPolicyId: (policy_ids: string[] | string, paginationOpts: PaginationOpts) => Promise>> private labelStore: Store static fromPolicyAndEvent({ policyContent, policyEvent, bitcoinUtil, nostrPublicKeys, sharedKeyAuth, }: { policyContent: Policy, policyEvent: Event, bitcoinUtil: BitcoinUtil, nostrPublicKeys: string[], sharedKeyAuth: Authenticator }, getSharedSigners: (publicKeys?: string | string[]) => Promise, getOwnedSigners: () => Promise, getProposalsByPolicyId: (policy_ids: string[] | string, paginationOpts: PaginationOpts) => Promise>>, getLabelsByPolicyId: (policy_ids: string[] | string, paginationOpts: PaginationOpts) => Promise>>, labelStore: Store ) : PublishedPolicy { return new PublishedPolicy( toPublished(policyContent, policyEvent), bitcoinUtil, nostrPublicKeys, sharedKeyAuth, getSharedSigners, getOwnedSigners, getProposalsByPolicyId, getLabelsByPolicyId, labelStore ) } constructor({ id, name, description, descriptor, createdAt, }: { id: string name: string description: string descriptor: string createdAt: Date }, bitcoinUtil: BitcoinUtil, nostrPublicKeys: string[], sharedKeyAuth: Authenticator, getSharedSigners: (publicKeys?: string | string[]) => Promise, getOwnedSigners: () => Promise, getProposalsByPolicyId: (policy_ids: string[] | string, paginationOpts: PaginationOpts) => Promise>>, getLabelsByPolicyId: (policy_ids: string[] | string, paginationOpts: PaginationOpts) => Promise>>, labelStore: Store, ) { this.id = id this.name = name this.description = description this.descriptor = descriptor this.createdAt = createdAt this.nostrPublicKeys = nostrPublicKeys this.syncTimeGap = bitcoinUtil.walletSyncTimeGap this.sharedKeyAuth = sharedKeyAuth this.wallet = bitcoinUtil.createWallet(descriptor) this.getSharedSigners = getSharedSigners this.getOwnedSigners = getOwnedSigners this.toMiniscript = bitcoinUtil.toMiniscript this.getProposalsByPolicyId = getProposalsByPolicyId this.getLabelsByPolicyId = getLabelsByPolicyId this.labelStore = labelStore } async getUiMetadata(): Promise { if (this.generatedUiMetadata) { return this.generatedUiMetadata; } const filter = this.getFilter(); const filteredOwnedSigners = this.filterSigners(await this.getOwnedSigners(), filter) as Array; const filteredSharedSigners = this.filterSigners(await this.getSharedSigners(this.nostrPublicKeys), filter) as Array; const uniqueSigners = this.removeDuplicates([...filteredSharedSigners, ...filteredOwnedSigners]); const keys = this.getKeys(uniqueSigners); return this.getUiMetadataFromDescriptor(filteredOwnedSigners, keys); } private getFilter(): string { return this.wallet.network() === 'testnet' ? 'tpub' : 'xpub'; } private filterSigners(signers: Array | Array, filter: string): Array | Array { return signers.filter(signer => signer.descriptor.includes(filter)); } private getKeys(signers: Array): Array { const filteredSigners = this.nostrPublicKeys.length > 0 ? signers.filter(signer => this.nostrPublicKeys.includes(signer.ownerPubKey!)) : signers; return filteredSigners.map(signer => ({ pubkey: signer.ownerPubKey!, fingerprint: signer.fingerprint, descriptor: signer.descriptor })) } private getUiMetadataFromDescriptor(ownedSigners: Array, keys: Array): UIMetadata { let uiMetadata = generateUiMetadata(this.descriptor, ownedSigners, this.toMiniscript); const keysWithOutPubkeys = uiMetadata.keys const keysWithPubkeys = keysWithOutPubkeys.map(key => { const keyWithPubkey = keys.find(k => k.fingerprint === key.fingerprint) if (keyWithPubkey) { return { ...key, pubkey: keyWithPubkey.pubkey } } return key } ) uiMetadata.keys = keysWithPubkeys this.generatedUiMetadata = uiMetadata; return uiMetadata; } sync(): Promise { if (!this.syncPromise) { this.syncPromise = this.wallet.sync() this.syncPromise .then(() => this.lastSyncTime = new Date()) .finally(() => this.syncPromise = undefined) } return this.syncPromise } async getBalance(): Promise { let balance = (await this.synced()).get_balance() return new Balance(balance) } async getNewAddress(): Promise { return (await this.synced()).get_new_address() } getPolicy(): Map { return this.wallet.get_policy() } async buildTrx({ address, amount, feeRate, policyPath, utxos, frozenUtxos }: { address: string, amount: string, feeRate: string, policyPath?: Map>, utxos?: Array, frozenUtxos?: Array }): Promise { return (await this.synced()).build_trx(address, amount, feeRate, policyPath, utxos, frozenUtxos) } async finalizeTrx(psbts: string[], broadcast: boolean): Promise { return this.wallet.finalize_trx(psbts, broadcast) } async getTrxs(): Promise> { const trxs = await (await this.synced()).get_trxs() return trxs.map(this.decorateTrxDetails) } async getTrx(txid: string): Promise { const trx = await (await this.synced()).get_trx(txid) return this.decorateTrxDetails(trx) } async getUtxos(): Promise> { return (await this.synced()).get_utxos() } getPolicyPathFromSigner(signer: BaseOwnedSigner): PolicyPathSelector | null { return this.wallet.get_policy_path_from_signer(signer) } async getPolicyPathsFromSigners(): Promise { const signers = await this.getOwnedSigners() return this.wallet.get_policy_paths_from_signers(signers) } searchUsedSigners(signers: Array): Array { return this.wallet.search_used_signers(signers) } async getLabeledUtxos(): Promise> { let utxos: Array = []; try { [utxos] = await Promise.all([ this.getUtxos(), this.getLabelsByPolicyId(this.id, {}) ]); } catch (error) { console.error("An error occurred:", error); return []; } const indexKey = "unhashed"; const frozenUtxos = await this.getFrozenUtxosOutpoints(); const maybeLabeledUtxos: Array = utxos.map(utxo => { const label: PublishedLabel | undefined = this.labelStore.get(utxo.address, indexKey) || this.labelStore.get(utxo.utxo.outpoint, indexKey); const frozen = frozenUtxos.includes(utxo.utxo.outpoint) ? true : false; if (label) { return { ...utxo, labelText: label.label.text, labelId: label.label_id, frozen }; } return { ...utxo, frozen }; }); return maybeLabeledUtxos; } async getFrozenUtxosOutpoints(): Promise { const policyId = this.id; const proposal = (await this.getProposalsByPolicyId(policyId, {})).get(policyId) as Array || []; const proposals = Array.isArray(proposal) ? proposal : [proposal]; const utxos = proposals.flatMap(proposal => proposal.utxos); return utxos; } async getUtxosOutpoints(): Promise { const utxos = await this.getUtxos() || []; const utxosOutpoints = utxos.map(utxo => utxo.utxo.outpoint); return utxosOutpoints; } async getFrozenBalance(): Promise { const utxos = await this.getLabeledUtxos(); const frozenUtxos = utxos.filter(utxo => utxo.frozen); const frozenBalance = frozenUtxos.reduce((accumulator, current) => accumulator + current.utxo.txout.value, 0); return frozenBalance; } getVaultData(): string { if (this.vaultData) { return this.vaultData } const vaultData = { description: this.description, descriptor: this.descriptor, name: this.name, publicKeys: this.nostrPublicKeys, } try { const vaultDataJSON = JSON.stringify(vaultData, null, 2) this.vaultData = vaultDataJSON return vaultDataJSON } catch (error) { throw new Error(`Error while parsing vault data: ${error}`) } } private decorateTrxDetails(trxDetails: any): any { trxDetails.net = trxDetails.received - trxDetails.sent if (trxDetails.confirmation_time) { trxDetails.confirmation_time.confirmedAt = fromNostrDate(trxDetails.confirmation_time.timestamp) } if (trxDetails.unconfirmed_last_seen != null) { trxDetails.unconfirmedLastSeenAt = fromNostrDate(trxDetails.unconfirmed_last_seen) } return trxDetails } private async synced(): Promise { if (this.requiresSync()) { await this.sync() } return this.wallet } private requiresSync(): boolean { return !this.lastSyncTime || this.lastSyncTime < TimeUtil.addMinutes(-1 * this.syncTimeGap) } private removeDuplicates(arr: any[]): any[] { return arr.reduce((accumulator: any[], current: any) => { const isDuplicate = accumulator.some(item => item.pubkey === current.pubkey && item.fingerprint === current.fingerprint && item.descriptor === current.descriptor ); if (!isDuplicate) { accumulator.push(current); } return accumulator; }, []); } }