@bsv/sdk 1.6.24 → 1.6.26
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.
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/src/identity/ContactsManager.js +334 -0
- package/dist/cjs/src/identity/ContactsManager.js.map +1 -0
- package/dist/cjs/src/identity/IdentityClient.js +26 -0
- package/dist/cjs/src/identity/IdentityClient.js.map +1 -1
- package/dist/cjs/src/transaction/Transaction.js +3 -0
- package/dist/cjs/src/transaction/Transaction.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/identity/ContactsManager.js +329 -0
- package/dist/esm/src/identity/ContactsManager.js.map +1 -0
- package/dist/esm/src/identity/IdentityClient.js +27 -0
- package/dist/esm/src/identity/IdentityClient.js.map +1 -1
- package/dist/esm/src/transaction/Transaction.js +3 -0
- package/dist/esm/src/transaction/Transaction.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/identity/ContactsManager.d.ts +30 -0
- package/dist/types/src/identity/ContactsManager.d.ts.map +1 -0
- package/dist/types/src/identity/IdentityClient.d.ts +21 -1
- package/dist/types/src/identity/IdentityClient.d.ts.map +1 -1
- package/dist/types/src/transaction/Transaction.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +3 -3
- package/dist/umd/bundle.js.map +1 -1
- package/docs/reference/identity.md +144 -4
- package/package.json +1 -1
- package/src/identity/ContactsManager.ts +372 -0
- package/src/identity/IdentityClient.ts +31 -0
- package/src/identity/__tests/IdentityClient.test.ts +360 -2
- 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
|
|
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
|
@@ -0,0 +1,372 @@
|
|
|
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
|
+
|
|
10
|
+
// In-memory cache for cross-platform compatibility
|
|
11
|
+
class MemoryCache {
|
|
12
|
+
private readonly cache = new Map<string, string>()
|
|
13
|
+
|
|
14
|
+
getItem (key: string): string | null {
|
|
15
|
+
return this.cache.get(key) ?? null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
setItem (key: string, value: string): void {
|
|
19
|
+
this.cache.set(key, value)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
removeItem (key: string): void {
|
|
23
|
+
this.cache.delete(key)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
clear (): void {
|
|
27
|
+
this.cache.clear()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class ContactsManager {
|
|
32
|
+
private readonly wallet: WalletInterface
|
|
33
|
+
private readonly cache = new MemoryCache()
|
|
34
|
+
private readonly CONTACTS_CACHE_KEY = 'metanet-contacts'
|
|
35
|
+
|
|
36
|
+
constructor (wallet?: WalletInterface) {
|
|
37
|
+
this.wallet = wallet ?? new WalletClient()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Load all records from the contacts basket
|
|
42
|
+
* @param identityKey Optional specific identity key to fetch
|
|
43
|
+
* @param forceRefresh Whether to force a check for new contact data
|
|
44
|
+
* @returns A promise that resolves with an array of contacts
|
|
45
|
+
*/
|
|
46
|
+
async getContacts (identityKey?: PubKeyHex, forceRefresh = false): Promise<Contact[]> {
|
|
47
|
+
// Check in-memory cache first unless forcing refresh
|
|
48
|
+
if (!forceRefresh) {
|
|
49
|
+
const cached = this.cache.getItem(this.CONTACTS_CACHE_KEY)
|
|
50
|
+
if (cached != null && cached !== '') {
|
|
51
|
+
try {
|
|
52
|
+
const cachedContacts: Contact[] = JSON.parse(cached)
|
|
53
|
+
return identityKey != null
|
|
54
|
+
? cachedContacts.filter(c => c.identityKey === identityKey)
|
|
55
|
+
: cachedContacts
|
|
56
|
+
} catch (e) {
|
|
57
|
+
console.warn('Invalid cached contacts JSON; will reload from chain', e)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const tags: string[] = []
|
|
63
|
+
if (identityKey != null) {
|
|
64
|
+
// Hash the identity key to use as a tag for quick lookup
|
|
65
|
+
const { hmac: hashedIdentityKey } = await this.wallet.createHmac({
|
|
66
|
+
protocolID: CONTACT_PROTOCOL_ID,
|
|
67
|
+
keyID: identityKey,
|
|
68
|
+
counterparty: 'self',
|
|
69
|
+
data: Utils.toArray(identityKey, 'utf8')
|
|
70
|
+
})
|
|
71
|
+
tags.push(`identityKey ${Utils.toHex(hashedIdentityKey)}`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Get all contact outputs from the contacts basket
|
|
75
|
+
const outputs = await this.wallet.listOutputs({
|
|
76
|
+
basket: 'contacts',
|
|
77
|
+
include: 'entire transactions',
|
|
78
|
+
includeCustomInstructions: true,
|
|
79
|
+
tags
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
if (outputs.outputs == null || outputs.outputs.length === 0) {
|
|
83
|
+
this.cache.setItem(this.CONTACTS_CACHE_KEY, JSON.stringify([]))
|
|
84
|
+
return []
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const contacts: Contact[] = []
|
|
88
|
+
|
|
89
|
+
// Process each contact output
|
|
90
|
+
for (const output of outputs.outputs) {
|
|
91
|
+
try {
|
|
92
|
+
const [txid, outputIndex] = output.outpoint.split('.')
|
|
93
|
+
const tx = Transaction.fromBEEF(outputs.BEEF as number[], txid)
|
|
94
|
+
const lockingScript = tx.outputs[Number(outputIndex)].lockingScript
|
|
95
|
+
|
|
96
|
+
// Decode the PushDrop data
|
|
97
|
+
const decoded = PushDrop.decode(lockingScript)
|
|
98
|
+
if (output.customInstructions == null) continue
|
|
99
|
+
const keyID = JSON.parse(output.customInstructions).keyID
|
|
100
|
+
|
|
101
|
+
// Decrypt the contact data
|
|
102
|
+
const { plaintext } = await this.wallet.decrypt({
|
|
103
|
+
ciphertext: decoded.fields[0],
|
|
104
|
+
protocolID: CONTACT_PROTOCOL_ID,
|
|
105
|
+
keyID,
|
|
106
|
+
counterparty: 'self'
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
// Parse the contact data
|
|
110
|
+
const contactData: Contact = JSON.parse(Utils.toUTF8(plaintext))
|
|
111
|
+
|
|
112
|
+
contacts.push(contactData)
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.warn('ContactsManager: Failed to decode contact output:', error)
|
|
115
|
+
// Skip this contact and continue with others
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Cache the loaded contacts
|
|
120
|
+
this.cache.setItem(this.CONTACTS_CACHE_KEY, JSON.stringify(contacts))
|
|
121
|
+
const filteredContacts = identityKey != null
|
|
122
|
+
? contacts.filter(c => c.identityKey === identityKey)
|
|
123
|
+
: contacts
|
|
124
|
+
return filteredContacts
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Save or update a Metanet contact
|
|
129
|
+
* @param contact The displayable identity information for the contact
|
|
130
|
+
* @param metadata Optional metadata to store with the contact (ex. notes, aliases, etc)
|
|
131
|
+
*/
|
|
132
|
+
async saveContact (contact: DisplayableIdentity, metadata?: Record<string, any>): Promise<void> {
|
|
133
|
+
// Get current contacts from cache or blockchain
|
|
134
|
+
const cached = this.cache.getItem(this.CONTACTS_CACHE_KEY)
|
|
135
|
+
let contacts: Contact[]
|
|
136
|
+
if (cached != null && cached !== '') {
|
|
137
|
+
contacts = JSON.parse(cached)
|
|
138
|
+
} else {
|
|
139
|
+
// If cache is empty, get current data from blockchain
|
|
140
|
+
contacts = await this.getContacts()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const existingIndex = contacts.findIndex(c => c.identityKey === contact.identityKey)
|
|
144
|
+
const contactToStore: Contact = {
|
|
145
|
+
...contact,
|
|
146
|
+
metadata
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (existingIndex >= 0) {
|
|
150
|
+
contacts[existingIndex] = contactToStore
|
|
151
|
+
} else {
|
|
152
|
+
contacts.push(contactToStore)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const { hmac: hashedIdentityKey } = await this.wallet.createHmac({
|
|
156
|
+
protocolID: CONTACT_PROTOCOL_ID,
|
|
157
|
+
keyID: contact.identityKey,
|
|
158
|
+
counterparty: 'self',
|
|
159
|
+
data: Utils.toArray(contact.identityKey, 'utf8')
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// Check if this contact already exists (to update it)
|
|
163
|
+
const outputs = await this.wallet.listOutputs({
|
|
164
|
+
basket: 'contacts',
|
|
165
|
+
include: 'entire transactions',
|
|
166
|
+
includeCustomInstructions: true,
|
|
167
|
+
tags: [`identityKey ${Utils.toHex(hashedIdentityKey)}`]
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
let existingOutput: any = null
|
|
171
|
+
let keyID = Utils.toBase64(Random(32))
|
|
172
|
+
if (outputs.outputs != null) {
|
|
173
|
+
// Find output by trying to decrypt and checking identityKey in payload
|
|
174
|
+
for (const output of outputs.outputs) {
|
|
175
|
+
try {
|
|
176
|
+
const [txid, outputIndex] = output.outpoint.split('.')
|
|
177
|
+
const tx = Transaction.fromBEEF(outputs.BEEF as number[], txid)
|
|
178
|
+
const decoded = PushDrop.decode(tx.outputs[Number(outputIndex)].lockingScript)
|
|
179
|
+
if (output.customInstructions == null) continue
|
|
180
|
+
keyID = JSON.parse(output.customInstructions).keyID
|
|
181
|
+
|
|
182
|
+
const { plaintext } = await this.wallet.decrypt({
|
|
183
|
+
ciphertext: decoded.fields[0],
|
|
184
|
+
protocolID: CONTACT_PROTOCOL_ID,
|
|
185
|
+
keyID,
|
|
186
|
+
counterparty: 'self'
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
const storedContact: Contact = JSON.parse(Utils.toUTF8(plaintext))
|
|
190
|
+
if (storedContact.identityKey === contact.identityKey) {
|
|
191
|
+
// Found the right output
|
|
192
|
+
existingOutput = output
|
|
193
|
+
break
|
|
194
|
+
}
|
|
195
|
+
} catch (e) {
|
|
196
|
+
// Skip malformed or undecryptable outputs
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Encrypt the contact data directly
|
|
202
|
+
const contactWithMetadata: Contact = {
|
|
203
|
+
...contact,
|
|
204
|
+
metadata
|
|
205
|
+
}
|
|
206
|
+
const { ciphertext } = await this.wallet.encrypt({
|
|
207
|
+
plaintext: Utils.toArray(JSON.stringify(contactWithMetadata), 'utf8'),
|
|
208
|
+
protocolID: CONTACT_PROTOCOL_ID,
|
|
209
|
+
keyID,
|
|
210
|
+
counterparty: 'self'
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
// Create locking script for the new contact token
|
|
214
|
+
const lockingScript = await new PushDrop(this.wallet).lock(
|
|
215
|
+
[ciphertext],
|
|
216
|
+
CONTACT_PROTOCOL_ID,
|
|
217
|
+
keyID,
|
|
218
|
+
'self'
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
if (existingOutput != null) {
|
|
222
|
+
// Update existing contact by spending its output
|
|
223
|
+
const [txid, outputIndex] = String(existingOutput.outpoint).split('.')
|
|
224
|
+
const prevOutpoint = `${txid}.${outputIndex}` as const
|
|
225
|
+
|
|
226
|
+
const pushdrop = new PushDrop(this.wallet)
|
|
227
|
+
const { signableTransaction } = await this.wallet.createAction({
|
|
228
|
+
description: 'Update Contact',
|
|
229
|
+
inputBEEF: outputs.BEEF as number[],
|
|
230
|
+
inputs: [{
|
|
231
|
+
outpoint: prevOutpoint,
|
|
232
|
+
unlockingScriptLength: 74,
|
|
233
|
+
inputDescription: 'Spend previous contact output'
|
|
234
|
+
}],
|
|
235
|
+
outputs: [{
|
|
236
|
+
basket: 'contacts',
|
|
237
|
+
satoshis: 1,
|
|
238
|
+
lockingScript: lockingScript.toHex(),
|
|
239
|
+
outputDescription: `Updated Contact: ${contact.name ?? contact.identityKey.slice(0, 10)}`,
|
|
240
|
+
tags: [`identityKey ${Utils.toHex(hashedIdentityKey)}`],
|
|
241
|
+
customInstructions: JSON.stringify({ keyID })
|
|
242
|
+
}],
|
|
243
|
+
options: { acceptDelayedBroadcast: false, randomizeOutputs: false } // TODO: Support custom config as needed.
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
if (signableTransaction == null) throw new Error('Unable to update contact')
|
|
247
|
+
|
|
248
|
+
const unlocker = pushdrop.unlock(CONTACT_PROTOCOL_ID, keyID, 'self')
|
|
249
|
+
const unlockingScript = await unlocker.sign(
|
|
250
|
+
Transaction.fromBEEF(signableTransaction.tx),
|
|
251
|
+
0
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
const { tx } = await this.wallet.signAction({
|
|
255
|
+
reference: signableTransaction.reference,
|
|
256
|
+
spends: { 0: { unlockingScript: unlockingScript.toHex() } }
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
if (tx == null) throw new Error('Failed to update contact output')
|
|
260
|
+
} else {
|
|
261
|
+
// Create new contact output
|
|
262
|
+
const { tx } = await this.wallet.createAction({
|
|
263
|
+
description: 'Add Contact',
|
|
264
|
+
outputs: [{
|
|
265
|
+
basket: 'contacts',
|
|
266
|
+
satoshis: 1,
|
|
267
|
+
lockingScript: lockingScript.toHex(),
|
|
268
|
+
outputDescription: `Contact: ${contact.name ?? contact.identityKey.slice(0, 10)}`,
|
|
269
|
+
tags: [`identityKey ${Utils.toHex(hashedIdentityKey)}`],
|
|
270
|
+
customInstructions: JSON.stringify({ keyID })
|
|
271
|
+
}],
|
|
272
|
+
options: { acceptDelayedBroadcast: false, randomizeOutputs: false } // TODO: Support custom config as needed.
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
if (tx == null) throw new Error('Failed to create contact output')
|
|
276
|
+
}
|
|
277
|
+
this.cache.setItem(this.CONTACTS_CACHE_KEY, JSON.stringify(contacts))
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Remove a contact from the contacts basket
|
|
282
|
+
* @param identityKey The identity key of the contact to remove
|
|
283
|
+
*/
|
|
284
|
+
async removeContact (identityKey: string): Promise<void> {
|
|
285
|
+
// Update in-memory cache
|
|
286
|
+
const cached = this.cache.getItem(this.CONTACTS_CACHE_KEY)
|
|
287
|
+
if (cached != null && cached !== '') {
|
|
288
|
+
try {
|
|
289
|
+
const contacts: Contact[] = JSON.parse(cached)
|
|
290
|
+
const filteredContacts = contacts.filter(c => c.identityKey !== identityKey)
|
|
291
|
+
this.cache.setItem(this.CONTACTS_CACHE_KEY, JSON.stringify(filteredContacts))
|
|
292
|
+
} catch (e) {
|
|
293
|
+
console.warn('Failed to update cache after contact removal:', e)
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Hash the identity key to use as a tag for quick lookup
|
|
298
|
+
const tags: string[] = []
|
|
299
|
+
const { hmac: hashedIdentityKey } = await this.wallet.createHmac({
|
|
300
|
+
protocolID: CONTACT_PROTOCOL_ID,
|
|
301
|
+
keyID: identityKey,
|
|
302
|
+
counterparty: 'self',
|
|
303
|
+
data: Utils.toArray(identityKey, 'utf8')
|
|
304
|
+
})
|
|
305
|
+
tags.push(`identityKey ${Utils.toHex(hashedIdentityKey)}`)
|
|
306
|
+
|
|
307
|
+
// Find and spend the contact's output
|
|
308
|
+
const outputs = await this.wallet.listOutputs({
|
|
309
|
+
basket: 'contacts',
|
|
310
|
+
include: 'entire transactions',
|
|
311
|
+
includeCustomInstructions: true,
|
|
312
|
+
tags
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
if (outputs.outputs == null) return
|
|
316
|
+
|
|
317
|
+
// Find the output for this specific contact by decrypting and checking identityKey
|
|
318
|
+
for (const output of outputs.outputs) {
|
|
319
|
+
try {
|
|
320
|
+
const [txid, outputIndex] = String(output.outpoint).split('.')
|
|
321
|
+
const tx = Transaction.fromBEEF(outputs.BEEF as number[], txid)
|
|
322
|
+
const decoded = PushDrop.decode(tx.outputs[Number(outputIndex)].lockingScript)
|
|
323
|
+
if (output.customInstructions == null) continue
|
|
324
|
+
const keyID = JSON.parse(output.customInstructions).keyID
|
|
325
|
+
|
|
326
|
+
const { plaintext } = await this.wallet.decrypt({
|
|
327
|
+
ciphertext: decoded.fields[0],
|
|
328
|
+
protocolID: CONTACT_PROTOCOL_ID,
|
|
329
|
+
keyID,
|
|
330
|
+
counterparty: 'self'
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
const storedContact: Contact = JSON.parse(Utils.toUTF8(plaintext))
|
|
334
|
+
if (storedContact.identityKey === identityKey) {
|
|
335
|
+
// Found the contact's output, spend it without creating a new one
|
|
336
|
+
const prevOutpoint = `${txid}.${outputIndex}` as const
|
|
337
|
+
|
|
338
|
+
const pushdrop = new PushDrop(this.wallet)
|
|
339
|
+
const { signableTransaction } = await this.wallet.createAction({
|
|
340
|
+
description: 'Delete Contact',
|
|
341
|
+
inputBEEF: outputs.BEEF as number[],
|
|
342
|
+
inputs: [{
|
|
343
|
+
outpoint: prevOutpoint,
|
|
344
|
+
unlockingScriptLength: 74,
|
|
345
|
+
inputDescription: 'Spend contact output to delete'
|
|
346
|
+
}],
|
|
347
|
+
outputs: [], // No outputs = deletion
|
|
348
|
+
options: { acceptDelayedBroadcast: false, randomizeOutputs: false } // TODO: Support custom config as needed.
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
if (signableTransaction == null) throw new Error('Unable to delete contact')
|
|
352
|
+
|
|
353
|
+
const unlocker = pushdrop.unlock(CONTACT_PROTOCOL_ID, keyID, 'self')
|
|
354
|
+
const unlockingScript = await unlocker.sign(
|
|
355
|
+
Transaction.fromBEEF(signableTransaction.tx),
|
|
356
|
+
0
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
const { tx: deleteTx } = await this.wallet.signAction({
|
|
360
|
+
reference: signableTransaction.reference,
|
|
361
|
+
spends: { 0: { unlockingScript: unlockingScript.toHex() } }
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
if (deleteTx == null) throw new Error('Failed to delete contact output')
|
|
365
|
+
return
|
|
366
|
+
}
|
|
367
|
+
} catch (e) {
|
|
368
|
+
// Skip malformed or undecryptable outputs
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
@@ -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
|