psffpp 1.0.1

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/index.js ADDED
@@ -0,0 +1,329 @@
1
+ /*
2
+ This library implement PS009 specification:
3
+ https://github.com/Permissionless-Software-Foundation/specifications/blob/master/ps009-multisig-approval.md
4
+ */
5
+
6
+ // global libraries
7
+ const bitcore = require('bitcore-lib-cash')
8
+ const axios = require('axios')
9
+
10
+ // Local libraries
11
+ const NFTs = require('./lib/nfts')
12
+ const UtilLib = require('./lib/util')
13
+
14
+ class MultisigApproval {
15
+ constructor (localConfig = {}) {
16
+ // Dependency Injection
17
+ this.wallet = localConfig.wallet
18
+ if (!this.wallet) {
19
+ throw new Error('Instance of minimal-slp-wallet must be passed in as a property called \'wallet\', when initializing the psf-multisig-approval library.')
20
+ }
21
+
22
+ // The default IPFS gateway can be overwritten by the user when this library
23
+ // is instantiated.
24
+ this.ipfsGateway = localConfig.ipfsGateway
25
+ if (!this.ipfsGateway) {
26
+ this.ipfsGateway = 'https://p2wdb-gateway-678.fullstack.cash'
27
+ }
28
+
29
+ // Encapsulate dependencies
30
+ this.bchjs = this.wallet.bchjs
31
+ this.bitcore = bitcore
32
+ this.axios = axios
33
+ this.nfts = new NFTs(localConfig)
34
+ this.util = new UtilLib(localConfig)
35
+
36
+ // Bind the this object to all subfunctions in this class
37
+ this.getNftHolderInfo = this.getNftHolderInfo.bind(this)
38
+ this.createMultisigAddress = this.createMultisigAddress.bind(this)
39
+ this.getApprovalTx = this.getApprovalTx.bind(this)
40
+
41
+ // Create a transaction details cache, to reduce the number of API calls.
42
+ this.txCache = {}
43
+ }
44
+
45
+ // This function retrieves the NFTs associated with a Group token ID. It then
46
+ // tries to retrieve the BCH addresses and public keys of the holders of those
47
+ // NFTs. It returns an object with all that information in it.
48
+ // The function defaults to the PSF Minting Council, but any Group token ID
49
+ // can be used.
50
+ async getNftHolderInfo (groupTokenId = '8e8d90ebdb1791d58eba7acd428ff3b1e21c47fb7aba2ba3b5b815aa0fe7d6d5') {
51
+ try {
52
+ // console.log('groupTokenId: ', groupTokenId)
53
+
54
+ const nfts = await this.nfts.getNftsFromGroup()
55
+ // console.log('getNftHolderInfo() nfts: ', nfts)
56
+
57
+ const addrs = await this.nfts.getAddrsFromNfts(nfts)
58
+ // console.log('getNftHolderInfo() addrs: ', addrs)
59
+
60
+ const { keys, keysNotFound } = await this.nfts.findKeys(addrs, nfts)
61
+
62
+ return { keys, keysNotFound }
63
+ } catch (err) {
64
+ console.error('Error in getNftHolderInfo()')
65
+ throw err
66
+ }
67
+ }
68
+
69
+ // Generate a P2SH multisignature wallet from the public keys of the NFT holders.
70
+ // The address for the wallet is returned.
71
+ // The input for this function should be the `keys` output from
72
+ // getNftHolderInfo()
73
+ createMultisigAddress (inObj = {}) {
74
+ try {
75
+ const { keys } = inObj
76
+ let requiredSigners = inObj.requiredSigners
77
+
78
+ // Input validation
79
+ if (!Array.isArray(keys)) {
80
+ throw new Error('keys must be an array containing public keys')
81
+ }
82
+
83
+ // Isolate just an array of public keys.
84
+ const pubKeys = []
85
+ for (let i = 0; i < keys.length; i++) {
86
+ const thisPair = keys[i]
87
+
88
+ pubKeys.push(thisPair.pubKey)
89
+ }
90
+
91
+ // If the number of required signers is not specified, then default to
92
+ // a 50% + 1 threashold.
93
+ if (!requiredSigners) {
94
+ requiredSigners = Math.floor(pubKeys.length / 2) + 1
95
+ }
96
+
97
+ // Multisig Address
98
+ const msAddr = new bitcore.Address(pubKeys, requiredSigners)
99
+
100
+ // Locking Script in hex representation.
101
+ const scriptHex = new bitcore.Script(msAddr).toHex()
102
+
103
+ const walletObj = {
104
+ address: msAddr.toString(),
105
+ scriptHex,
106
+ publicKeys: pubKeys,
107
+ requiredSigners
108
+ }
109
+
110
+ return walletObj
111
+ } catch (err) {
112
+ console.error('Error in createMultisigWallet()')
113
+ throw err
114
+ }
115
+ }
116
+
117
+ // Given a BCH address, scan its transaction history to find the latest
118
+ // APPROVAL transaction. This function returns the TXID of the UPDATE
119
+ // transaction that the APPROVAL transaction approves.
120
+ // If no APPROVAL transaction can be found, then function returns null.
121
+ // An optional input, filterTxids, is an array of transaction IDs to ignore. This can
122
+ // be used to ignore/skip any known, fake approval transactions.
123
+ async getApprovalTx (inObj = {}) {
124
+ try {
125
+ let address = inObj.address
126
+ const { filterTxids } = inObj
127
+
128
+ // Input validation
129
+ if (address.includes('simpleledger:')) {
130
+ address = this.bchjs.SLP.Address.toCashAddress(address)
131
+ }
132
+ if (!address.includes('bitcoincash:')) {
133
+ throw new Error('Input address must start with bitcoincash: or simpleledger:')
134
+ }
135
+
136
+ // Get the transaction history for the address
137
+ const txHistory = await this.wallet.getTransactions(address)
138
+ // console.log('txHistory: ', JSON.stringify(txHistory, null, 2))
139
+
140
+ // Loop through the transaction history
141
+ for (let i = 0; i < txHistory.length; i++) {
142
+ const thisTxid = txHistory[i]
143
+
144
+ // const height = thisTxid.height
145
+ const txid = thisTxid.tx_hash
146
+
147
+ // Skip the txid if it is in the filter list.
148
+ if (Array.isArray(filterTxids)) {
149
+ const txidFound = filterTxids.find(x => x === txid)
150
+ // console.log('txidFound: ', txidFound)
151
+ if (txidFound) {
152
+ continue
153
+ }
154
+ }
155
+
156
+ // Get the transaction details for the transaction
157
+ const txDetails = await this.util.getTxData(txid)
158
+ // console.log('txDetails: ', JSON.stringify(txDetails, null, 2))
159
+ // console.log(`txid: ${txid}`)
160
+
161
+ const out2ascii = Buffer.from(txDetails.vout[0].scriptPubKey.hex, 'hex').toString('ascii')
162
+ // console.log('out2ascii: ', out2ascii)
163
+
164
+ // If the first output is not an OP_RETURN, then the tx can be discarded.
165
+ if (!out2ascii.includes('APPROVE')) {
166
+ continue
167
+ }
168
+
169
+ const updateTxid = out2ascii.slice(10)
170
+ // console.log('updateTxid: ', updateTxid)
171
+
172
+ const outObj = {
173
+ approvalTxid: txid,
174
+ updateTxid,
175
+ approvalTxDetails: txDetails,
176
+ opReturn: out2ascii
177
+ }
178
+
179
+ return outObj
180
+ }
181
+
182
+ return null
183
+ } catch (err) {
184
+ console.error('Error in getApprovedData()')
185
+ throw err
186
+ }
187
+ }
188
+
189
+ // This function will retrieve an update transaction, given its txid. It will
190
+ // return an object with data about the transaction, including the CID and
191
+ // timestamp values encoded in the transactions OP_RETURN.
192
+ async getUpdateTx (inObj = {}) {
193
+ try {
194
+ const { txid } = inObj
195
+
196
+ // Input validation
197
+ if (!txid) {
198
+ throw new Error('txid required')
199
+ }
200
+
201
+ // Get the transaction details for the transaction
202
+ const txDetails = await this.util.getTxData(txid)
203
+ // console.log('txDetails: ', JSON.stringify(txDetails, null, 2))
204
+ // console.log(`txid: ${txid}`)
205
+
206
+ let updateObj = {}
207
+ try {
208
+ const out2ascii = Buffer.from(txDetails.vout[0].scriptPubKey.hex, 'hex').toString('ascii')
209
+ // console.log('out2ascii: ', out2ascii)
210
+
211
+ const jsonStr = out2ascii.slice(4)
212
+ // console.log('jsonStr: ', jsonStr)
213
+
214
+ updateObj = JSON.parse(jsonStr)
215
+ } catch (err) {
216
+ throw new Error('Could not parse JSON inside the transaction')
217
+ }
218
+
219
+ updateObj.txid = txid
220
+ updateObj.txDetails = txDetails
221
+
222
+ return updateObj
223
+ } catch (err) {
224
+ console.error('Error in getUpdateTx()')
225
+ throw err
226
+ }
227
+ }
228
+
229
+ // Given an CID, this function will retrieve the update data from an IPFS
230
+ // gateway.
231
+ async getCidData (inObj = {}) {
232
+ try {
233
+ const { cid } = inObj
234
+
235
+ // Input validation
236
+ if (!cid) {
237
+ throw new Error('cid a required input')
238
+ }
239
+
240
+ const urlStr = `${this.ipfsGateway}/ipfs/${cid}/data.json`
241
+ // console.log('urlStr: ', urlStr)
242
+
243
+ const request = await this.axios.get(urlStr)
244
+
245
+ return request.data
246
+ } catch (err) {
247
+ console.error('Error in getCidData()')
248
+ throw err
249
+ }
250
+ }
251
+
252
+ // This function will validate the approval transaction.
253
+ // This function will return true or false, to indicate the validity of the
254
+ // approval transaction.
255
+ // The input to this function is the output of several of the above function:
256
+ // - approvalObj is the output of getApprovalTx()
257
+ // - updateObj is the output of getUpdateTx()
258
+ // - updateData is the IPFS CID data retrieved with getCidData()
259
+ // - groupTokenId is optional. If not specified, it will default to the Group
260
+ // token used to generate the PSF Minting Council NFTs.
261
+ async validateApproval (inObj = {}) {
262
+ try {
263
+ // console.log('inObj: ', JSON.stringify(inObj, null, 2))
264
+
265
+ const { approvalObj, updateObj, updateData, groupTokenId } = inObj
266
+
267
+ let validationResult = false
268
+
269
+ // Input validation
270
+ if (!approvalObj) {
271
+ throw new Error('Output object of getApprovalTx() is expected as input to this function, as \'approvalObj\'')
272
+ }
273
+ if (!updateObj) {
274
+ throw new Error('Output object of getUpdateTx() is expected as input to this function, as \'updateObj\'')
275
+ }
276
+ if (!updateData) {
277
+ throw new Error('Update CID JSON data is expected as input to this function, as \'updateData\'')
278
+ }
279
+
280
+ // Regenerate the multisig address from the pubkeys in the update data.
281
+ // Ensure it matches the input address to the approval transaction.
282
+ const pubKeys = updateData.walletObj.publicKeys
283
+ const requiredSigners = updateData.walletObj.requiredSigners
284
+ const approvalInputAddr = approvalObj.approvalTxDetails.vin[0].address
285
+ const msAddr = new this.bitcore.Address(pubKeys, requiredSigners).toString()
286
+ if (msAddr !== approvalInputAddr) {
287
+ console.log(`Approval TX input address (${approvalInputAddr}) does not match calculated multisig address ${msAddr}`)
288
+ return validationResult
289
+ }
290
+
291
+ // Get public key data for each NFT holder, from the blockchain.
292
+ const nftData = await this.getNftHolderInfo(groupTokenId)
293
+ const tokenPubKeys = nftData.keys
294
+
295
+ // Loop through the public keys from the token data, and count the matches.
296
+ let matches = 0
297
+ for (let i = 0; i < tokenPubKeys.length; i++) {
298
+ const thisTokenPubKey = tokenPubKeys[i].pubKey
299
+
300
+ // Loop through the public keys from the update data
301
+ for (let j = 0; j < pubKeys.length; j++) {
302
+ const thisUpdatePubKey = pubKeys[j]
303
+
304
+ if (thisTokenPubKey === thisUpdatePubKey) {
305
+ matches++
306
+ break
307
+ }
308
+ }
309
+ }
310
+
311
+ // Set a threshold for success.
312
+ let threshold = 2 // Minimum
313
+ if (requiredSigners > threshold) threshold = requiredSigners
314
+
315
+ // If the threshold of public keys match, then the approval transaction
316
+ // has been validated.
317
+ if (matches >= threshold) {
318
+ validationResult = true
319
+ }
320
+
321
+ return validationResult
322
+ } catch (err) {
323
+ console.error('Error in validateApproval()')
324
+ throw err
325
+ }
326
+ }
327
+ }
328
+
329
+ module.exports = MultisigApproval
package/lib/nfts.js ADDED
@@ -0,0 +1,113 @@
1
+ /*
2
+ This library contains code for handling NFT and Group SLP tokens. Specifically
3
+ it has functions around looking up the NFTs related to a Group token, then
4
+ retrieving data on the holders of those NFTs.
5
+ */
6
+
7
+ class NFTs {
8
+ constructor (localConfig = {}) {
9
+ this.wallet = localConfig.wallet
10
+ if (!this.wallet) {
11
+ throw new Error('Instance of minimal-slp-wallet must be passed in as a property called \'wallet\', when initializing the nfts.js library.')
12
+ }
13
+ }
14
+
15
+ // Retrieve a list of NFTs from the Group token that spawned them.
16
+ // The default value is the PSF Minting Council Group token.
17
+ async getNftsFromGroup (groupId = '8e8d90ebdb1791d58eba7acd428ff3b1e21c47fb7aba2ba3b5b815aa0fe7d6d5') {
18
+ try {
19
+ const groupData = await this.wallet.getTokenData(groupId)
20
+ // console.log('groupData: ', groupData)
21
+
22
+ const nfts = groupData.genesisData.nfts
23
+
24
+ return nfts
25
+ } catch (err) {
26
+ console.error('Error in getNftsFromGroup()')
27
+ throw err
28
+ }
29
+ }
30
+
31
+ // This function expects an array of strings as input. Each element in the
32
+ // input array is expected to be the Token ID of the an NFT. The address
33
+ // holding each NFT is looked up on the blockchain. The array of returned
34
+ // addresses are filtered for duplicates, before being returned.
35
+ async getAddrsFromNfts (nfts = []) {
36
+ try {
37
+ let addrs = []
38
+
39
+ // Loop through each NFT Token ID in the array.
40
+ for (let i = 0; i < nfts.length; i++) {
41
+ const thisNft = nfts[i]
42
+
43
+ // console.log('getAddrsFromNfts() this.wallet.bchjs.restURL: ', this.wallet.bchjs.restURL)
44
+
45
+ const nftData = await this.wallet.getTokenData(thisNft, true)
46
+ // console.log('getAddrsFromNfts() nftData: ', nftData)
47
+
48
+ if (!nftData.genesisData.nftHolder) {
49
+ throw new Error(`SLP indexer data does not include a holder address for this NFT. nftData: ${JSON.stringify(nftData, null, 2)}`)
50
+ }
51
+
52
+ addrs.push(nftData.genesisData.nftHolder)
53
+ }
54
+
55
+ // Remove duplicate addresses
56
+ addrs = [...new Set(addrs)]
57
+
58
+ return addrs
59
+ } catch (err) {
60
+ console.error('Error in getAddrsFromNfts(): ', err)
61
+ throw err
62
+ }
63
+ }
64
+
65
+ // The input to this function is the output of getNftsFromGroup() and
66
+ // getAddrsFromNfts(). It expects two arrays of strings, one an array of
67
+ // NFT Token IDs, the other an array of addresses holding those NFTs.
68
+ //
69
+ // For each address, it attempts to lookup the public key for that address.
70
+ // It returns an object with a keys and keysNotFound property:
71
+ // - keys - Object containing address and public key
72
+ // - keysNotFound - Array of addresses whos public keys could not be found.
73
+ async findKeys (addrs, nfts) {
74
+ // It is assumed that element 1 in the addrs array is associated with
75
+ // element 1 in the nfts array.
76
+
77
+ try {
78
+ const keys = []
79
+ const keysNotFound = []
80
+
81
+ for (let i = 0; i < addrs.length; i++) {
82
+ const thisAddr = addrs[i]
83
+ const thisNft = nfts[i]
84
+
85
+ // console.log('findKeys() thisAddr: ', thisAddr)
86
+ // console.log('this.wallet: ', this.wallet)
87
+ // console.log('this.wallet.interface: ', this.wallet.interface)
88
+ // console.log('this.wallet.restURL: ', this.wallet.restURL)
89
+
90
+ // Get public Key for reciever from the blockchain.
91
+ const publicKey = await this.wallet.getPubKey(thisAddr)
92
+ // console.log(`publicKey: ${JSON.stringify(publicKey, null, 2)}`)
93
+
94
+ if (publicKey.includes('not found')) {
95
+ keysNotFound.push(thisAddr)
96
+ } else {
97
+ keys.push({
98
+ addr: thisAddr,
99
+ pubKey: publicKey,
100
+ nft: thisNft
101
+ })
102
+ }
103
+ }
104
+
105
+ return { keys, keysNotFound }
106
+ } catch (err) {
107
+ console.error('Error in findKeys()')
108
+ throw err
109
+ }
110
+ }
111
+ }
112
+
113
+ module.exports = NFTs
package/lib/util.js ADDED
@@ -0,0 +1,50 @@
1
+ /*
2
+ Generic utility functions that might be used in multiple places of the library.
3
+ */
4
+
5
+ class UtilLib {
6
+ constructor (localConfig = {}) {
7
+ // Dependecy Injection
8
+ this.wallet = localConfig.wallet
9
+ if (!this.wallet) {
10
+ throw new Error('Instance of minimal-slp-wallet must be passed in as a property called \'wallet\', when initializing the util library.')
11
+ }
12
+
13
+ // Bind the 'this' object to all subfunctions.
14
+ this.getTxData = this.getTxData.bind(this)
15
+
16
+ // Create a transaction details cache, to reduce the number of API calls.
17
+ this.txCache = {}
18
+ }
19
+
20
+ // Given a transaction ID, retrieve the transaction details. If the details
21
+ // have already been downloaded, then they will be stored in a local cache.
22
+ // The cache speeds things up and reduces the number of API calls.
23
+ async getTxData (txid) {
24
+ try {
25
+ if (!txid) {
26
+ throw new Error('txid (transactoin ID) required')
27
+ }
28
+
29
+ // Try to get the transaction details from the cache
30
+ let txDetails = this.txCache[txid]
31
+
32
+ // Download from the full node if details are not in the cache.
33
+ if (!txDetails) {
34
+ // Get the transaction details from the full node
35
+ txDetails = await this.wallet.getTxData([txid])
36
+ txDetails = txDetails[0]
37
+
38
+ // Add the details to the cache
39
+ this.txCache[txid] = txDetails
40
+ }
41
+
42
+ return txDetails
43
+ } catch (err) {
44
+ console.error('Error in getTxData()')
45
+ throw err
46
+ }
47
+ }
48
+ }
49
+
50
+ module.exports = UtilLib
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "psffpp",
3
+ "version": "1.0.1",
4
+ "description": "PS010 PSF File Pinning Protocol",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "start": "node index.js",
8
+ "test": "npm run lint && TEST=unit nyc mocha test/unit/",
9
+ "test:integration": "mocha --timeout 120000 test/integration/",
10
+ "lint": "standard --env mocha --fix",
11
+ "docs": "./node_modules/.bin/apidoc -i src/ -o docs",
12
+ "coverage:report": "nyc --reporter=html mocha test/unit/ --exit"
13
+ },
14
+ "keywords": [
15
+ "bitcoin",
16
+ "bitcoin cash",
17
+ "wallet",
18
+ "javascript",
19
+ "cryptocurrency",
20
+ "react",
21
+ "front end",
22
+ "client",
23
+ "apidoc",
24
+ "slp",
25
+ "tokens"
26
+ ],
27
+ "author": "Chris Troutner <chris.troutner@gmail.com>",
28
+ "license": "MIT",
29
+ "apidoc": {
30
+ "title": "psffpp",
31
+ "url": "localhost:5000"
32
+ },
33
+ "repository": "Permissionless-Software-Foundation/psffpp",
34
+ "dependencies": {
35
+ "axios": "1.3.5",
36
+ "bitcore-lib-cash": "10.0.2"
37
+ },
38
+ "devDependencies": {
39
+ "apidoc": "0.54.0",
40
+ "chai": "4.3.7",
41
+ "husky": "8.0.3",
42
+ "lodash.clonedeep": "4.5.0",
43
+ "minimal-slp-wallet": "5.8.8",
44
+ "mocha": "10.2.0",
45
+ "nyc": "15.1.0",
46
+ "semantic-release": "19.0.5",
47
+ "sinon": "15.0.3",
48
+ "standard": "17.0.0"
49
+ },
50
+ "release": {
51
+ "publish": [
52
+ {
53
+ "path": "@semantic-release/npm",
54
+ "npmPublish": true
55
+ }
56
+ ]
57
+ },
58
+ "husky": {
59
+ "hooks": {
60
+ "pre-commit": "npm run lint"
61
+ }
62
+ }
63
+ }
@@ -0,0 +1,117 @@
1
+ /*
2
+ Integration tests for the main library.
3
+ */
4
+
5
+ // Global npm libraries
6
+ const SlpWallet = require('minimal-slp-wallet')
7
+ const assert = require('chai').assert
8
+
9
+ // Local libraries
10
+ const MultisigApproval = require('../../index')
11
+
12
+ describe('#psf-multisig-approval', () => {
13
+ let uut
14
+
15
+ before(async () => {
16
+ const wallet = new SlpWallet(undefined, {
17
+ interface: 'consumer-api',
18
+ // restURL: 'https://bch-consumer-anacortes-wa-usa.fullstackcash.nl'
19
+ restURL: 'https://free-bch.fullstack.cash'
20
+ })
21
+ await wallet.walletInfoPromise
22
+
23
+ uut = new MultisigApproval({ wallet })
24
+ })
25
+
26
+ describe('#getNftHolderInfo', () => {
27
+ it('should get address and pubkeys for Minting Council NFT holders', async () => {
28
+ const result = await uut.getNftHolderInfo()
29
+ // console.log('result: ', result)
30
+
31
+ // Assert expected properties exist
32
+ assert.property(result, 'keys')
33
+ assert.property(result, 'keysNotFound')
34
+
35
+ // Assert that each property is an array.
36
+ assert.isArray(result.keys)
37
+ assert.isArray(result.keysNotFound)
38
+ })
39
+ })
40
+
41
+ describe('#createMultisigAddress', () => {
42
+ it('should generate a multisig address from token holder info', async () => {
43
+ const tokenHolderInfo = await uut.getNftHolderInfo()
44
+
45
+ const keys = tokenHolderInfo.keys
46
+
47
+ const result = await uut.createMultisigAddress({ keys })
48
+ // console.log('result: ', result)
49
+
50
+ assert.property(result, 'address')
51
+ assert.property(result, 'scriptHex')
52
+ assert.property(result, 'publicKeys')
53
+ assert.property(result, 'requiredSigners')
54
+ })
55
+ })
56
+
57
+ describe('#getApprovalTx', () => {
58
+ it('should retrieve the latest APPROVAL transaction from an address', async () => {
59
+ const address = 'bitcoincash:qrwe6kxhvu47ve6jvgrf2d93w0q38av7s5xm9xfehr'
60
+
61
+ const result = await uut.getApprovalTx({ address })
62
+ // const result = await uut.getApprovalTx({ address, filterTxids: ['a63f9fbcc901316e6e89f5a8caaad6b2ab268278b29866c6c22088bd3ab93900'] })
63
+ // console.log('result: ', result)
64
+
65
+ assert.equal(result.updateTxid.length, 64)
66
+ })
67
+ })
68
+
69
+ describe('#getUpdateTx', () => {
70
+ it('should retrieve an update transaction', async () => {
71
+ const txid = 'f8ea1fcd4481adfd62c6251c6a4f63f3d5ac3d5fdcc38b350d321d93254df65f'
72
+
73
+ const result = await uut.getUpdateTx({ txid })
74
+ // console.log('result: ', result)
75
+
76
+ // Assert returned object has expected properties
77
+ assert.property(result, 'cid')
78
+ assert.property(result, 'ts')
79
+ assert.property(result, 'txid')
80
+ assert.property(result, 'txDetails')
81
+ })
82
+ })
83
+
84
+ describe('#getCidData', () => {
85
+ it('should get JSON data from an IPFS gateway', async () => {
86
+ const cid = 'bafybeib5d6s6t3tq4lhwp2ocvz7y2ws4czgkrmhlhv5y5aeyh6bqrmsxxi'
87
+ const result = await uut.getCidData({ cid })
88
+ // console.log('result: ', result)
89
+
90
+ // Assert expected properties exist
91
+ assert.property(result, 'groupId')
92
+ assert.property(result, 'keys')
93
+ assert.property(result, 'walletObj')
94
+ assert.property(result, 'multisigAddr')
95
+ assert.property(result, 'p2wdbWritePrice')
96
+ })
97
+ })
98
+
99
+ describe('#validateApproval', () => {
100
+ it('should validate a valid approval transaction', async () => {
101
+ const address = 'bitcoincash:qrwe6kxhvu47ve6jvgrf2d93w0q38av7s5xm9xfehr'
102
+ const approvalObj = await uut.getApprovalTx({ address })
103
+
104
+ const txid = 'f8ea1fcd4481adfd62c6251c6a4f63f3d5ac3d5fdcc38b350d321d93254df65f'
105
+ const updateObj = await uut.getUpdateTx({ txid })
106
+
107
+ const cid = 'bafybeib5d6s6t3tq4lhwp2ocvz7y2ws4czgkrmhlhv5y5aeyh6bqrmsxxi'
108
+ const updateData = await uut.getCidData({ cid })
109
+
110
+ const inObj = { approvalObj, updateObj, updateData }
111
+ const result = await uut.validateApproval(inObj)
112
+ // console.log('result: ', result)
113
+
114
+ assert.equal(result, true)
115
+ })
116
+ })
117
+ })