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/.editorconfig +30 -0
- package/.eslintrc.json +10 -0
- package/.on-save.json +8 -0
- package/.travis.yml +33 -0
- package/LICENSE.md +8 -0
- package/PEDIGREE.md +3 -0
- package/README.md +211 -0
- package/apidoc.json +3 -0
- package/examples/create-wallet.js +23 -0
- package/examples/list-tokens.js +26 -0
- package/examples/send-bch.js +60 -0
- package/examples/send-tokens.js +66 -0
- package/index.js +329 -0
- package/lib/nfts.js +113 -0
- package/lib/util.js +50 -0
- package/package.json +63 -0
- package/test/integration/main-index-integration.js +117 -0
- package/test/unit/main-index-unit.js +372 -0
- package/test/unit/mocks/main-index-mocks.js +290 -0
- package/test/unit/mocks/util-mocks.js +32 -0
- package/test/unit/nfts-unit.js +190 -0
- package/test/unit/util-unit.js +91 -0
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
|
+
})
|