psffpp 1.0.1 → 1.1.0

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 CHANGED
@@ -1,17 +1,18 @@
1
1
  /*
2
- This library implement PS009 specification:
2
+ This library implement PS010 specification:
3
3
  https://github.com/Permissionless-Software-Foundation/specifications/blob/master/ps009-multisig-approval.md
4
4
  */
5
5
 
6
6
  // global libraries
7
- const bitcore = require('bitcore-lib-cash')
8
- const axios = require('axios')
7
+ import MultisigApproval from 'psf-multisig-approval'
9
8
 
10
9
  // Local libraries
11
- const NFTs = require('./lib/nfts')
12
- const UtilLib = require('./lib/util')
13
10
 
14
- class MultisigApproval {
11
+ // Constants
12
+ const PSF_HARDCODE_WRITE_PRICE = 0.08335233
13
+ const WRITE_PRICE_ADDR = 'bitcoincash:qrwe6kxhvu47ve6jvgrf2d93w0q38av7s5xm9xfehr'
14
+
15
+ class PSFFPP {
15
16
  constructor (localConfig = {}) {
16
17
  // Dependency Injection
17
18
  this.wallet = localConfig.wallet
@@ -19,311 +20,228 @@ class MultisigApproval {
19
20
  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
  }
21
22
 
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
23
  // Encapsulate dependencies
30
24
  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)
25
+ this.ps009 = null // placeholder for Multisig Approval library.
35
26
 
36
27
  // 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)
28
+ this._initPs009 = this._initPs009.bind(this)
29
+ this.getMcWritePrice = this.getMcWritePrice.bind(this)
30
+ this.createPinClaim = this.createPinClaim.bind(this)
40
31
 
41
- // Create a transaction details cache, to reduce the number of API calls.
42
- this.txCache = {}
32
+ // State
33
+ this.currentWritePrice = null
34
+ this.filterTxids = []
43
35
  }
44
36
 
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)
37
+ // Initialize the PS009 Multisig Approval library if it hasn't already been
38
+ // initialized.
39
+ async _initPs009 () {
40
+ if (!this.ps009) {
41
+ this.ps009 = new MultisigApproval({ wallet: this.wallet })
42
+ }
53
43
 
54
- const nfts = await this.nfts.getNftsFromGroup()
55
- // console.log('getNftHolderInfo() nfts: ', nfts)
44
+ return true
45
+ }
56
46
 
57
- const addrs = await this.nfts.getAddrsFromNfts(nfts)
58
- // console.log('getNftHolderInfo() addrs: ', addrs)
47
+ // Get the write price set by the PSF Minting Council.
48
+ // This function assumes the transaction history retrieved from the Cash
49
+ // Stack is sorted in descending order with the biggest (newest) block
50
+ // in the first element in the transaction history array.
51
+ async getMcWritePrice () {
52
+ // Hard codeded value. 3/2/24
53
+ // This value is returned if there are any issues returning the write price.
54
+ // It should be higher than actual fee, so that any writes will propegate to
55
+ // the nodes that successfully retrieved the current write price.
56
+ let writePrice = PSF_HARDCODE_WRITE_PRICE
59
57
 
60
- const { keys, keysNotFound } = await this.nfts.findKeys(addrs, nfts)
58
+ try {
59
+ // Return the saved write price if this function has already been called once.
60
+ if (this.currentWritePrice) return this.currentWritePrice
61
61
 
62
- return { keys, keysNotFound }
63
- } catch (err) {
64
- console.error('Error in getNftHolderInfo()')
65
- throw err
66
- }
67
- }
62
+ await this._initPs009()
68
63
 
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
64
+ // Find the PS009 approval transaction the addresses tx history.
65
+ console.log('\nSearching blockchain for updated write price...')
66
+ const approvalObj = await this.ps009.getApprovalTx({
67
+ address: WRITE_PRICE_ADDR,
68
+ filterTxids: this.filterTxids
69
+ })
70
+ // console.log('approvalObj: ', JSON.stringify(approvalObj, null, 2))
77
71
 
78
- // Input validation
79
- if (!Array.isArray(keys)) {
80
- throw new Error('keys must be an array containing public keys')
72
+ // Throw an error if no approval transaction can be found in the
73
+ // transaction history.
74
+ if (approvalObj === null) {
75
+ throw new Error(`APPROVAL transaction could not be found in the TX history of ${WRITE_PRICE_ADDR}. Can not reach consensus on write price.`)
81
76
  }
82
77
 
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]
78
+ const { approvalTxid, updateTxid } = approvalObj
79
+ console.log(`New approval txid found (${approvalTxid}), validating...`)
87
80
 
88
- pubKeys.push(thisPair.pubKey)
89
- }
81
+ // Get the CID from the update transaction.
82
+ const updateObj = await this.ps009.getUpdateTx({ txid: updateTxid })
83
+ // console.log(`updateObj: ${JSON.stringify(updateObj, null, 2)}`)
84
+ const { cid } = updateObj
90
85
 
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
- }
86
+ // Resolve the CID into JSON data from the IPFS gateway.
87
+ const updateData = await this.ps009.getCidData({ cid })
88
+ // console.log(`updateData: ${JSON.stringify(updateData, null, 2)}`)
96
89
 
97
- // Multisig Address
98
- const msAddr = new bitcore.Address(pubKeys, requiredSigners)
90
+ // Validate the approval transaction
91
+ const approvalIsValid = await this.ps009.validateApproval({
92
+ approvalObj,
93
+ updateObj,
94
+ updateData
95
+ })
99
96
 
100
- // Locking Script in hex representation.
101
- const scriptHex = new bitcore.Script(msAddr).toHex()
97
+ if (approvalIsValid) {
98
+ console.log('Approval TXID validated.')
102
99
 
103
- const walletObj = {
104
- address: msAddr.toString(),
105
- scriptHex,
106
- publicKeys: pubKeys,
107
- requiredSigners
108
- }
100
+ // Return the write price from the update data.
101
+ writePrice = updateData.p2wdbWritePrice
102
+ } else {
103
+ // Approval transaction failed validation.
104
+ console.log(`Approval TXID was found to be invalid: ${approvalTxid}`)
109
105
 
110
- return walletObj
106
+ // Add this invalid TXID to the filter array so that it is skipped.
107
+ this.filterTxids.push(approvalTxid)
108
+
109
+ // Continue looking for the correct approval transaction by recursivly
110
+ // calling this function.
111
+ writePrice = await this.getMcWritePrice()
112
+ }
111
113
  } catch (err) {
112
- console.error('Error in createMultisigWallet()')
113
- throw err
114
+ console.error('Error in getMcWritePrice()')
115
+ console.log(`Using hard-coded, safety value of ${writePrice} PSF tokens per write.`)
114
116
  }
117
+ // Save the curent write price to the state.
118
+ this.currentWritePrice = writePrice
119
+ return writePrice
115
120
  }
116
121
 
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 = {}) {
122
+ // Given information about a file, this function will generate a Pin Claim.
123
+ // This function takes controls of the wallet and uses it to broadcast two
124
+ // transactions: a Proof-of-Burn (pobTxid) and a Pin Claim (climTxid). The
125
+ // function returns an object with the transaction ID of those two transacions.
126
+ async createPinClaim (inObj = {}) {
124
127
  try {
125
- let address = inObj.address
126
- const { filterTxids } = inObj
128
+ const { cid, filename, fileSizeInMegabytes } = inObj
127
129
 
128
130
  // Input validation
129
- if (address.includes('simpleledger:')) {
130
- address = this.bchjs.SLP.Address.toCashAddress(address)
131
+ if (!cid) {
132
+ throw new Error('cid required to generate pin claim.')
131
133
  }
132
- if (!address.includes('bitcoincash:')) {
133
- throw new Error('Input address must start with bitcoincash: or simpleledger:')
134
+ if (!filename) {
135
+ throw new Error('filename required to generate pin claim.')
134
136
  }
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
137
+ if (!fileSizeInMegabytes) {
138
+ throw new Error('fileSizeInMegabytes size in megabytes required to generate pin claim.')
180
139
  }
181
140
 
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
141
+ // Initialize the wallet
142
+ await this.wallet.initialize()
195
143
 
196
- // Input validation
197
- if (!txid) {
198
- throw new Error('txid required')
199
- }
144
+ // Initialize the PS009 library
145
+ await this._initPs009()
200
146
 
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}`)
147
+ // Get the cost in PSF tokens to store 1MB
148
+ const writePrice = await this.getMcWritePrice()
205
149
 
206
- let updateObj = {}
207
- try {
208
- const out2ascii = Buffer.from(txDetails.vout[0].scriptPubKey.hex, 'hex').toString('ascii')
209
- // console.log('out2ascii: ', out2ascii)
150
+ // Create a proof-of-burn (PoB) transaction
151
+ // const WRITE_PRICE = 0.08335233 // Cost in PSF tokens to pin 1MB
152
+ const PSF_TOKEN_ID = '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0'
210
153
 
211
- const jsonStr = out2ascii.slice(4)
212
- // console.log('jsonStr: ', jsonStr)
154
+ // Calculate the write cost
155
+ const dataCost = writePrice * fileSizeInMegabytes
156
+ const minCost = writePrice
157
+ let actualCost = minCost
158
+ if (dataCost > minCost) actualCost = dataCost
159
+ console.log(`Burning ${actualCost} PSF tokens for ${fileSizeInMegabytes} MB of data.`)
213
160
 
214
- updateObj = JSON.parse(jsonStr)
215
- } catch (err) {
216
- throw new Error('Could not parse JSON inside the transaction')
217
- }
161
+ const pobTxid = await this.wallet.burnTokens(actualCost, PSF_TOKEN_ID)
162
+ // console.log(`Proof-of-burn TX: ${pobTxid}`)
218
163
 
219
- updateObj.txid = txid
220
- updateObj.txDetails = txDetails
164
+ // Get info and libraries from the wallet.
165
+ const addr = this.wallet.walletInfo.address
166
+ const bchjs = this.wallet.bchjs
167
+ const wif = this.wallet.walletInfo.privateKey
221
168
 
222
- return updateObj
223
- } catch (err) {
224
- console.error('Error in getUpdateTx()')
225
- throw err
226
- }
227
- }
169
+ // Get a UTXO to spend to generate the pin claim TX.
170
+ let utxos = await this.wallet.getUtxos()
171
+ utxos = utxos.bchUtxos
172
+ const utxo = bchjs.Utxo.findBiggestUtxo(utxos)
228
173
 
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
174
+ // instance of transaction builder
175
+ const transactionBuilder = new bchjs.TransactionBuilder()
234
176
 
235
- // Input validation
236
- if (!cid) {
237
- throw new Error('cid a required input')
238
- }
177
+ const originalAmount = utxo.value
178
+ const vout = utxo.tx_pos
179
+ const txid = utxo.tx_hash
239
180
 
240
- const urlStr = `${this.ipfsGateway}/ipfs/${cid}/data.json`
241
- // console.log('urlStr: ', urlStr)
181
+ // add input with txid and index of vout
182
+ transactionBuilder.addInput(txid, vout)
242
183
 
243
- const request = await this.axios.get(urlStr)
184
+ // TODO: Compute the 1 sat/byte fee.
185
+ const fee = 500
244
186
 
245
- return request.data
246
- } catch (err) {
247
- console.error('Error in getCidData()')
248
- throw err
249
- }
250
- }
187
+ // BEGIN - Construction of OP_RETURN transaction.
251
188
 
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))
189
+ // Add the OP_RETURN to the transaction.
190
+ const script = [
191
+ bchjs.Script.opcodes.OP_RETURN,
192
+ Buffer.from('00510000', 'hex'),
193
+ Buffer.from(pobTxid, 'hex'),
194
+ Buffer.from(cid),
195
+ Buffer.from(filename)
196
+ ]
264
197
 
265
- const { approvalObj, updateObj, updateData, groupTokenId } = inObj
198
+ // Compile the script array into a bitcoin-compliant hex encoded string.
199
+ const data = bchjs.Script.encode(script)
266
200
 
267
- let validationResult = false
201
+ // Add the OP_RETURN output.
202
+ transactionBuilder.addOutput(data, 0)
268
203
 
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
- }
204
+ // END - Construction of OP_RETURN transaction.
279
205
 
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
- }
206
+ // Send the same amount - fee.
207
+ transactionBuilder.addOutput(addr, originalAmount - fee)
290
208
 
291
- // Get public key data for each NFT holder, from the blockchain.
292
- const nftData = await this.getNftHolderInfo(groupTokenId)
293
- const tokenPubKeys = nftData.keys
209
+ // Create an EC Key Pair from the user-supplied WIF.
210
+ const ecPair = bchjs.ECPair.fromWIF(wif)
294
211
 
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
212
+ // Sign the transaction with the HD node.
213
+ let redeemScript
214
+ transactionBuilder.sign(
215
+ 0,
216
+ ecPair,
217
+ redeemScript,
218
+ transactionBuilder.hashTypes.SIGHASH_ALL,
219
+ originalAmount
220
+ )
299
221
 
300
- // Loop through the public keys from the update data
301
- for (let j = 0; j < pubKeys.length; j++) {
302
- const thisUpdatePubKey = pubKeys[j]
222
+ // build tx
223
+ const tx = transactionBuilder.build()
303
224
 
304
- if (thisTokenPubKey === thisUpdatePubKey) {
305
- matches++
306
- break
307
- }
308
- }
309
- }
225
+ // output rawhex
226
+ const hex = tx.toHex()
227
+ // console.log(`TX hex: ${hex}`);
228
+ // console.log(` `);
310
229
 
311
- // Set a threshold for success.
312
- let threshold = 2 // Minimum
313
- if (requiredSigners > threshold) threshold = requiredSigners
230
+ // Broadcast transation to the network
231
+ const claimTxid = await this.wallet.broadcast({ hex })
232
+ // console.log(`Claim Transaction ID: ${claimTxid}`)
233
+ // console.log(`https://blockchair.com/bitcoin-cash/transaction/${claimTxid}`)
314
234
 
315
- // If the threshold of public keys match, then the approval transaction
316
- // has been validated.
317
- if (matches >= threshold) {
318
- validationResult = true
235
+ return {
236
+ pobTxid,
237
+ claimTxid
319
238
  }
320
-
321
- return validationResult
322
239
  } catch (err) {
323
- console.error('Error in validateApproval()')
240
+ console.error('Error in ps010/createPinClaim()')
324
241
  throw err
325
242
  }
326
243
  }
327
244
  }
328
245
 
329
- module.exports = MultisigApproval
246
+ // module.exports = PSFFPP
247
+ export default PSFFPP
package/package.json CHANGED
@@ -1,15 +1,16 @@
1
1
  {
2
2
  "name": "psffpp",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "PS010 PSF File Pinning Protocol",
5
5
  "main": "index.js",
6
+ "type": "module",
6
7
  "scripts": {
7
8
  "start": "node index.js",
8
- "test": "npm run lint && TEST=unit nyc mocha test/unit/",
9
+ "test": "npm run lint && TEST=unit c8 --reporter=text mocha test/unit/",
9
10
  "test:integration": "mocha --timeout 120000 test/integration/",
10
11
  "lint": "standard --env mocha --fix",
11
12
  "docs": "./node_modules/.bin/apidoc -i src/ -o docs",
12
- "coverage:report": "nyc --reporter=html mocha test/unit/ --exit"
13
+ "coverage:report": "c8 --reporter=html mocha test/unit/ --exit"
13
14
  },
14
15
  "keywords": [
15
16
  "bitcoin",
@@ -33,16 +34,18 @@
33
34
  "repository": "Permissionless-Software-Foundation/psffpp",
34
35
  "dependencies": {
35
36
  "axios": "1.3.5",
36
- "bitcore-lib-cash": "10.0.2"
37
+ "bitcore-lib-cash": "10.0.2",
38
+ "psf-multisig-approval": "2.0.3"
37
39
  },
38
40
  "devDependencies": {
41
+ "@psf/bch-js": "^6.7.4",
39
42
  "apidoc": "0.54.0",
43
+ "c8": "^9.1.0",
40
44
  "chai": "4.3.7",
41
45
  "husky": "8.0.3",
42
46
  "lodash.clonedeep": "4.5.0",
43
47
  "minimal-slp-wallet": "5.8.8",
44
48
  "mocha": "10.2.0",
45
- "nyc": "15.1.0",
46
49
  "semantic-release": "19.0.5",
47
50
  "sinon": "15.0.3",
48
51
  "standard": "17.0.0"