Compare commits

..
12 Commits
10 changed files with 297 additions and 19 deletions
+15 -14
View File
@@ -9,12 +9,12 @@
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"@psf/bch-js": "6.5.7",
"@psf/bch-js": "6.7.1",
"axios": "0.27.2",
"bcryptjs": "2.4.3",
"glob": "7.1.6",
"ipfs": "0.62.3",
"ipfs-coord": "8.0.5",
"ipfs-coord": "8.0.8",
"ipfs-http-client": "55.0.0",
"jsonrpc-lite": "2.2.0",
"jsonwebtoken": "8.5.1",
@@ -1481,9 +1481,9 @@
"license": "BSD-3-Clause"
},
"node_modules/@psf/bch-js": {
"version": "6.5.7",
"resolved": "http://94.130.170.209:4873/@psf%2fbch-js/-/bch-js-6.5.7.tgz",
"integrity": "sha512-V9UweiRCCU4UzM51Ct2Szhpv6V0B686t5ZjMHNkPu8zyJ9+H7mQfteeQECHK5j38QHvLchfUOif5WWQxP6GRVQ==",
"version": "6.7.1",
"resolved": "http://94.130.170.209:4873/@psf%2fbch-js/-/bch-js-6.7.1.tgz",
"integrity": "sha512-B7qGxgXQ3CtIWyGpBOUIMylBe7Y3fAZl10kDgbYIiNeHw/W5q7nhczl5it1FtqigcwekOnETMMhjZdabZ/A+pw==",
"license": "MIT",
"dependencies": {
"@chris.troutner/bip32-utils": "1.0.5",
@@ -8840,9 +8840,10 @@
}
},
"node_modules/ipfs-coord": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/ipfs-coord/-/ipfs-coord-8.0.5.tgz",
"integrity": "sha512-V4pqR+MU290ZxFRE+Rx/7JuxR7LsOFBLuL0av3eVU5ZQWqeF/Zi9vh52zCqpp1IBCXi4QXNZGlysvm16xFDXww==",
"version": "8.0.8",
"resolved": "http://94.130.170.209:4873/ipfs-coord/-/ipfs-coord-8.0.8.tgz",
"integrity": "sha512-2ivZ7dCIrpS7VBN0DohaGBYRe3bzybkGbl0lOTyNeCUhrrYniC66dAnfdCY8CmpEeXoEYsaW2hLmohz4jDHywQ==",
"license": "MIT",
"dependencies": {
"axios": "0.21.4",
"bch-encrypt-lib": "2.0.0",
@@ -23176,9 +23177,9 @@
"integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA="
},
"@psf/bch-js": {
"version": "6.5.7",
"resolved": "http://94.130.170.209:4873/@psf%2fbch-js/-/bch-js-6.5.7.tgz",
"integrity": "sha512-V9UweiRCCU4UzM51Ct2Szhpv6V0B686t5ZjMHNkPu8zyJ9+H7mQfteeQECHK5j38QHvLchfUOif5WWQxP6GRVQ==",
"version": "6.7.1",
"resolved": "http://94.130.170.209:4873/@psf%2fbch-js/-/bch-js-6.7.1.tgz",
"integrity": "sha512-B7qGxgXQ3CtIWyGpBOUIMylBe7Y3fAZl10kDgbYIiNeHw/W5q7nhczl5it1FtqigcwekOnETMMhjZdabZ/A+pw==",
"requires": {
"@chris.troutner/bip32-utils": "1.0.5",
"@psf/bip21": "2.0.1",
@@ -28416,9 +28417,9 @@
}
},
"ipfs-coord": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/ipfs-coord/-/ipfs-coord-8.0.5.tgz",
"integrity": "sha512-V4pqR+MU290ZxFRE+Rx/7JuxR7LsOFBLuL0av3eVU5ZQWqeF/Zi9vh52zCqpp1IBCXi4QXNZGlysvm16xFDXww==",
"version": "8.0.8",
"resolved": "http://94.130.170.209:4873/ipfs-coord/-/ipfs-coord-8.0.8.tgz",
"integrity": "sha512-2ivZ7dCIrpS7VBN0DohaGBYRe3bzybkGbl0lOTyNeCUhrrYniC66dAnfdCY8CmpEeXoEYsaW2hLmohz4jDHywQ==",
"requires": {
"axios": "0.21.4",
"bch-encrypt-lib": "2.0.0",
+2 -2
View File
@@ -23,12 +23,12 @@
},
"repository": "Permissionless-Software-Foundation/ipfs-bch-wallet-consumer",
"dependencies": {
"@psf/bch-js": "6.5.7",
"@psf/bch-js": "6.7.1",
"axios": "0.27.2",
"bcryptjs": "2.4.3",
"glob": "7.1.6",
"ipfs": "0.62.3",
"ipfs-coord": "8.0.5",
"ipfs-coord": "8.0.8",
"ipfs-http-client": "55.0.0",
"jsonrpc-lite": "2.2.0",
"jsonwebtoken": "8.5.1",
+54
View File
@@ -208,6 +208,60 @@ class BchAdapter {
}
}
// Query the UTXOs associated with an array of up to 20 addresses.
async getUtxosBulk (addrs) {
try {
// console.log('getUtxosBulk() addrs: ', addrs)
// Throw an error if this IPFS node has not yet made a connection to a
// wallet service provider.
const selectedProvider =
this.ipfs.ipfsCoordAdapter.state.selectedServiceProvider
if (!selectedProvider) {
throw new Error('No BCH Wallet Service provider available yet.')
}
// Input validation
if (!Array.isArray(addrs)) {
throw new Error('addresses parameter must be an array')
}
if (addrs.length > 20) {
throw new Error('addresses parameter must not exceed 20 elements')
}
const rpcData = {
endpoint: 'utxosBulk',
addresses: addrs
}
// Generate a UUID for the call.
const rpcId = this.uid()
// Generate a JSON RPC command.
const cmd = this.jsonrpc.request(rpcId, 'bch', rpcData)
const cmdStr = JSON.stringify(cmd)
// console.log('cmdStr: ', cmdStr)
// Send the RPC command to selected wallet service.
const thisNode = this.ipfs.ipfsCoordAdapter.ipfsCoord.thisNode
await this.ipfs.ipfsCoordAdapter.ipfsCoord.useCases.peer.sendPrivateMessage(
selectedProvider,
cmdStr,
thisNode
)
// Wait for data to come back from the wallet service.
const data = await this.waitForRPCResponse(rpcId)
// console.log(`getUtxosBulk() data: ${JSON.stringify(data, null, 2)}`)
return data
} catch (err) {
// console.log('createUser() error: ', err)
wlogger.error('Error in use-cases/bch.js/getUtxosBulk()')
throw err
}
}
async broadcast (hex) {
try {
// Throw an error if this IPFS node has not yet made a connection to a
+6
View File
@@ -214,6 +214,8 @@ class IpfsCoordAdapter {
// providers. This method is called periodically by a timer-interval.
pollForBchServices () {
try {
// console.log('pollForBchServices() polling for BCH service')
// An array of IPFS IDs of other nodes in the coordination pubsub channel.
const peers = _this.ipfsCoord.thisNode.peerList
// console.log(`peers: ${JSON.stringify(peers, null, 2)}`)
@@ -255,6 +257,8 @@ class IpfsCoordAdapter {
console.log(`---->BCH wallet service selected: ${thisPeer}`)
}
// console.log('preferredProvider: ', _this.config.preferredProvider)
// If a preferred provider is set in the config file, then connect
// to the preferred provider when it's discovered.
if (
@@ -264,6 +268,8 @@ class IpfsCoordAdapter {
_this.state.selectedServiceProvider = thisPeer
}
// console.log('selectedServiceProvider: ', _this.state.selectedServiceProvider)
// Check if the peer has already been added to the list of providers.
const alreadyExists = _this.state.serviceProviders.filter(
(x) => x === thisPeer
@@ -234,6 +234,103 @@ class BchRESTControllerLib {
}
}
/**
* @api {post} /bch/utxosBulk UTXOs Bulk
* @apiName UTXOs Bulk
* @apiGroup REST BCH
* @apiDescription This endpoint returns UTXOs held at an address, hydrated
* with token information.
* @apiDescription This endpoint is the same as the /bch/utxos endpoint, but
* it allows an array of up to 20 addresses to be queried at the same time.
* This reduces the number of JSON RPC calls, and is very handy
* for HD wallets that need to quickly scan a lot of addresses.
*
* Given an address, this endpoint will return an object with thre following
* properties:
*
* - addresses: [] - the addresses 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:
* curl -H "Content-Type: application/json" -X POST -d '{ "address": "bitcoincash:qrl2nlsaayk6ekxn80pq0ks32dya8xfclyktem2mqj" }' localhost:5001/bch/utxos
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* [
* {
* "addresses": ["bitcoincash:qrl2nlsaayk6ekxn80pq0ks32dya8xfclyktem2mqj"],
* "bchUtxos":[
* {
* "height":631219,
* "tx_hash":"ae2daa01c8172545b5edd205ea438706bcb74e63d4084a26b9ff2a46d46dc97f",
* "tx_pos":0,
* "value":1000,
* "txid":"ae2daa01c8172545b5edd205ea438706bcb74e63d4084a26b9ff2a46d46dc97f",
* "vout":0,
* "isValid":false
* }
* ],
* "nullUtxos":[
*
* ],
* "slpUtxos":{
* "type1":{
* "mintBatons":[
*
* ],
* "tokens":[
*
* ]
* },
* "nft":{
* "groupMintBatons":[
*
* ],
* "groupTokens":[
*
* ],
* "tokens":[
*
* ]
* }
* }
* }
* ]
*
* @apiError UnprocessableEntity Missing required parameters
*
* @apiErrorExample {json} Error-Response:
* HTTP/1.1 422 Unprocessable Entity
* {
* "status": 422,
* "error": "Unprocessable Entity"
* }
*/
async utxosBulk (ctx) {
try {
const addrs = ctx.request.body.addresses
// console.log('address: ', address)
const utxos = await this.adapters.bch.getUtxosBulk(addrs)
// console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
ctx.body = utxos
} catch (err) {
this.handleError(ctx, err)
}
}
/**
* @api {post} /bch/broadcast Broadcast
* @apiName Broadcast
@@ -609,11 +706,13 @@ class BchRESTControllerLib {
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X POST -d '{ "tokenId": "43eddfb11c9941edffb8c8815574bb0a43969a7b1de39ad14cd043eaa24fd38d" }' https://bc01-ca-bch-consumer.fullstackcash.nl/bch/getTokenData2
* curl -H "Content-Type: application/json" -X POST -d '{ "tokenId": "43eddfb11c9941edffb8c8815574bb0a43969a7b1de39ad14cd043eaa24fd38d", "updateCache": true }' https://bc01-ca-bch-consumer.fullstackcash.nl/bch/getTokenData2
*/
async getTokenData2 (ctx) {
try {
const tokenId = ctx.request.body.tokenId
const updateCache = ctx.request.body.updateCache
console.log('getTokenData2() updateCache: ', updateCache)
const data = await this.adapters.bch.getTokenData2(tokenId, updateCache)
console.log(`getTokenData2 data: ${JSON.stringify(data, null, 2)}`)
+5
View File
@@ -55,6 +55,7 @@ class BchRouter {
this.router.post('/provider', this.postProvider)
this.router.post('/balance', this.postBalance)
this.router.post('/utxos', this.postUtxos)
this.router.post('/utxosBulk', this.postUtxosBulk)
this.router.post('/broadcast', this.postBroadcast)
this.router.post('/txHistory', this.postTxHistory)
this.router.post('/txData', this.postTxData)
@@ -84,6 +85,10 @@ class BchRouter {
await _this.bchRESTController.utxos(ctx, next)
}
async postUtxosBulk (ctx, next) {
await _this.bchRESTController.utxosBulk(ctx, next)
}
async postBroadcast (ctx, next) {
await _this.bchRESTController.broadcast(ctx, next)
}
-3
View File
@@ -1,3 +0,0 @@
/key/swarm/psk/1.0.0/
/base16/
bbd935b70105b03ebd0c6a3c2d2730cd22fc0d18c490ccf689b6c8a22e1bed2a
+76
View File
@@ -165,6 +165,82 @@ describe('#bch-use-case', () => {
})
})
describe('#getUtxos', () => {
it('should get the UTXOs of an address', async () => {
// Force connection to a wallet service
uut.ipfs.ipfsCoordAdapter.state = {
selectedServiceProvider: 'abc123'
}
// Mock depenencies
sandbox.stub(uut, 'waitForRPCResponse').resolves({ key: 'value' })
const addr = 'addr'
const result = await uut.getUtxos(addr)
// console.log('result: ', result)
assert.equal(result.key, 'value')
})
it('should catch and throw an error', async () => {
try {
await uut.getUtxos()
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.equal(err.message, 'test error')
}
})
})
describe('#getUtxosBulk', () => {
it('should get the UTXOs of an address', async () => {
// Force connection to a wallet service
uut.ipfs.ipfsCoordAdapter.state = {
selectedServiceProvider: 'abc123'
}
// Mock depenencies
sandbox.stub(uut, 'waitForRPCResponse').resolves({ key: 'value' })
const addrs = ['addr']
const result = await uut.getUtxosBulk(addrs)
// console.log('result: ', result)
assert.equal(result.key, 'value')
})
it('should throw an error if addresses is not an array', async () => {
try {
await uut.getUtxosBulk()
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.equal(err.message, 'addresses parameter must be an array')
}
})
it('should throw an error if addresses array is larger than 20 elements', async () => {
try {
const addrs = []
for (let i = 0; i < 25; i++) {
addrs.push(i)
}
await uut.getUtxosBulk(addrs)
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.equal(err.message, 'addresses parameter must not exceed 20 elements')
}
})
})
describe('#broadcast', () => {
it('should broadcast a transaction', async () => {
// Force connection to a wallet service
@@ -177,6 +177,42 @@ describe('#BCH-REST-Controller', () => {
})
})
describe('#utxosBulk', () => {
it('should return 422 status on arbitrary error', async () => {
try {
// Force an error
sandbox
.stub(uut.adapters.bch, 'getUtxosBulk')
.rejects(new Error('test error'))
ctx.request.body = {
address: 'blah'
}
await uut.utxosBulk(ctx)
assert.fail('Unexpected result')
} catch (err) {
console.log('err: ', err)
assert.equal(err.status, 422)
assert.include(err.message, 'test error')
}
})
it('should return 200 status on success', async () => {
sandbox.stub(uut.adapters.bch, 'getUtxosBulk').resolves({ status: 200 })
ctx.request.body = {
address: 'blah'
}
await uut.utxosBulk(ctx)
// Assert the expected HTTP response
assert.equal(ctx.status, 200)
})
})
describe('#broadcast', () => {
it('should return 422 status on arbitrary error', async () => {
try {
+4
View File
@@ -93,6 +93,10 @@ class BchUseCaseMock {
return {}
}
async getUtxosBulk () {
return {}
}
async broadcast () {
return {}
}