diff --git a/src/utxo.js b/src/utxo.js index 82438e7..1a1e97e 100644 --- a/src/utxo.js +++ b/src/utxo.js @@ -22,16 +22,25 @@ class UTXO { * @apiGroup UTXO * @apiDescription Get UTXOs for an address * - * Given an address, this function will return an object with three arrays: - * - bchUtxos - UTXOs confirmed to be spendable as normal BCH - * - slpUtxos - UTXOs confirmed to be colored as valid SLP tokens - * - nullUtxo - UTXOs that did not pass SLP validation. Should be ignored and + * Given an address, this function will return an object with thre following + * properties: + * - address: "" - the address these UTXOs are associated with + * - bchUtxos: [] - UTXOs confirmed to be spendable as normal BCH + * - nullUtxo: [] - UTXOs that did not pass SLP validation. Should be ignored and * not spent, to be safe. + * - slpUtxos: {} - UTXOs confirmed to be colored as valid SLP tokens + * - type1: {} + * - tokens: [] - SLP token Type 1 tokens. + * - mintBatons: [] - SLP token Type 1 mint batons. + * - nft: {} + * - tokens: [] - NFT tokens + * - groupTokens: [] - NFT Group tokens, used to create NFT tokens. + * - groupMintBatons: [] - Minting baton to create more NFT Group tokens. * * @apiExample Example usage: * (async () => { * try { - * let utxos = await bchjs.Utxo.get('simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9'); + * let utxos = await bchjs.Utxo.get('simpleledger:qrm0c67wwqh0w7wjxua2gdt2xggnm90xwsr5k22euj'); * console.log(utxos); * } catch(error) { * console.error(error) @@ -39,41 +48,131 @@ class UTXO { * })() * * // returns - * { - * "bchUtxos": [ - * { - * "height": 674331, - * "tx_hash": "5e86cd911110e4f5db0cc3e8f459d5e8850b49adf57059a71daee674b2867b31", - * "tx_pos": 0, - * "value": 1000, - * "txid": "5e86cd911110e4f5db0cc3e8f459d5e8850b49adf57059a71daee674b2867b31", - * "vout": 0, - * "isValid": false + * [ + * { + * "address": "bitcoincash:qrm0c67wwqh0w7wjxua2gdt2xggnm90xws00a3lezv", + * "bchUtxos": [ + * { + * "height": 674513, + * "tx_hash": "705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04", + * "tx_pos": 3, + * "value": 38134, + * "txid": "705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04", + * "vout": 3, + * "isValid": false + * } + * ], + * "nullUtxos": [], + * "slpUtxos": { + * "type1": { + * "mintBatons": [ + * { + * "height": 674512, + * "tx_hash": "acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230", + * "tx_pos": 2, + * "value": 546, + * "txid": "acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230", + * "vout": 2, + * "utxoType": "minting-baton", + * "tokenId": "acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230", + * "tokenTicker": "SLPTEST", + * "tokenName": "SLP Test Token", + * "tokenDocumentUrl": "https://FullStack.cash", + * "tokenDocumentHash": "", + * "decimals": 8, + * "tokenType": 1, + * "isValid": true + * } + * ], + * "tokens": [ + * { + * "height": 674512, + * "tx_hash": "acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230", + * "tx_pos": 1, + * "value": 546, + * "txid": "acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230", + * "vout": 1, + * "utxoType": "token", + * "tokenQty": "100", + * "tokenId": "acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230", + * "tokenTicker": "SLPTEST", + * "tokenName": "SLP Test Token", + * "tokenDocumentUrl": "https://FullStack.cash", + * "tokenDocumentHash": "", + * "decimals": 8, + * "tokenType": 1, + * "isValid": true + * } + * ] + * }, + * "nft": { + * "groupMintBatons": [ + * { + * "height": 674513, + * "tx_hash": "705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04", + * "tx_pos": 2, + * "value": 546, + * "txid": "705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04", + * "vout": 2, + * "utxoType": "minting-baton", + * "transactionType": "mint", + * "tokenId": "a9a2458a0f9f0761d5b8725c256f2e7fa35b9de4dec6f47b46e9f20d92d0e395", + * "tokenType": 129, + * "tokenTicker": "NFTGT", + * "tokenName": "NFT Test Group Token", + * "tokenDocumentUrl": "https://FullStack.cash", + * "tokenDocumentHash": "", + * "decimals": 0, + * "mintBatonVout": 2, + * "isValid": true + * } + * ], + * "groupTokens": [ + * { + * "height": 674513, + * "tx_hash": "705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04", + * "tx_pos": 1, + * "value": 546, + * "txid": "705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04", + * "vout": 1, + * "utxoType": "token", + * "tokenQty": "10", + * "transactionType": "mint", + * "tokenId": "a9a2458a0f9f0761d5b8725c256f2e7fa35b9de4dec6f47b46e9f20d92d0e395", + * "tokenType": 129, + * "tokenTicker": "NFTGT", + * "tokenName": "NFT Test Group Token", + * "tokenDocumentUrl": "https://FullStack.cash", + * "tokenDocumentHash": "", + * "decimals": 0, + * "mintBatonVout": 2, + * "isValid": true + * } + * ], + * "tokens": [ + * { + * "height": 674512, + * "tx_hash": "eeddccc4d716f04157ea132ac93a48040fea34a6b57f3d8f0cccb7d1a731ab2b", + * "tx_pos": 1, + * "value": 546, + * "txid": "eeddccc4d716f04157ea132ac93a48040fea34a6b57f3d8f0cccb7d1a731ab2b", + * "vout": 1, + * "utxoType": "token", + * "tokenQty": "1", + * "tokenId": "eeddccc4d716f04157ea132ac93a48040fea34a6b57f3d8f0cccb7d1a731ab2b", + * "tokenTicker": "NFT004", + * "tokenName": "NFT Child", + * "tokenDocumentUrl": "https://FullStack.cash", + * "tokenDocumentHash": "", + * "decimals": 0, + * "tokenType": 65, + * "isValid": true + * } + * ] + * } * } - * ], - * "slpUtxos": [ - * { - * "height": 569108, - * "tx_hash": "384e1b8197e8de7d38f98317af2cf5f6bcb50007c46943b3498a6fab6e8aeb7c", - * "tx_pos": 1, - * "value": 546, - * "txid": "384e1b8197e8de7d38f98317af2cf5f6bcb50007c46943b3498a6fab6e8aeb7c", - * "vout": 1, - * "utxoType": "token", - * "transactionType": "send", - * "tokenId": "a436c8e1b6bee3d701c6044d190f76f774be83c36de8d34a988af4489e86dd37", - * "tokenTicker": "sleven", - * "tokenName": "sleven", - * "tokenDocumentUrl": "sleven", - * "tokenDocumentHash": "", - * "decimals": 7, - * "tokenType": 1, - * "tokenQty": "1", - * "isValid": true - * } - * ], - * "nullUtxos": [] - * } + * } + * ] * * */ @@ -109,7 +208,57 @@ class UTXO { // Filter out the different types of UTXOs. addrObj.bchUtxos = thisAddr.utxos.filter(elem => elem.isValid === false) addrObj.nullUtxos = thisAddr.utxos.filter(elem => elem.isValid === null) - addrObj.slpUtxos = thisAddr.utxos.filter(elem => elem.isValid === true) + + // Break down the SLP UTXOs. + addrObj.slpUtxos = { + type1: {}, + nft: {} + } + + // Token Type 1 Minting Batons. + addrObj.slpUtxos.type1.mintBatons = thisAddr.utxos.filter(elem => { + const isValid = elem.isValid === true + const tokenTypeIs1 = elem.tokenType === 1 + const isMintingBaton = elem.utxoType === 'minting-baton' + + return isValid && tokenTypeIs1 && isMintingBaton + }) + + // Token Type 1 tokens. + addrObj.slpUtxos.type1.tokens = thisAddr.utxos.filter(elem => { + const isValid = elem.isValid === true + const tokenTypeIs1 = elem.tokenType === 1 + const isToken = elem.utxoType === 'token' + + return isValid && tokenTypeIs1 && isToken + }) + + // NFT Group Minting Batons + addrObj.slpUtxos.nft.groupMintBatons = thisAddr.utxos.filter(elem => { + const isValid = elem.isValid === true + const tokenTypeIs129 = elem.tokenType === 129 + const isMintingBaton = elem.utxoType === 'minting-baton' + + return isValid && tokenTypeIs129 && isMintingBaton + }) + + // NFT Group tokens + addrObj.slpUtxos.nft.groupTokens = thisAddr.utxos.filter(elem => { + const isValid = elem.isValid === true + const tokenTypeIs129 = elem.tokenType === 129 + const isToken = elem.utxoType === 'token' + + return isValid && tokenTypeIs129 && isToken + }) + + // NFT (Child) tokens + addrObj.slpUtxos.nft.tokens = thisAddr.utxos.filter(elem => { + const isValid = elem.isValid === true + const tokenTypeIs65 = elem.tokenType === 65 + const isToken = elem.utxoType === 'token' + + return isValid && tokenTypeIs65 && isToken + }) retAry.push(addrObj) } diff --git a/test/integration/chains/bchn/utxo-integration.js b/test/integration/chains/bchn/utxo-integration.js index 0c78ce9..b05305d 100644 --- a/test/integration/chains/bchn/utxo-integration.js +++ b/test/integration/chains/bchn/utxo-integration.js @@ -28,7 +28,6 @@ describe('#UTXO', () => { assert.property(result[0], 'nullUtxos') assert.property(result[0], 'slpUtxos') assert.isArray(result[0].bchUtxos) - assert.isArray(result[0].slpUtxos) assert.isArray(result[0].nullUtxos) }) @@ -47,9 +46,29 @@ describe('#UTXO', () => { assert.property(result[0], 'nullUtxos') assert.property(result[0], 'slpUtxos') assert.isArray(result[0].bchUtxos) - assert.isArray(result[0].slpUtxos) assert.isArray(result[0].nullUtxos) }) + + it('should handle NFTs and minting batons', async () => { + const addr = 'simpleledger:qrm0c67wwqh0w7wjxua2gdt2xggnm90xwsr5k22euj' + + const result = await bchjs.Utxo.get(addr) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isArray(result) + assert.property(result[0], 'address') + assert.property(result[0], 'bchUtxos') + assert.property(result[0], 'nullUtxos') + assert.property(result[0], 'slpUtxos') + assert.isArray(result[0].bchUtxos) + assert.isArray(result[0].nullUtxos) + + assert.isArray(result[0].slpUtxos.type1.mintBatons) + assert.isArray(result[0].slpUtxos.type1.tokens) + assert.isArray(result[0].slpUtxos.nft.groupMintBatons) + assert.isArray(result[0].slpUtxos.nft.groupTokens) + assert.isArray(result[0].slpUtxos.nft.tokens) + }) }) }) diff --git a/test/unit/fixtures/utxo-mocks.js b/test/unit/fixtures/utxo-mocks.js index aeb519d..e261c59 100644 --- a/test/unit/fixtures/utxo-mocks.js +++ b/test/unit/fixtures/utxo-mocks.js @@ -125,8 +125,137 @@ const mockTwoHydratedAddrs = { ] } +const mockEveryUtxoType = { + slpUtxos: [ + { + utxos: [ + { + height: 674512, + tx_hash: + 'acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230', + tx_pos: 1, + value: 546, + txid: + 'acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230', + vout: 1, + utxoType: 'token', + tokenQty: '100', + tokenId: + 'acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230', + tokenTicker: 'SLPTEST', + tokenName: 'SLP Test Token', + tokenDocumentUrl: 'https://FullStack.cash', + tokenDocumentHash: '', + decimals: 8, + tokenType: 1, + isValid: true + }, + { + height: 674512, + tx_hash: + 'acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230', + tx_pos: 2, + value: 546, + txid: + 'acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230', + vout: 2, + utxoType: 'minting-baton', + tokenId: + 'acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230', + tokenTicker: 'SLPTEST', + tokenName: 'SLP Test Token', + tokenDocumentUrl: 'https://FullStack.cash', + tokenDocumentHash: '', + decimals: 8, + tokenType: 1, + isValid: true + }, + { + height: 674512, + tx_hash: + 'eeddccc4d716f04157ea132ac93a48040fea34a6b57f3d8f0cccb7d1a731ab2b', + tx_pos: 1, + value: 546, + txid: + 'eeddccc4d716f04157ea132ac93a48040fea34a6b57f3d8f0cccb7d1a731ab2b', + vout: 1, + utxoType: 'token', + tokenQty: '1', + tokenId: + 'eeddccc4d716f04157ea132ac93a48040fea34a6b57f3d8f0cccb7d1a731ab2b', + tokenTicker: 'NFT004', + tokenName: 'NFT Child', + tokenDocumentUrl: 'https://FullStack.cash', + tokenDocumentHash: '', + decimals: 0, + tokenType: 65, + isValid: true + }, + { + height: 674513, + tx_hash: + '705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04', + tx_pos: 1, + value: 546, + txid: + '705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04', + vout: 1, + utxoType: 'token', + tokenQty: '10', + transactionType: 'mint', + tokenId: + 'a9a2458a0f9f0761d5b8725c256f2e7fa35b9de4dec6f47b46e9f20d92d0e395', + tokenType: 129, + tokenTicker: 'NFTGT', + tokenName: 'NFT Test Group Token', + tokenDocumentUrl: 'https://FullStack.cash', + tokenDocumentHash: '', + decimals: 0, + mintBatonVout: 2, + isValid: true + }, + { + height: 674513, + tx_hash: + '705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04', + tx_pos: 2, + value: 546, + txid: + '705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04', + vout: 2, + utxoType: 'minting-baton', + transactionType: 'mint', + tokenId: + 'a9a2458a0f9f0761d5b8725c256f2e7fa35b9de4dec6f47b46e9f20d92d0e395', + tokenType: 129, + tokenTicker: 'NFTGT', + tokenName: 'NFT Test Group Token', + tokenDocumentUrl: 'https://FullStack.cash', + tokenDocumentHash: '', + decimals: 0, + mintBatonVout: 2, + isValid: true + }, + { + height: 674513, + tx_hash: + '705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04', + tx_pos: 3, + value: 38134, + txid: + '705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04', + vout: 3, + isValid: false + } + ], + address: 'bitcoincash:qrm0c67wwqh0w7wjxua2gdt2xggnm90xws00a3lezv' + } + ] +} + module.exports = { mockUtxoData, mockHydratedUtxos, - mockTwoHydratedAddrs + mockTwoHydratedAddrs, + mockEveryUtxoType } diff --git a/test/unit/utxo-unit.js b/test/unit/utxo-unit.js index 4b83dbd..589fbe9 100644 --- a/test/unit/utxo-unit.js +++ b/test/unit/utxo-unit.js @@ -34,7 +34,6 @@ describe('#utxo', () => { assert.property(result[0], 'nullUtxos') assert.property(result[0], 'slpUtxos') assert.isArray(result[0].bchUtxos) - assert.isArray(result[0].slpUtxos) assert.isArray(result[0].nullUtxos) }) @@ -57,7 +56,7 @@ describe('#utxo', () => { it('should handle an array of addresses', async () => { // Mock dependencies. - sandbox.stub(bchjs.Utxo.electrumx, 'utxo').resolves(mockData.mockUtxoData) + sandbox.stub(bchjs.Utxo.electrumx, 'utxo').resolves({ utxos: {} }) sandbox .stub(bchjs.Utxo.slp.Utils, 'hydrateUtxos') .resolves(mockData.mockTwoHydratedAddrs) @@ -77,7 +76,6 @@ describe('#utxo', () => { assert.property(result[0], 'nullUtxos') assert.property(result[0], 'slpUtxos') assert.isArray(result[0].bchUtxos) - assert.isArray(result[0].slpUtxos) assert.isArray(result[0].nullUtxos) }) @@ -97,5 +95,32 @@ describe('#utxo', () => { assert.include(err.message, 'Too many elements, 20 max.') } }) + + it('should handle NFTs and minting batons', async () => { + // Mock dependencies. + sandbox.stub(bchjs.Utxo.electrumx, 'utxo').resolves({ utxos: {} }) + sandbox + .stub(bchjs.Utxo.slp.Utils, 'hydrateUtxos') + .resolves(mockData.mockEveryUtxoType) + + const addr = 'simpleledger:qrm0c67wwqh0w7wjxua2gdt2xggnm90xwsr5k22euj' + + const result = await bchjs.Utxo.get(addr) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isArray(result) + assert.property(result[0], 'address') + assert.property(result[0], 'bchUtxos') + assert.property(result[0], 'nullUtxos') + assert.property(result[0], 'slpUtxos') + assert.isArray(result[0].bchUtxos) + assert.isArray(result[0].nullUtxos) + + assert.isArray(result[0].slpUtxos.type1.mintBatons) + assert.isArray(result[0].slpUtxos.type1.tokens) + assert.isArray(result[0].slpUtxos.nft.groupMintBatons) + assert.isArray(result[0].slpUtxos.nft.groupTokens) + assert.isArray(result[0].slpUtxos.nft.tokens) + }) }) })