diff --git a/src/adapters/bch.js b/src/adapters/bch.js index 3533b16..043d976 100644 --- a/src/adapters/bch.js +++ b/src/adapters/bch.js @@ -66,6 +66,9 @@ class Bch { // } // } + /** DEV NOTE: This function are deprecated.? + * https://github.com/Permissionless-Software-Foundation/bch-message-lib/pull/41 + */ async getMerit (slpAddr) { try { if (!slpAddr || typeof slpAddr !== 'string') { diff --git a/src/adapters/wallet.js b/src/adapters/wallet.js index 18ea4ef..92530ad 100644 --- a/src/adapters/wallet.js +++ b/src/adapters/wallet.js @@ -34,6 +34,7 @@ class WalletAdapter { this.config = config this.bitcoinJs = bitcoinJs this.BchWallet = BchWallet + this.BchTokenSweep = BchTokenSweep this.bchWallet = {} // Will be replaced when initialized. // this.advancedConfig = localConfig.advancedConfig this.retryQueue = new RetryQueue({ @@ -205,7 +206,9 @@ class WalletAdapter { // Generate a cryptographic signature, required to write to the P2WDB. async generateSignature (message) { try { - // TODO: Add input validation for message. + if (!message || typeof message !== 'string') { + throw new Error('message is required!') + } const privKey = this.bchWallet.walletInfo.privateKey @@ -370,6 +373,12 @@ class WalletAdapter { async moveTokens (inObj = {}) { try { const { tokenId, qty } = inObj + if (!tokenId || typeof tokenId !== 'string') { + throw new Error('tokenId must be a string!') + } + if (!qty || typeof qty !== 'number') { + throw new Error('qty must be a number!') + } const keyPair = await this.getKeyPair() console.log('keyPair: ', keyPair) @@ -392,6 +401,10 @@ class WalletAdapter { x => x.tokenId === tokenId ) + if (tokenUtxos.length === 0) { + throw new Error('tokenId not found!') + } + const txid = await this.bchWallet.sendTokens(receiver, 3) const utxoInfo = { @@ -412,6 +425,10 @@ class WalletAdapter { // generate a segregated UTXO. async moveBch (amountSat) { try { + if (!amountSat) { + throw new Error('amountSat is required!') + } + const keyPair = await this.getKeyPair() console.log('keyPair: ', keyPair) @@ -444,6 +461,10 @@ class WalletAdapter { // representing the transaction. async deseralizeTx (txHex) { try { + // Input validation + if (!txHex || typeof txHex !== 'string') { + throw new Error('hex must be a string!') + } // Ensure the URL points at FullStack.cash, since the web 3 infra does not // yet support this call. const oldUrl = this.bchWallet.bchjs.RawTransactions.restURL @@ -555,8 +576,12 @@ class WalletAdapter { // Given an Order database object, this function sends the tokens from the // HD index where they are stored, back to the root address of the wallet. - async reclaimTokens (orderData) { + async reclaimTokens (orderData = {}) { try { + if (!orderData.hdIndex) { + throw new Error('orderData.hdIndex is required!') + } + // Get the key pair from the Order. const keyPair = await this.getKeyPair(orderData.hdIndex) console.log('keyPair: ', keyPair) @@ -576,7 +601,7 @@ class WalletAdapter { // const txid = await tempWallet.sendTokens(receiver) // Sweep the tokens to the root address. - const sweeper = new BchTokenSweep( + const sweeper = new this.BchTokenSweep( keyPair.wif, this.bchWallet.walletInfo.privateKey, this.bchWallet, diff --git a/src/adapters/webhook.js b/src/adapters/webhook.js index 11d1f10..f3b4e73 100644 --- a/src/adapters/webhook.js +++ b/src/adapters/webhook.js @@ -6,13 +6,15 @@ import config from '../../config/index.js' import axios from 'axios' -let _this - class WebHook { constructor () { - _this = this - _this.config = config - _this.axios = axios + this.config = config + this.axios = axios + this.sleepTime = 2000 + this.createWebhook = this.createWebhook.bind(this) + this.deleteWebhook = this.deleteWebhook.bind(this) + this.waitUntilSuccess = this.waitUntilSuccess.bind(this) + this.sleep = this.sleep.bind(this) } // REST petition to create a webhook in p2wdb-service @@ -22,14 +24,14 @@ class WebHook { throw new Error('url must be a string') } - const endpoint = _this.config.webhookService + const endpoint = this.config.webhookService const obj = { appId: this.config.p2wdbAppId, url } - const result = await axios.post(endpoint, obj) + const result = await this.axios.post(endpoint, obj) return result.data } catch (err) { @@ -45,14 +47,14 @@ class WebHook { throw new Error('url must be a string') } - const endpoint = _this.config.webhookService + const endpoint = this.config.webhookService const obj = { appId: this.config.p2wdbAppId, url } - const result = await axios.delete(endpoint, { data: obj }) + const result = await this.axios.delete(endpoint, { data: obj }) return result.data } catch (err) { @@ -86,7 +88,7 @@ class WebHook { } catch (err) { const now = new Date() console.log(`${now.toLocaleString()}: Error trying to create webhook with P2WDB. Trying again...`) - await sleep(2000) + await this.sleep(this.sleepTime) } } while (!success) @@ -96,10 +98,10 @@ class WebHook { throw err } } -} -function sleep (ms) { - return new Promise(resolve => setTimeout(resolve, ms)) + async sleep (ms) { + return new Promise(resolve => setTimeout(resolve, ms)) + } } export default WebHook diff --git a/test/unit/adapters/adapters-index-unit.js b/test/unit/adapters/adapters-index-unit.js index 6d244f5..97bab08 100644 --- a/test/unit/adapters/adapters-index-unit.js +++ b/test/unit/adapters/adapters-index-unit.js @@ -24,19 +24,18 @@ describe('#adapters', () => { }) describe('#start', () => { - // it('should start the async adapters', async () => { - // // Mock dependencies - // uut.config.getJwtAtStartup = true - // uut.config.useIpfs = true - // uut.config.env = 'not-a-test' - // sandbox.stub(uut.fullStackJwt, 'getJWT').resolves() - // sandbox.stub(uut.fullStackJwt, 'instanceBchjs').resolves() - // sandbox.stub(uut.ipfs, 'start').resolves() + it('should start the async adapters', async () => { + // Mock dependencies + uut.config.getJwtAtStartup = true + uut.config.useIpfs = true + uut.config.env = 'not-a-test' + sandbox.stub(uut.wallet, 'instanceWallet').resolves() + sandbox.stub(uut.ipfs, 'start').resolves() - // const result = await uut.start() + const result = await uut.start() - // assert.equal(result, true) - // }) + assert.equal(result, true) + }) it('should not start ipfs on test enviroment', async () => { // Mock dependencies diff --git a/test/unit/adapters/bch.adapter.unit.js b/test/unit/adapters/bch.adapter.unit.js index 3e6a02c..123edcc 100644 --- a/test/unit/adapters/bch.adapter.unit.js +++ b/test/unit/adapters/bch.adapter.unit.js @@ -135,18 +135,19 @@ describe('bch', () => { } }) - // it('should return the merit ', async () => { - // try { - // // Mock live network calls. - // sandbox.stub(uut.msgLib.merit, 'agMerit').resolves(100) - // - // const slpAddr = - // 'simpleledger:qqgnksc6zr4nzxrye69fq625wu2myxey6uh9kzjy96' - // const merit = await uut.getMerit(slpAddr) - // assert.isNumber(merit) - // } catch (err) { - // assert.fail('Unexpected result') - // } - // }) + /* it('should return the merit ', async () => { + try { + // Mock live network calls. + sandbox.stub(uut.msgLib.merit, 'agMerit').resolves(100) + + const slpAddr = + 'simpleledger:qqgnksc6zr4nzxrye69fq625wu2myxey6uh9kzjy96' + const merit = await uut.getMerit(slpAddr) + assert.isNumber(merit) + } catch (err) { + console.log('err: ', err) + assert.fail('Unexpected result') + } + }) */ }) }) diff --git a/test/unit/adapters/nostr.adapter.unit.js b/test/unit/adapters/nostr.adapter.unit.js index 341512b..9a0b5e0 100644 --- a/test/unit/adapters/nostr.adapter.unit.js +++ b/test/unit/adapters/nostr.adapter.unit.js @@ -128,4 +128,100 @@ describe('#nostr.js', () => { assert.equal(result.length, 1) }) }) + describe('#eventId2note', () => { + it('should handle encode error', async () => { + try { + await uut.eventId2note('invalid format') + + assert.fail('Unexpected code path') + } catch (err) { + // console.log('err: ', err) + assert.include(err.message, 'Invalid byte sequence') + } + }) + + it('should handle valid eventId', async () => { + const result = await uut.eventId2note('b1e4a3d5e40b6d0b3f1e2c234b6c9dbf4d6a80e9b3e8a2d765c7f9ed84c9a5ef') + assert.isString(result) + assert.include(result, 'note') + }) + }) + describe('#pubkey2npub', () => { + it('should handle encode error', async () => { + try { + await uut.pubkey2npub('invalid format') + + assert.fail('Unexpected code path') + } catch (err) { + // console.log('err: ', err) + assert.include(err.message, 'Invalid byte sequence') + } + }) + + it('should handle valid pubkey', async () => { + const result = await uut.pubkey2npub('f5a1b39d2cfae8e10e93b9b4d7ec8248eaf45fbe4a73409d8d4c67015e9729a3') + assert.isString(result) + assert.include(result, 'npub') + }) + }) + describe('#npub2pubkey', () => { + it('should handle decode error', async () => { + try { + await uut.npub2pubkey('npub17ksm88fvlt5wzr5nhx6d0myzfr40g') + + assert.fail('Unexpected code path') + } catch (err) { + // console.log('err: ', err) + assert.include(err.message, 'Invalid checksum') + } + }) + + it('should handle valid pubkey', async () => { + const result = await uut.npub2pubkey('npub17ksm88fvlt5wzr5nhx6d0myzfr40gha7ffe5p8vdf3nszh5h9x3sdwl7u2') + assert.isObject(result) + assert.property(result, 'type') + assert.property(result, 'data') + assert.equal(result.type, 'npub') + assert.equal(result.data.length, 64) + }) + }) + describe('#readGlobalFeed', () => { + it('should throw an error if provided limit is wrong', async () => { + try { + await uut.readGlobalFeed({ limit: -5 }) + + assert.fail('Unexpected code path') + } catch (err) { + // console.log('err: ', err) + assert.include(err.message, 'Limit must be greater than 0') + } + }) + + it('should read message', async () => { + const result = await uut.readGlobalFeed() + assert.isArray(result) + }) + + it('should read message with custom limit', async () => { + const result = await uut.readGlobalFeed({ limit: 1 }) + assert.isArray(result) + }) + }) + describe('#getFollowers', () => { + it('should throw an error if no pubkey is provided', async () => { + try { + await uut.getFollowers() + + assert.fail('Unexpected code path') + } catch (err) { + // console.log('err: ', err) + assert.include(err.message, 'pubkey must be a string!') + } + }) + + it('should get followers', async () => { + const result = await uut.getFollowers({ pubkey: 'f5a1b39d2cfae8e10e93b9b4d7ec8248eaf45fbe4a73409d8d4c67015e9729a3' }) + assert.isArray(result) + }) + }) }) diff --git a/test/unit/adapters/wallet.unit.js b/test/unit/adapters/wallet.unit.js index e6d3ed3..a658d53 100644 --- a/test/unit/adapters/wallet.unit.js +++ b/test/unit/adapters/wallet.unit.js @@ -9,24 +9,56 @@ import sinon from 'sinon' import WalletAdapter from '../../../src/adapters/wallet.js' import { MockBchWallet } from '../mocks/adapters/wallet.js' +import { BchTokenSweepMock } from '../mocks/bch-token-sweep-mock.js' +import offerMockData from '../mocks/use-cases/offer-mock-data.js' +import fs from 'fs' +import * as url from 'url' + +const __dirname = url.fileURLToPath(new URL('.', import.meta.url)) + +const testWalletFile = `${__dirname.toString()}/../../../test-wallet.json` describe('#wallet', () => { let uut let sandbox + let testWalletData + + before(() => { + // try to remove the test wallet file + try { + fs.unlinkSync(testWalletFile) + } catch (error) { + console.log('error fsUnlinkSync: ', error) + + // silently fail + } + }) + after(() => { + // try to remove the test wallet file + try { + fs.unlinkSync(testWalletFile) + } catch (error) { + console.log('error fsUnlinkSync: ', error) + // silently fail + } + }) beforeEach(() => { uut = new WalletAdapter() + uut.BchWallet = MockBchWallet uut.bchWallet = new MockBchWallet() + uut.BchTokenSweep = BchTokenSweepMock + uut.WALLET_FILE = testWalletFile uut.bitcoinJs = { Transaction: { - fromHex: () => { return { } }, - SIGHASH_ALL: () => { return { } } + fromHex: () => { return {} }, + SIGHASH_ALL: () => { return {} } }, TransactionBuilder: { - fromTransaction: () => { return { sign: () => { return { } }, build: () => { return { toHex: () => { return 'hex' } } } } } + fromTransaction: () => { return { sign: () => { return {} }, build: () => { return { toHex: () => { return 'hex' } } } } } }, ECPair: { - fromWIF: () => { return { } } + fromWIF: () => { return {} } } } sandbox = sinon.createSandbox() @@ -34,6 +66,278 @@ describe('#wallet', () => { afterEach(() => sandbox.restore()) + describe('#openWallet', () => { + it('should create a new wallet when wallet file does not exist', async () => { + // Ensure we open the test file, not the production wallet file. + const result = await uut.openWallet() + testWalletData = result + // console.log('result: ', result) + assert.property(result, 'mnemonic') + assert.property(result, 'privateKey') + assert.property(result, 'publicKey') + assert.property(result, 'cashAddress') + assert.property(result, 'address') + assert.property(result, 'slpAddress') + assert.property(result, 'legacyAddress') + assert.property(result, 'hdPath') + }) + + it('should open existing wallet file', async () => { + // This test case uses the file created in the previous test case. + // Ensure we open the test file, not the production wallet file. + const result = await uut.openWallet() + // console.log('result: ', result) + assert.property(result, 'mnemonic') + assert.property(result, 'privateKey') + assert.property(result, 'publicKey') + assert.property(result, 'cashAddress') + assert.property(result, 'address') + assert.property(result, 'slpAddress') + assert.property(result, 'legacyAddress') + assert.property(result, 'hdPath') + }) + + it('should catch and throw an error', async () => { + try { + uut.WALLET_FILE = '' + // Force an error + uut.BchWallet = () => { + } + await uut.openWallet() + // console.log('result: ', result) + assert.fail('Unexpected code path') + } catch (err) { + // console.log('err: ', err) + assert.include(err.message, 'this.BchWallet is not a constructor') + } + }) + }) + describe('#instanceWallet', () => { + it('should create an instance of BchWallet with rest-api', async () => { + uut.config.useFullStackCash = true + const result = await uut.instanceWallet(testWalletData) + assert.equal(result.advancedOptions.interface, 'rest-api') + }) + + it('should create an instance of BchWallet with consumer-api', async () => { + uut.config.useFullStackCash = false + const result = await uut.instanceWallet(testWalletData) + assert.equal(result.advancedOptions.interface, 'consumer-api') + }) + it('should catch and throw an error if no mnemonic is provided', async () => { + try { + // Force an error + await uut.instanceWallet({}) + assert.fail('Unexpected code path') + } catch (err) { + // console.log('err: ', err) + assert.include(err.message, 'Wallet data is not formatted correctly. Can not read mnemonic in wallet file!') + } + }) + }) + describe('#incrementNextAddress', () => { + it('should increment the nextAddress property', async () => { + const result = await uut.incrementNextAddress() + assert.equal(result, 2) + }) + + it('should catch and throw an error', async () => { + try { + // Force an error + sandbox.stub(uut, 'openWallet').rejects(new Error('test error')) + await uut.incrementNextAddress() + assert.fail('Unexpected code path') + } catch (err) { + assert.include(err.message, 'test error') + } + }) + }) + + describe('#getKeyPair', () => { + it('should return an object with a key pair', async () => { + // Ensure we open the test file, not the production wallet file. + // uut.WALLET_FILE = testWalletFile + uut.config.walletFile = testWalletFile + + const result = await uut.getKeyPair() + // console.log('result: ', result) + + assert.property(result, 'cashAddress') + assert.property(result, 'wif') + assert.property(result, 'hdIndex') + }) + + it('should catch and throw an error', async () => { + try { + // Force an error + sandbox + .stub(uut, 'incrementNextAddress') + .rejects(new Error('test error')) + await uut.getKeyPair() + assert.fail('Unexpected code path') + } catch (err) { + assert.include(err.message, 'test error') + } + }) + }) + describe('#generateSignature', () => { + it('should sign message', async () => { + const result = await uut.generateSignature('unit test message') + // console.log('result: ', result) + + assert.isString(result) + }) + + it('should catch and throw an error', async () => { + try { + await uut.generateSignature() + } catch (err) { + assert.include(err.message, 'message is required!') + } + }) + }) + describe('#generatePartialTx', () => { + it('should generate tx for fungible token', async () => { + const utxoInfo = { + txid: '241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87', + vout: 0, + sats: 1000, + wif: 'L2fzmF5hCeTZRVnr7Cyu66ucJAAKhkhkfinGVuUAKDtvAfwEx8a3' + } + + sandbox.stub(uut.bchWallet, 'getTxData').resolves([offerMockData.fungibleTokenData01.genesisData]) + const result = await uut.generatePartialTx(offerMockData.fungibleOffer01, utxoInfo) + + assert.isString(result) + }) + it('should generate tx for nft', async () => { + const utxoInfo = { + txid: '241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87', + vout: 0, + sats: 1000, + wif: 'L2fzmF5hCeTZRVnr7Cyu66ucJAAKhkhkfinGVuUAKDtvAfwEx8a3' + } + + sandbox.stub(uut.bchWallet, 'getTxData').resolves([offerMockData.nftTokenData01.genesisData]) + const result = await uut.generatePartialTx(offerMockData.nftOffer01, utxoInfo) + // console.log('result: ', result) + + assert.isString(result) + }) + + it('should handle unknown token type', async () => { + try { + const utxoInfo = { + txid: '241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87', + vout: 0, + sats: 1000, + wif: 'L2fzmF5hCeTZRVnr7Cyu66ucJAAKhkhkfinGVuUAKDtvAfwEx8a3' + } + + const offerMock = Object.assign({}, offerMockData.nftOffer01) + offerMock.tokenType = 100 + await uut.generatePartialTx(offerMock, utxoInfo) + } catch (err) { + assert.include(err.message, 'Unknown token type') + } + }) + it('should handle multiple outputs', async () => { + const utxoInfo = { + txid: '241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87', + vout: 0, + sats: 1000, + wif: 'L2fzmF5hCeTZRVnr7Cyu66ucJAAKhkhkfinGVuUAKDtvAfwEx8a3' + } + + sandbox.stub(uut.bchWallet, 'getTxData').resolves([offerMockData.nftTokenData01.genesisData]) + sandbox.stub(uut.bchWallet.bchjs.SLP.NFT1, 'generateNFTChildSendOpReturn').returns({ outputs: 5 }) + const res = await uut.generatePartialTx(offerMockData.nftOffer01, utxoInfo) + assert.isUndefined(res) + }) + }) + + describe('#moveTokens', () => { + it('should move tokens', async () => { + sandbox.stub(uut, 'getKeyPair').resolves({ + cashAddress: 'cashAddress', + wif: 'wif', + hdIndex: 11 + }) + const input = { + tokenId: uut.bchWallet.utxos.utxoStore.slpUtxos.type1.tokens[0].tokenId, + qty: 1 + } + const res = await uut.moveTokens(input) + assert.isObject(res) + }) + + it('should throw an error if the tokenId is not found', async () => { + try { + sandbox.stub(uut, 'getKeyPair').resolves({ + cashAddress: 'cashAddress', + wif: 'wif', + hdIndex: 11 + }) + const input = { + tokenId: '241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87', + qty: 1 + } + await uut.moveTokens(input) + assert.fail('Unexpected code path') + } catch (error) { + assert.equal(error.message, 'tokenId not found!') + } + }) + + it('should throw an error if the tokenId is not provided', async () => { + try { + await uut.moveTokens({ + qty: 1 + }) + assert.fail('Unexpected code path') + } catch (error) { + assert.equal(error.message, 'tokenId must be a string!') + } + }) + it('should throw an error if the qty is not provided', async () => { + try { + await uut.moveTokens({ + tokenId: 'tokenId' + }) + assert.fail('Unexpected code path') + } catch (error) { + assert.equal(error.message, 'qty must be a number!') + } + }) + }) + + describe('#moveBch', () => { + it('should move bch', async () => { + sandbox.stub(uut, 'getKeyPair').resolves({ + cashAddress: 'cashAddress', + wif: 'wif', + hdIndex: 11 + }) + + const res = await uut.moveBch(100) + assert.isObject(res) + }) + + it('should throw an error if the amountSat is not provided', async () => { + try { + sandbox.stub(uut, 'getKeyPair').resolves({ + cashAddress: 'cashAddress', + wif: 'wif', + hdIndex: 11 + }) + + await uut.moveBch() + assert.fail('Unexpected code path') + } catch (error) { + assert.equal(error.message, 'amountSat is required!') + } + }) + }) describe('#moveTokensFromCustomWallet', () => { it('should move tokens from a custom wallet', async () => { sandbox.stub(uut, 'getKeyPair').resolves({ @@ -100,28 +404,27 @@ describe('#wallet', () => { } }) }) + describe('#deseralizeTx', () => { + it('should throw an error if the hex is not provided', async () => { + try { + await uut.deseralizeTx() + assert.fail('Unexpected code path') + } catch (error) { + assert.equal(error.message, 'hex must be a string!') + } + }) + it('should decode hex', async () => { + try { + sandbox.stub(uut.bchWallet.bchjs.RawTransactions, 'decodeRawTransaction').resolves({}) + const hex = '020000000286b2959cccdb80163b68bb1360abac8e0228ba7b70c779443168cc65c7c42d820100000000ffffffff879b066729eda979f1f062c83b7eccdb9e77f49b417e4723864b3861bf061c24000000006b483045022100b99c6161e18e4e34c1e438b6b03e808703695f217b30da42c3f787798a9f40cc02206d26b4f6799735779d4fac823bc60d276fbb3cea91c66dbfa136cf5b1bd0d8324121028dcd38a58c8295dbd105ef524014f77af4cddfa19b26c50926b4aaeebd95c7ceffffffff030000000000000000376a04534c500001010453454e4420a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b208000000000000006422020000000000001976a914bef6c5180648165704f22e71a1b011ba56336aee88acf81d0000000000001976a914c1f16a3876f8fbe3701a66d3cb3b9c7abb07a06c88ac00000000' + const result = await uut.deseralizeTx(hex) + assert.isObject(result) + } catch (error) { + assert.fail('Unexpected code path') + } + }) + }) describe('#completeTx', () => { - // TODO: This test needs to be refactored to use the new instanceWallet() method for loading the user wallet. - - // it('should complete a transaction', async () => { - // sandbox.stub(uut, 'getKeyPair').resolves({ - // cashAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00', - // wif: 'L5D2UAam8tvo3uii5kpgaGyjvVMimdrXu8nWGQSQjuuAix6ji1YQ', - // hdIndex: 11 - // }) - // sandbox.stub(uut, 'deseralizeTx').resolves({ - // txid: 'complete tx id result' - // }) - // sandbox.stub(uut.retryQueue, 'addToQueue').resolves('complete tx id result') - - // const hex = 'hex' - // const hdIndex = 11 - // const mnemonic = 'course abstract aerobic deer try switch turtle diet fence affair butter top' - - // const txid = await uut.completeTx(hex, hdIndex, mnemonic) - // assert.equal(txid, 'complete tx id result') - // }) - it('should throw an error if the hex is not provided', async () => { try { const hdIndex = 11 @@ -134,11 +437,60 @@ describe('#wallet', () => { it('should throw an error if the hdIndex is not provided', async () => { try { const hex = 'hex' - await uut.completeTx(hex, null) + await uut.completeTx(hex) assert.fail('Unexpected code path') } catch (error) { assert.equal(error.message, 'hdIndex must be a non-negative number!') } }) + it('should throw an error if the mnemonic is not provided', async () => { + try { + const hex = 'hex' + const hdIndex = 11 + await uut.completeTx(hex, hdIndex) + assert.fail('Unexpected code path') + } catch (error) { + assert.equal(error.message, 'mnemonic must be a string!') + } + }) + it('should complete a transaction', async () => { + sandbox.stub(uut, 'getKeyPair').resolves({ + cashAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00', + wif: 'L5D2UAam8tvo3uii5kpgaGyjvVMimdrXu8nWGQSQjuuAix6ji1YQ', + hdIndex: 11 + }) + sandbox.stub(uut, 'deseralizeTx').resolves({ + txid: 'complete tx id result' + }) + sandbox.stub(uut.retryQueue, 'addToQueue').resolves('complete tx id result') + + const hex = 'hex' + const hdIndex = 11 + const mnemonic = 'course abstract aerobic deer try switch turtle diet fence affair butter top' + + const txid = await uut.completeTx(hex, hdIndex, mnemonic) + assert.equal(txid, 'complete tx id result') + }) + }) + + describe('#reClaimTokens', () => { + it('should re-claim tokens', async () => { + const orderData = { + hdIndex: 11, + tokenId: 'tokenId', + numTokens: 1 + } + const res = await uut.reclaimTokens(orderData) + assert.equal(res, 'fakeTxid') + }) + it('should handle error if order hd index is not provided', async () => { + try { + const orderData = {} + await uut.reclaimTokens(orderData) + assert.fail('Unexpected code path') + } catch (error) { + assert.equal(error.message, 'orderData.hdIndex is required!') + } + }) }) }) diff --git a/test/unit/adapters/webhook.adapter.unit.js b/test/unit/adapters/webhook.adapter.unit.js index a5c2a9f..c6a7321 100644 --- a/test/unit/adapters/webhook.adapter.unit.js +++ b/test/unit/adapters/webhook.adapter.unit.js @@ -65,4 +65,51 @@ describe('#Webhook-Adapter', () => { assert.isString(result.id) }) }) + describe('#deleteWebhook', () => { + it('should throw error if input is not provided', async () => { + try { + await uut.deleteWebhook() + assert.fail('unexpected code path') + } catch (err) { + assert.include(err.message, 'url must be a string') + } + }) + it('should delete webhook', async () => { + sandbox.stub(uut.axios, 'delete').resolves({ data: { success: true } }) + const url = 'https://test.com/my-webhook' + const result = await uut.deleteWebhook(url) + assert.isObject(result) + assert.property(result, 'success') + }) + }) + describe('#waitUntilSuccess', () => { + it('should handle error', async () => { + try { + sandbox.stub(uut, 'sleep').throws(new Error('test error')) + sandbox.stub(uut, 'deleteWebhook') + .onCall(0).throws(new Error('test error')) + + sandbox.stub(uut, 'createWebhook') + .onCall(0).throws(new Error('test error')) + .onCall(1).resolves({ data: { success: true } }) + await uut.waitUntilSuccess() + assert.fail('unexpected code path') + } catch (err) { + assert.include(err.message, 'test error') + } + }) + it('should wait until success', async () => { + uut.sleepTime = 100 + + sandbox.stub(uut, 'deleteWebhook') + .onCall(0).throws(new Error('test error')) + + sandbox.stub(uut, 'createWebhook') + .onCall(0).throws(new Error('test error')) + .onCall(1).resolves({ data: { success: true } }) + + const result = await uut.waitUntilSuccess() + assert.isTrue(result) + }) + }) }) diff --git a/test/unit/mocks/adapters/wallet.js b/test/unit/mocks/adapters/wallet.js index 70af917..22a60de 100644 --- a/test/unit/mocks/adapters/wallet.js +++ b/test/unit/mocks/adapters/wallet.js @@ -12,8 +12,18 @@ const mockWallet = { nextAddress: 1 }; -class MockBchWallet { + +class AdapterRoute { constructor() { + this.sendTx = async () => { + return 'fakeTxid'; + } + } +} + +class MockBchWallet { + constructor(mnemonic, advancedOptions) { + this.advancedOptions = advancedOptions this.walletInfoPromise = true; this.walletInfo = mockWallet; this.initialize = async () => {} @@ -24,6 +34,9 @@ class MockBchWallet { this.sendTokens = async () => { return 'fakeTxid'; }; + this.send = async () => { + return 'fakeTxid'; + }; this.utxoIsValid =async ()=>{} this.getUtxos = async () => { }; this.getBalance = async () => { }; @@ -33,7 +46,15 @@ class MockBchWallet { tokenTicker: 'TROUT' }]; }; + this.getKeyPair = async () => { + return { + cashAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00', + wif: 'L5D2UAam8tvo3uii5kpgaGyjvVMimdrXu8nWGQSQjuuAix6ji1YQ', + hdIndex: 11 + } + }; this.optimize = async () => { }; + this.ar = new AdapterRoute() // Environment variable is used by wallet-balance.unit.js to force an error. if (process.env.NO_UTXO) { this.utxos = {}; diff --git a/test/unit/mocks/bch-token-sweep-mock.js b/test/unit/mocks/bch-token-sweep-mock.js new file mode 100644 index 0000000..171079c --- /dev/null +++ b/test/unit/mocks/bch-token-sweep-mock.js @@ -0,0 +1,27 @@ +class BchTokenSweepMock { + constructor(wif, privateKey, bchWallet, fee, cashAddress) { + this.wif = wif + this.privateKey = privateKey + this.bchWallet = bchWallet + this.fee = fee + this.cashAddress = cashAddress + } + populateObjectFromNetwork() { + return { + success: true, + txid: 'txid' + } + } + sweepTo() { + return { + success: true, + txid: 'txid' + } + } +} + +export { BchTokenSweepMock } + +export default { + BchTokenSweepMock +} \ No newline at end of file diff --git a/test/unit/mocks/use-cases/offer-mock-data.js b/test/unit/mocks/use-cases/offer-mock-data.js index 4cd3395..c2cc82f 100644 --- a/test/unit/mocks/use-cases/offer-mock-data.js +++ b/test/unit/mocks/use-cases/offer-mock-data.js @@ -115,6 +115,7 @@ const fungibleTokenData01 = { "documentUri": "troutsblog.com", "documentHash": "", "decimals": 2, + "tokenDecimals": 2, "mintBatonIsActive": true, "tokensInCirculationBN": "100097954686", "tokensInCirculationStr": "100097954686",