@bsv/sdk 1.6.24 → 1.6.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist/cjs/package.json +1 -1
  2. package/dist/cjs/src/identity/ContactsManager.js +298 -0
  3. package/dist/cjs/src/identity/ContactsManager.js.map +1 -0
  4. package/dist/cjs/src/identity/IdentityClient.js +26 -0
  5. package/dist/cjs/src/identity/IdentityClient.js.map +1 -1
  6. package/dist/cjs/src/transaction/Transaction.js +3 -0
  7. package/dist/cjs/src/transaction/Transaction.js.map +1 -1
  8. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  9. package/dist/esm/src/identity/ContactsManager.js +295 -0
  10. package/dist/esm/src/identity/ContactsManager.js.map +1 -0
  11. package/dist/esm/src/identity/IdentityClient.js +27 -0
  12. package/dist/esm/src/identity/IdentityClient.js.map +1 -1
  13. package/dist/esm/src/transaction/Transaction.js +3 -0
  14. package/dist/esm/src/transaction/Transaction.js.map +1 -1
  15. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  16. package/dist/types/src/identity/ContactsManager.d.ts +28 -0
  17. package/dist/types/src/identity/ContactsManager.d.ts.map +1 -0
  18. package/dist/types/src/identity/IdentityClient.d.ts +21 -1
  19. package/dist/types/src/identity/IdentityClient.d.ts.map +1 -1
  20. package/dist/types/src/transaction/Transaction.d.ts.map +1 -1
  21. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  22. package/dist/umd/bundle.js +3 -3
  23. package/dist/umd/bundle.js.map +1 -1
  24. package/docs/reference/identity.md +144 -4
  25. package/package.json +1 -1
  26. package/src/identity/ContactsManager.ts +332 -0
  27. package/src/identity/IdentityClient.ts +31 -0
  28. package/src/identity/__tests/IdentityClient.test.ts +326 -2
  29. package/src/transaction/Transaction.ts +2 -0
@@ -48,6 +48,80 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
48
48
  ---
49
49
  ## Classes
50
50
 
51
+ | |
52
+ | --- |
53
+ | [ContactsManager](#class-contactsmanager) |
54
+ | [IdentityClient](#class-identityclient) |
55
+
56
+ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
57
+
58
+ ---
59
+
60
+ ### Class: ContactsManager
61
+
62
+ ```ts
63
+ export class ContactsManager {
64
+ constructor(wallet?: WalletInterface)
65
+ async getContacts(identityKey?: PubKeyHex, forceRefresh = false): Promise<Contact[]>
66
+ async saveContact(contact: DisplayableIdentity, metadata?: Record<string, any>): Promise<void>
67
+ async removeContact(identityKey: string): Promise<void>
68
+ }
69
+ ```
70
+
71
+ See also: [Contact](./identity.md#type-contact), [DisplayableIdentity](./identity.md#interface-displayableidentity), [PubKeyHex](./wallet.md#type-pubkeyhex), [WalletInterface](./wallet.md#interface-walletinterface)
72
+
73
+ #### Method getContacts
74
+
75
+ Load all records from the contacts basket
76
+
77
+ ```ts
78
+ async getContacts(identityKey?: PubKeyHex, forceRefresh = false): Promise<Contact[]>
79
+ ```
80
+ See also: [Contact](./identity.md#type-contact), [PubKeyHex](./wallet.md#type-pubkeyhex)
81
+
82
+ Returns
83
+
84
+ A promise that resolves with an array of contacts
85
+
86
+ Argument Details
87
+
88
+ + **identityKey**
89
+ + Optional specific identity key to fetch
90
+ + **forceRefresh**
91
+ + Whether to force a check for new contact data
92
+
93
+ #### Method removeContact
94
+
95
+ Remove a contact from the contacts basket
96
+
97
+ ```ts
98
+ async removeContact(identityKey: string): Promise<void>
99
+ ```
100
+
101
+ Argument Details
102
+
103
+ + **identityKey**
104
+ + The identity key of the contact to remove
105
+
106
+ #### Method saveContact
107
+
108
+ Save or update a Metanet contact
109
+
110
+ ```ts
111
+ async saveContact(contact: DisplayableIdentity, metadata?: Record<string, any>): Promise<void>
112
+ ```
113
+ See also: [DisplayableIdentity](./identity.md#interface-displayableidentity)
114
+
115
+ Argument Details
116
+
117
+ + **contact**
118
+ + The displayable identity information for the contact
119
+ + **metadata**
120
+ + Optional metadata to store with the contact (ex. notes, aliases, etc)
121
+
122
+ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
123
+
124
+ ---
51
125
  ### Class: IdentityClient
52
126
 
53
127
  IdentityClient lets you discover who others are, and let the world know who you are.
@@ -58,17 +132,42 @@ export class IdentityClient {
58
132
  async publiclyRevealAttributes(certificate: WalletCertificate, fieldsToReveal: CertificateFieldNameUnder50Bytes[]): Promise<BroadcastResponse | BroadcastFailure>
59
133
  async resolveByIdentityKey(args: DiscoverByIdentityKeyArgs): Promise<DisplayableIdentity[]>
60
134
  async resolveByAttributes(args: DiscoverByAttributesArgs): Promise<DisplayableIdentity[]>
135
+ public async getContacts(identityKey?: PubKeyHex, forceRefresh = false): Promise<Contact[]>
136
+ public async saveContact(contact: DisplayableIdentity, metadata?: Record<string, any>): Promise<void>
137
+ public async removeContact(identityKey: PubKeyHex): Promise<void>
61
138
  static parseIdentity(identityToParse: IdentityCertificate): DisplayableIdentity
62
139
  }
63
140
  ```
64
141
 
65
- See also: [BroadcastFailure](./transaction.md#interface-broadcastfailure), [BroadcastResponse](./transaction.md#interface-broadcastresponse), [CertificateFieldNameUnder50Bytes](./wallet.md#type-certificatefieldnameunder50bytes), [DEFAULT_IDENTITY_CLIENT_OPTIONS](./identity.md#variable-default_identity_client_options), [DiscoverByAttributesArgs](./wallet.md#interface-discoverbyattributesargs), [DiscoverByIdentityKeyArgs](./wallet.md#interface-discoverbyidentitykeyargs), [DisplayableIdentity](./identity.md#interface-displayableidentity), [IdentityCertificate](./wallet.md#interface-identitycertificate), [OriginatorDomainNameStringUnder250Bytes](./wallet.md#type-originatordomainnamestringunder250bytes), [WalletCertificate](./wallet.md#interface-walletcertificate), [WalletInterface](./wallet.md#interface-walletinterface)
142
+ See also: [BroadcastFailure](./transaction.md#interface-broadcastfailure), [BroadcastResponse](./transaction.md#interface-broadcastresponse), [CertificateFieldNameUnder50Bytes](./wallet.md#type-certificatefieldnameunder50bytes), [Contact](./identity.md#type-contact), [DEFAULT_IDENTITY_CLIENT_OPTIONS](./identity.md#variable-default_identity_client_options), [DiscoverByAttributesArgs](./wallet.md#interface-discoverbyattributesargs), [DiscoverByIdentityKeyArgs](./wallet.md#interface-discoverbyidentitykeyargs), [DisplayableIdentity](./identity.md#interface-displayableidentity), [IdentityCertificate](./wallet.md#interface-identitycertificate), [OriginatorDomainNameStringUnder250Bytes](./wallet.md#type-originatordomainnamestringunder250bytes), [PubKeyHex](./wallet.md#type-pubkeyhex), [WalletCertificate](./wallet.md#interface-walletcertificate), [WalletInterface](./wallet.md#interface-walletinterface)
66
143
 
67
- #### Method parseIdentity
144
+ #### Method getContacts
68
145
 
69
146
  TODO: Implement once revocation overlay is created
70
147
  Remove public certificate revelation from overlay services by spending the identity token
71
148
 
149
+ Load all records from the contacts basket
150
+
151
+ ```ts
152
+ public async getContacts(identityKey?: PubKeyHex, forceRefresh = false): Promise<Contact[]>
153
+ ```
154
+ See also: [Contact](./identity.md#type-contact), [PubKeyHex](./wallet.md#type-pubkeyhex)
155
+
156
+ Returns
157
+
158
+ A promise that resolves with an array of contacts
159
+
160
+ Argument Details
161
+
162
+ + **serialNumber**
163
+ + Unique serial number of the certificate to revoke revelation
164
+ + **identityKey**
165
+ + Optional specific identity key to fetch
166
+ + **forceRefresh**
167
+ + Whether to force a check for new contact data
168
+
169
+ #### Method parseIdentity
170
+
72
171
  Parse out identity and certifier attributes to display from an IdentityCertificate
73
172
 
74
173
  ```ts
@@ -82,8 +181,6 @@ Returns
82
181
 
83
182
  Argument Details
84
183
 
85
- + **serialNumber**
86
- + Unique serial number of the certificate to revoke revelation
87
184
  + **identityToParse**
88
185
  + The Identity Certificate to parse
89
186
 
@@ -112,6 +209,20 @@ Throws
112
209
 
113
210
  Throws an error if the certificate is invalid, the fields cannot be revealed, or if the broadcast fails.
114
211
 
212
+ #### Method removeContact
213
+
214
+ Remove a contact from the contacts basket
215
+
216
+ ```ts
217
+ public async removeContact(identityKey: PubKeyHex): Promise<void>
218
+ ```
219
+ See also: [PubKeyHex](./wallet.md#type-pubkeyhex)
220
+
221
+ Argument Details
222
+
223
+ + **identityKey**
224
+ + The identity key of the contact to remove
225
+
115
226
  #### Method resolveByAttributes
116
227
 
117
228
  Resolves displayable identity certificates by specific identity attributes, issued by a trusted entity.
@@ -148,6 +259,22 @@ Argument Details
148
259
  + **args**
149
260
  + Arguments for requesting the discovery based on the identity key.
150
261
 
262
+ #### Method saveContact
263
+
264
+ Save or update a Metanet contact
265
+
266
+ ```ts
267
+ public async saveContact(contact: DisplayableIdentity, metadata?: Record<string, any>): Promise<void>
268
+ ```
269
+ See also: [DisplayableIdentity](./identity.md#interface-displayableidentity)
270
+
271
+ Argument Details
272
+
273
+ + **contact**
274
+ + The displayable identity information for the contact
275
+ + **metadata**
276
+ + Optional metadata to store with the contact (ex. notes, aliases, etc)
277
+
151
278
  Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
152
279
 
153
280
  ---
@@ -155,6 +282,19 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
155
282
 
156
283
  ## Types
157
284
 
285
+ ### Type: Contact
286
+
287
+ ```ts
288
+ export type Contact = DisplayableIdentity & {
289
+ metadata?: Record<string, any>;
290
+ }
291
+ ```
292
+
293
+ See also: [DisplayableIdentity](./identity.md#interface-displayableidentity)
294
+
295
+ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
296
+
297
+ ---
158
298
  ## Enums
159
299
 
160
300
  ## Variables
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsv/sdk",
3
- "version": "1.6.24",
3
+ "version": "1.6.25",
4
4
  "type": "module",
5
5
  "description": "BSV Blockchain Software Development Kit",
6
6
  "main": "dist/cjs/mod.js",
@@ -0,0 +1,332 @@
1
+ import { PubKeyHex, WalletClient, WalletInterface, WalletProtocol } from '../wallet/index.js'
2
+ import { Utils, Random } from '../primitives/index.js'
3
+ import { DisplayableIdentity } from './types/index.js'
4
+ import { PushDrop } from '../script/index.js'
5
+ import { Transaction } from '../transaction/index.js'
6
+ export type Contact = DisplayableIdentity & { metadata?: Record<string, any> }
7
+
8
+ const CONTACT_PROTOCOL_ID: WalletProtocol = [2, 'contact']
9
+ // Local cache key for performance
10
+ const CONTACTS_CACHE_KEY = 'metanet-contacts'
11
+
12
+ export class ContactsManager {
13
+ private readonly wallet: WalletInterface
14
+
15
+ constructor (wallet?: WalletInterface) {
16
+ this.wallet = wallet ?? new WalletClient()
17
+ }
18
+
19
+ /**
20
+ * Load all records from the contacts basket
21
+ * @param identityKey Optional specific identity key to fetch
22
+ * @param forceRefresh Whether to force a check for new contact data
23
+ * @returns A promise that resolves with an array of contacts
24
+ */
25
+ async getContacts (identityKey?: PubKeyHex, forceRefresh = false): Promise<Contact[]> {
26
+ // Check localStorage cache first unless forcing refresh
27
+ if (!forceRefresh) {
28
+ const cached = localStorage.getItem(CONTACTS_CACHE_KEY)
29
+ if (cached != null && cached !== '') {
30
+ try {
31
+ const cachedContacts: Contact[] = JSON.parse(cached)
32
+ return identityKey != null
33
+ ? cachedContacts.filter(c => c.identityKey === identityKey)
34
+ : cachedContacts
35
+ } catch (e) {
36
+ console.warn('Invalid cached contacts JSON; will reload from chain', e)
37
+ }
38
+ }
39
+ }
40
+
41
+ const tags: string[] = []
42
+ if (identityKey != null) {
43
+ // Hash the identity key to use as a tag for quick lookup
44
+ const { hmac: hashedIdentityKey } = await this.wallet.createHmac({
45
+ protocolID: CONTACT_PROTOCOL_ID,
46
+ keyID: identityKey,
47
+ counterparty: 'self',
48
+ data: Utils.toArray(identityKey, 'utf8')
49
+ })
50
+ tags.push(`identityKey ${Utils.toHex(hashedIdentityKey)}`)
51
+ }
52
+
53
+ // Get all contact outputs from the contacts basket
54
+ const outputs = await this.wallet.listOutputs({
55
+ basket: 'contacts',
56
+ include: 'entire transactions',
57
+ tags
58
+ })
59
+
60
+ if (outputs.outputs == null || outputs.outputs.length === 0) {
61
+ localStorage.setItem(CONTACTS_CACHE_KEY, JSON.stringify([]))
62
+ return []
63
+ }
64
+
65
+ const contacts: Contact[] = []
66
+
67
+ // Process each contact output
68
+ for (const output of outputs.outputs) {
69
+ try {
70
+ const [txid, outputIndex] = output.outpoint.split('.')
71
+ const tx = Transaction.fromBEEF(outputs.BEEF as number[], txid)
72
+ const lockingScript = tx.outputs[Number(outputIndex)].lockingScript
73
+
74
+ // Decode the PushDrop data
75
+ const decoded = PushDrop.decode(lockingScript)
76
+ if (output.customInstructions == null) continue
77
+ const keyID = JSON.parse(output.customInstructions).keyID
78
+
79
+ // Decrypt the contact data
80
+ const { plaintext } = await this.wallet.decrypt({
81
+ ciphertext: decoded.fields[0],
82
+ protocolID: CONTACT_PROTOCOL_ID,
83
+ keyID,
84
+ counterparty: 'self'
85
+ })
86
+
87
+ // Parse the contact data
88
+ const contactData: Contact = JSON.parse(Utils.toUTF8(plaintext))
89
+
90
+ contacts.push(contactData)
91
+ } catch (error) {
92
+ console.warn('ContactsManager: Failed to decode contact output:', error)
93
+ // Skip this contact and continue with others
94
+ }
95
+ }
96
+
97
+ // Cache the loaded contacts
98
+ localStorage.setItem(CONTACTS_CACHE_KEY, JSON.stringify(contacts))
99
+ const filteredContacts = identityKey != null
100
+ ? contacts.filter(c => c.identityKey === identityKey)
101
+ : contacts
102
+ return filteredContacts
103
+ }
104
+
105
+ /**
106
+ * Save or update a Metanet contact
107
+ * @param contact The displayable identity information for the contact
108
+ * @param metadata Optional metadata to store with the contact (ex. notes, aliases, etc)
109
+ */
110
+ async saveContact (contact: DisplayableIdentity, metadata?: Record<string, any>): Promise<void> {
111
+ // Update localStorage cache immediately
112
+ const cached = localStorage.getItem(CONTACTS_CACHE_KEY)
113
+ const contacts: Contact[] = (cached != null && cached !== '') ? JSON.parse(cached) : []
114
+ const existingIndex = contacts.findIndex(c => c.identityKey === contact.identityKey)
115
+ const contactToStore: Contact = {
116
+ ...contact,
117
+ metadata
118
+ }
119
+
120
+ if (existingIndex >= 0) {
121
+ contacts[existingIndex] = contactToStore
122
+ } else {
123
+ contacts.push(contactToStore)
124
+ }
125
+ localStorage.setItem(CONTACTS_CACHE_KEY, JSON.stringify(contacts))
126
+
127
+ const { hmac: hashedIdentityKey } = await this.wallet.createHmac({
128
+ protocolID: CONTACT_PROTOCOL_ID,
129
+ keyID: contact.identityKey,
130
+ counterparty: 'self',
131
+ data: Utils.toArray(contact.identityKey, 'utf8')
132
+ })
133
+
134
+ // Check if this contact already exists (to update it)
135
+ const outputs = await this.wallet.listOutputs({
136
+ basket: 'contacts',
137
+ include: 'entire transactions',
138
+ includeCustomInstructions: true,
139
+ tags: [`identityKey ${Utils.toHex(hashedIdentityKey)}`]
140
+ })
141
+
142
+ let existingOutput: any = null
143
+ let keyID = Utils.toBase64(Random(32))
144
+ if (outputs.outputs != null) {
145
+ // Find output by trying to decrypt and checking identityKey in payload
146
+ for (const output of outputs.outputs) {
147
+ try {
148
+ const [txid, outputIndex] = output.outpoint.split('.')
149
+ const tx = Transaction.fromBEEF(outputs.BEEF as number[], txid)
150
+ const decoded = PushDrop.decode(tx.outputs[Number(outputIndex)].lockingScript)
151
+ if (output.customInstructions == null) continue
152
+ keyID = JSON.parse(output.customInstructions).keyID
153
+
154
+ const { plaintext } = await this.wallet.decrypt({
155
+ ciphertext: decoded.fields[0],
156
+ protocolID: CONTACT_PROTOCOL_ID,
157
+ keyID,
158
+ counterparty: 'self'
159
+ })
160
+
161
+ const storedContact: Contact = JSON.parse(Utils.toUTF8(plaintext))
162
+ if (storedContact.identityKey === contact.identityKey) {
163
+ // Found the right output
164
+ existingOutput = output
165
+ break
166
+ }
167
+ } catch (e) {
168
+ // Skip malformed or undecryptable outputs
169
+ }
170
+ }
171
+ }
172
+
173
+ // Encrypt the contact data directly
174
+ const contactWithMetadata: Contact = {
175
+ ...contact,
176
+ metadata
177
+ }
178
+ const { ciphertext } = await this.wallet.encrypt({
179
+ plaintext: Utils.toArray(JSON.stringify(contactWithMetadata), 'utf8'),
180
+ protocolID: CONTACT_PROTOCOL_ID,
181
+ keyID,
182
+ counterparty: 'self'
183
+ })
184
+
185
+ // Create locking script for the new contact token
186
+ const lockingScript = await new PushDrop(this.wallet).lock(
187
+ [ciphertext],
188
+ CONTACT_PROTOCOL_ID,
189
+ keyID,
190
+ 'self'
191
+ )
192
+
193
+ if (existingOutput != null) {
194
+ // Update existing contact by spending its output
195
+ const [txid, outputIndex] = String(existingOutput.outpoint).split('.')
196
+ const prevOutpoint = `${txid}.${outputIndex}` as const
197
+
198
+ const pushdrop = new PushDrop(this.wallet)
199
+ const { signableTransaction } = await this.wallet.createAction({
200
+ description: 'Update Contact',
201
+ inputBEEF: outputs.BEEF as number[],
202
+ inputs: [{
203
+ outpoint: prevOutpoint,
204
+ unlockingScriptLength: 74,
205
+ inputDescription: 'Spend previous contact output'
206
+ }],
207
+ outputs: [{
208
+ basket: 'contacts',
209
+ satoshis: 1,
210
+ lockingScript: lockingScript.toHex(),
211
+ outputDescription: `Updated Contact: ${contact.name ?? contact.identityKey.slice(0, 10)}`,
212
+ tags: [`identityKey ${Utils.toHex(hashedIdentityKey)}`],
213
+ customInstructions: JSON.stringify({ keyID })
214
+ }],
215
+ options: { acceptDelayedBroadcast: false, randomizeOutputs: false } // TODO: Support custom config as needed.
216
+ })
217
+
218
+ if (signableTransaction == null) throw new Error('Unable to update contact')
219
+
220
+ const unlocker = pushdrop.unlock(CONTACT_PROTOCOL_ID, keyID, 'self')
221
+ const unlockingScript = await unlocker.sign(
222
+ Transaction.fromBEEF(signableTransaction.tx),
223
+ 0
224
+ )
225
+
226
+ const { tx } = await this.wallet.signAction({
227
+ reference: signableTransaction.reference,
228
+ spends: { 0: { unlockingScript: unlockingScript.toHex() } }
229
+ })
230
+
231
+ if (tx == null) throw new Error('Failed to update contact output')
232
+ } else {
233
+ // Create new contact output
234
+ const { tx } = await this.wallet.createAction({
235
+ description: 'Add Contact',
236
+ outputs: [{
237
+ basket: 'contacts',
238
+ satoshis: 1,
239
+ lockingScript: lockingScript.toHex(),
240
+ outputDescription: `Contact: ${contact.name ?? contact.identityKey.slice(0, 10)}`,
241
+ tags: [`identityKey ${Utils.toHex(hashedIdentityKey)}`],
242
+ customInstructions: JSON.stringify({ keyID })
243
+ }],
244
+ options: { acceptDelayedBroadcast: false, randomizeOutputs: false } // TODO: Support custom config as needed.
245
+ })
246
+
247
+ if (tx == null) throw new Error('Failed to create contact output')
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Remove a contact from the contacts basket
253
+ * @param identityKey The identity key of the contact to remove
254
+ */
255
+ async removeContact (identityKey: string): Promise<void> {
256
+ // Update localStorage cache
257
+ const cached = localStorage.getItem(CONTACTS_CACHE_KEY)
258
+ if (cached != null && cached !== '') {
259
+ try {
260
+ const contacts: Contact[] = JSON.parse(cached)
261
+ const filteredContacts = contacts.filter(c => c.identityKey !== identityKey)
262
+ localStorage.setItem(CONTACTS_CACHE_KEY, JSON.stringify(filteredContacts))
263
+ } catch (e) {
264
+ console.warn('Failed to update cache after contact removal:', e)
265
+ }
266
+ }
267
+
268
+ // Find and spend the contact's output
269
+ const outputs = await this.wallet.listOutputs({
270
+ basket: 'contacts',
271
+ include: 'entire transactions',
272
+ includeCustomInstructions: true
273
+ })
274
+
275
+ if (outputs.outputs == null) return
276
+
277
+ // Find the output for this specific contact by decrypting and checking identityKey
278
+ for (const output of outputs.outputs) {
279
+ try {
280
+ const [txid, outputIndex] = String(output.outpoint).split('.')
281
+ const tx = Transaction.fromBEEF(outputs.BEEF as number[], txid)
282
+ const decoded = PushDrop.decode(tx.outputs[Number(outputIndex)].lockingScript)
283
+ if (output.customInstructions == null) continue
284
+ const keyID = JSON.parse(output.customInstructions).keyID
285
+
286
+ const { plaintext } = await this.wallet.decrypt({
287
+ ciphertext: decoded.fields[0],
288
+ protocolID: CONTACT_PROTOCOL_ID,
289
+ keyID,
290
+ counterparty: 'self'
291
+ })
292
+
293
+ const storedContact: Contact = JSON.parse(Utils.toUTF8(plaintext))
294
+ if (storedContact.identityKey === identityKey) {
295
+ // Found the contact's output, spend it without creating a new one
296
+ const prevOutpoint = `${txid}.${outputIndex}` as const
297
+
298
+ const pushdrop = new PushDrop(this.wallet)
299
+ const { signableTransaction } = await this.wallet.createAction({
300
+ description: 'Delete Contact',
301
+ inputBEEF: outputs.BEEF as number[],
302
+ inputs: [{
303
+ outpoint: prevOutpoint,
304
+ unlockingScriptLength: 74,
305
+ inputDescription: 'Spend contact output to delete'
306
+ }],
307
+ outputs: [], // No outputs = deletion
308
+ options: { acceptDelayedBroadcast: false, randomizeOutputs: false } // TODO: Support custom config as needed.
309
+ })
310
+
311
+ if (signableTransaction == null) throw new Error('Unable to delete contact')
312
+
313
+ const unlocker = pushdrop.unlock(CONTACT_PROTOCOL_ID, keyID, 'self')
314
+ const unlockingScript = await unlocker.sign(
315
+ Transaction.fromBEEF(signableTransaction.tx),
316
+ 0
317
+ )
318
+
319
+ const { tx: deleteTx } = await this.wallet.signAction({
320
+ reference: signableTransaction.reference,
321
+ spends: { 0: { unlockingScript: unlockingScript.toHex() } }
322
+ })
323
+
324
+ if (deleteTx == null) throw new Error('Failed to delete contact output')
325
+ return
326
+ }
327
+ } catch (e) {
328
+ // Skip malformed or undecryptable outputs
329
+ }
330
+ }
331
+ }
332
+ }
@@ -6,6 +6,7 @@ import {
6
6
  DiscoverByIdentityKeyArgs,
7
7
  IdentityCertificate,
8
8
  OriginatorDomainNameStringUnder250Bytes,
9
+ PubKeyHex,
9
10
  WalletCertificate,
10
11
  WalletClient,
11
12
  WalletInterface
@@ -15,6 +16,7 @@ import Certificate from '../auth/certificates/Certificate.js'
15
16
  import { PushDrop } from '../script/index.js'
16
17
  import { PrivateKey, Utils } from '../primitives/index.js'
17
18
  import { TopicBroadcaster } from '../overlay-tools/index.js'
19
+ import { ContactsManager, Contact } from './ContactsManager.js'
18
20
 
19
21
  /**
20
22
  * IdentityClient lets you discover who others are, and let the world know who you are.
@@ -22,6 +24,7 @@ import { TopicBroadcaster } from '../overlay-tools/index.js'
22
24
  export class IdentityClient {
23
25
  private readonly authClient: AuthFetch
24
26
  private readonly wallet: WalletInterface
27
+ private readonly contactsManager: ContactsManager
25
28
  constructor (
26
29
  wallet?: WalletInterface,
27
30
  private readonly options = DEFAULT_IDENTITY_CLIENT_OPTIONS,
@@ -29,6 +32,7 @@ export class IdentityClient {
29
32
  ) {
30
33
  this.wallet = wallet ?? new WalletClient()
31
34
  this.authClient = new AuthFetch(this.wallet)
35
+ this.contactsManager = new ContactsManager(this.wallet)
32
36
  }
33
37
 
34
38
  /**
@@ -213,6 +217,33 @@ export class IdentityClient {
213
217
  // return await broadcaster.broadcast(Transaction.fromAtomicBEEF(signedTx as number[]))
214
218
  // }
215
219
 
220
+ /**
221
+ * Load all records from the contacts basket
222
+ * @param identityKey Optional specific identity key to fetch
223
+ * @param forceRefresh Whether to force a check for new contact data
224
+ * @returns A promise that resolves with an array of contacts
225
+ */
226
+ public async getContacts (identityKey?: PubKeyHex, forceRefresh = false): Promise<Contact[]> {
227
+ return await this.contactsManager.getContacts(identityKey, forceRefresh)
228
+ }
229
+
230
+ /**
231
+ * Save or update a Metanet contact
232
+ * @param contact The displayable identity information for the contact
233
+ * @param metadata Optional metadata to store with the contact (ex. notes, aliases, etc)
234
+ */
235
+ public async saveContact (contact: DisplayableIdentity, metadata?: Record<string, any>): Promise<void> {
236
+ return await this.contactsManager.saveContact(contact, metadata)
237
+ }
238
+
239
+ /**
240
+ * Remove a contact from the contacts basket
241
+ * @param identityKey The identity key of the contact to remove
242
+ */
243
+ public async removeContact (identityKey: PubKeyHex): Promise<void> {
244
+ return await this.contactsManager.removeContact(identityKey)
245
+ }
246
+
216
247
  /**
217
248
  * Parse out identity and certifier attributes to display from an IdentityCertificate
218
249
  * @param identityToParse - The Identity Certificate to parse