diff --git a/packages/bitcore-wallet-client/src/lib/api.ts b/packages/bitcore-wallet-client/src/lib/api.ts index 11bc9b30a3d..10d81c181c3 100644 --- a/packages/bitcore-wallet-client/src/lib/api.ts +++ b/packages/bitcore-wallet-client/src/lib/api.ts @@ -1403,6 +1403,7 @@ export class API extends EventEmitter { API._encryptMessage(opts.message, this.credentials.sharedEncryptingKey) || null; args.payProUrl = opts.payProUrl || null; + args.isTokenSwap = opts.isTokenSwap || null; _.each(args.outputs, o => { o.message = API._encryptMessage(o.message, this.credentials.sharedEncryptingKey) || @@ -1434,6 +1435,7 @@ export class API extends EventEmitter { // * @param {number} opts.fee - Optional. Use an fixed fee for this TX (only when opts.inputs is specified) // * @param {Boolean} opts.noShuffleOutputs - Optional. If set, TX outputs won't be shuffled. Defaults to false // * @param {String} opts.signingMethod - Optional. If set, force signing method (ecdsa or schnorr) otherwise use default for coin + // * @param {Boolean} opts.isTokenSwap - Optional. To specify if we are trying to make a token swap // * @returns {Callback} cb - Return error or the transaction proposal // * @param {String} baseUrl - Optional. ONLY FOR TESTING // */ @@ -3184,4 +3186,17 @@ export class API extends EventEmitter { ); }); } + + oneInchGetSwap(data): Promise { + return new Promise((resolve, reject) => { + this.request.post( + '/v1/service/oneInch/getSwap', + data, + (err, data) => { + if (err) return reject(err); + return resolve(data); + } + ); + }); + } } diff --git a/packages/bitcore-wallet-client/src/lib/common/utils.ts b/packages/bitcore-wallet-client/src/lib/common/utils.ts index 924b2a3fdeb..a6307d7c94e 100644 --- a/packages/bitcore-wallet-client/src/lib/common/utils.ts +++ b/packages/bitcore-wallet-client/src/lib/common/utils.ts @@ -413,7 +413,8 @@ export class Utils { outputs, payProUrl, tokenAddress, - multisigContractAddress + multisigContractAddress, + isTokenSwap } = txp; const recipients = outputs.map(output => { return { @@ -428,7 +429,7 @@ export class Utils { recipients[0].data = data; } const unsignedTxs = []; - const isERC20 = tokenAddress && !payProUrl; + const isERC20 = tokenAddress && !payProUrl && !isTokenSwap; const isETHMULTISIG = multisigContractAddress; const chain = isETHMULTISIG ? 'ETHMULTISIG' diff --git a/packages/bitcore-wallet-service/src/config.ts b/packages/bitcore-wallet-service/src/config.ts index d3a6448212a..81fdcf7c9f1 100644 --- a/packages/bitcore-wallet-service/src/config.ts +++ b/packages/bitcore-wallet-service/src/config.ts @@ -128,6 +128,11 @@ module.exports = { // secret: 'changelly_secret', // api: 'https://api.changelly.com' // }, + // oneInch: { + // api: 'https://api.1inch.exchange', + // referrerAddress: 'one_inch_referrer_address', // ETH + // referrerFee: 'one_inch_referrer_fee', // min: 0; max: 3; (represents percentage) + // }, // To use email notifications uncomment this: // emailOpts: { // host: 'localhost', diff --git a/packages/bitcore-wallet-service/src/lib/chain/eth/index.ts b/packages/bitcore-wallet-service/src/lib/chain/eth/index.ts index c23034d12bc..ec45c82dfc2 100644 --- a/packages/bitcore-wallet-service/src/lib/chain/eth/index.ts +++ b/packages/bitcore-wallet-service/src/lib/chain/eth/index.ts @@ -191,8 +191,8 @@ export class EthChain implements IChain { } getBitcoreTx(txp, opts = { signed: true }) { - const { data, outputs, payProUrl, tokenAddress, multisigContractAddress } = txp; - const isERC20 = tokenAddress && !payProUrl; + const { data, outputs, payProUrl, tokenAddress, multisigContractAddress, isTokenSwap } = txp; + const isERC20 = tokenAddress && !payProUrl && !isTokenSwap; const isETHMULTISIG = multisigContractAddress; const chain = isETHMULTISIG ? 'ETHMULTISIG' : isERC20 ? 'ERC20' : 'ETH'; const recipients = outputs.map(output => { diff --git a/packages/bitcore-wallet-service/src/lib/expressapp.ts b/packages/bitcore-wallet-service/src/lib/expressapp.ts index 69e2b87587c..5093efb6304 100644 --- a/packages/bitcore-wallet-service/src/lib/expressapp.ts +++ b/packages/bitcore-wallet-service/src/lib/expressapp.ts @@ -1454,6 +1454,36 @@ export class ExpressApp { }); }); + router.get('/v1/service/oneInch/getReferrerFee', (req, res) => { + let server; + try { + server = getServer(req, res); + } catch (ex) { + return returnError(ex, res, req); + } + server + .oneInchGetReferrerFee(req) + .then(response => { + res.json(response); + }) + .catch(err => { + if (err) return returnError(err, res, req); + }); + }); + + router.post('/v1/service/oneInch/getSwap', (req, res) => { + getServerWithAuth(req, res, server => { + server + .oneInchGetSwap(req) + .then(response => { + res.json(response); + }) + .catch(err => { + if (err) return returnError(err, res, req); + }); + }); + }); + router.get('/v1/service/payId/:payId', (req, res) => { let server; const payId = req.params['payId']; diff --git a/packages/bitcore-wallet-service/src/lib/model/txproposal.ts b/packages/bitcore-wallet-service/src/lib/model/txproposal.ts index 5edb382572e..cc9f2e6d0a7 100644 --- a/packages/bitcore-wallet-service/src/lib/model/txproposal.ts +++ b/packages/bitcore-wallet-service/src/lib/model/txproposal.ts @@ -69,6 +69,7 @@ export interface ITxProposal { destinationTag?: string; invoiceID?: string; lockUntilBlockHeight?: number; + isTokenSwap?: boolean; } export class TxProposal { @@ -129,6 +130,7 @@ export class TxProposal { destinationTag?: string; invoiceID?: string; lockUntilBlockHeight?: number; + isTokenSwap?: boolean; static create(opts) { opts = opts || {}; @@ -200,6 +202,7 @@ export class TxProposal { x.gasLimit = opts.gasLimit; // Backward compatibility for BWC <= 8.9.0 x.data = opts.data; // Backward compatibility for BWC <= 8.9.0 x.tokenAddress = opts.tokenAddress; + x.isTokenSwap = opts.isTokenSwap; x.multisigContractAddress = opts.multisigContractAddress; // XRP @@ -263,6 +266,7 @@ export class TxProposal { x.gasLimit = obj.gasLimit; // Backward compatibility for BWC <= 8.9.0 x.data = obj.data; // Backward compatibility for BWC <= 8.9.0 x.tokenAddress = obj.tokenAddress; + x.isTokenSwap = obj.isTokenSwap; x.multisigContractAddress = obj.multisigContractAddress; x.multisigTxId = obj.multisigTxId; diff --git a/packages/bitcore-wallet-service/src/lib/server.ts b/packages/bitcore-wallet-service/src/lib/server.ts index fbe37d42f50..15390b0790d 100644 --- a/packages/bitcore-wallet-service/src/lib/server.ts +++ b/packages/bitcore-wallet-service/src/lib/server.ts @@ -2215,6 +2215,7 @@ export class WalletService { * @param {Boolean} opts.signingMethod[=ecdsa] - do not use cashaddress for bch * @param {string} opts.tokenAddress - optional. ERC20 Token Contract Address * @param {string} opts.multisigContractAddress - optional. MULTISIG ETH Contract Address + * @param {Boolean} opts.isTokenSwap - Optional. To specify if we are trying to make a token swap * @returns {TxProposal} Transaction proposal. outputs address format will use the same format as inpunt. */ createTx(opts, cb) { @@ -2352,7 +2353,8 @@ export class WalletService { multisigContractAddress: opts.multisigContractAddress, destinationTag: opts.destinationTag, invoiceID: opts.invoiceID, - signingMethod: opts.signingMethod + signingMethod: opts.signingMethod, + isTokenSwap: opts.isTokenSwap }; txp = TxProposal.create(txOpts); next(); @@ -4862,6 +4864,70 @@ export class WalletService { }); } + oneInchGetCredentials() { + if (!config.oneInch) throw new Error('1Inch missing credentials'); + + const credentials = { + API: config.oneInch.api, + referrerAddress: config.oneInch.referrerAddress, + referrerFee: config.oneInch.referrerFee + }; + + return credentials; + } + + oneInchGetReferrerFee(req): Promise { + return new Promise((resolve, reject) => { + const credentials = this.oneInchGetCredentials(); + + const referrerFee: number = credentials.referrerFee; + + resolve({ referrerFee }); + }); + } + + oneInchGetSwap(req): Promise { + return new Promise((resolve, reject) => { + const credentials = this.oneInchGetCredentials(); + + if (!checkRequired(req.body, ['fromTokenAddress', 'toTokenAddress', 'amount', 'fromAddress', 'slippage', 'destReceiver'])) { + return reject(new ClientError('oneInchGetSwap request missing arguments')); + } + + const headers = { + 'Content-Type': 'application/json' + }; + + let qs = []; + qs.push('fromTokenAddress=' + req.body.fromTokenAddress); + qs.push('toTokenAddress=' + req.body.toTokenAddress); + qs.push('amount=' + req.body.amount); + qs.push('fromAddress=' + req.body.fromAddress); + qs.push('slippage=' + req.body.slippage); + qs.push('destReceiver=' + req.body.destReceiver); + + if(credentials.referrerFee) qs.push('fee=' + credentials.referrerFee); + if(credentials.referrerAddress) qs.push('referrerAddress=' + credentials.referrerAddress); + + const URL: string = credentials.API + '/v3.0/1/swap/?' + qs.join('&'); + + this.request.get( + URL, + { + headers, + json: true + }, + (err, data) => { + if (err) { + return reject(err.body ?? err); + } else { + return resolve(data.body); + } + } + ); + }); + } + getPayId(url: string): Promise { return new Promise((resolve, reject) => { const headers = { diff --git a/packages/bitcore-wallet-service/test/integration/oneInch.js b/packages/bitcore-wallet-service/test/integration/oneInch.js new file mode 100644 index 00000000000..56102f83a3c --- /dev/null +++ b/packages/bitcore-wallet-service/test/integration/oneInch.js @@ -0,0 +1,148 @@ +'use strict'; + +const chai = require('chai'); +const should = chai.should(); +const { WalletService } = require('../../ts_build/lib/server'); +const TestData = require('../testdata'); +const helpers = require('./helpers'); + +let config = require('../../ts_build/config.js'); +let server, wallet, fakeRequest, req; + +describe('OneInch integration', () => { + before((done) => { + helpers.before((res) => { + done(); + }); + }); + beforeEach((done) => { + config.suspendedChains = []; + config.oneInch = { + api: 'xxxx', + referrerAddress: 'referrerAddress', + referrerFee: 'referrerFee1' + } + + fakeRequest = { + post: (_url, _opts, _cb) => { return _cb(null, { body: 'data'}) }, + get: (_url, _opts, _cb) => { return _cb(null, { body: 'data'}) }, + }; + + helpers.beforeEach((res) => { + helpers.createAndJoinWallet(1, 1, (s, w) => { + wallet = w; + const priv = TestData.copayers[0].privKey_1H_0; + const sig = helpers.signMessage('hello world', priv); + + WalletService.getInstanceWithAuth({ + // test assumes wallet's copayer[0] is TestData's copayer[0] + copayerId: wallet.copayers[0].id, + message: 'hello world', + signature: sig, + clientVersion: 'bwc-2.0.0', + walletId: '123', + }, (err, s) => { + should.not.exist(err); + server = s; + done(); + }); + }); + }); + }); + after((done) => { + helpers.after(done); + }); + + describe('#oneInchGetReferrerFee', () => { + beforeEach(() => { + req = {} + }); + + it('should get referrel fee if it is defined in config', () => { + server.request = fakeRequest; + server.oneInchGetReferrerFee(req).then(data => { + should.exist(data); + data.referrerFee.should.equal('referrerFee1'); + }).catch(err => { + should.not.exist(err); + }); + }); + + it('should return error if oneInch is commented in config', () => { + config.oneInch = undefined; + + server.request = fakeRequest; + server.oneInchGetReferrerFee(req).then(data => { + should.not.exist(data); + }).catch(err => { + should.exist(err); + err.message.should.equal('1Inch missing credentials'); + }); + }); + }); + + describe('#oneInchGetSwap', () => { + beforeEach(() => { + req = { + headers: {}, + body: { + fromTokenAddress: 'fromTokenAddress1', + toTokenAddress: 'toTokenAddress1', + amount: 100, + fromAddress: 'fromAddress1', + slippage: 0.5, + destReceiver: 'destReceiver1' + } + } + }); + + it('should work properly if req is OK', () => { + server.request = fakeRequest; + server.oneInchGetSwap(req).then(data => { + should.exist(data); + }).catch(err => { + should.not.exist(err); + }); + }); + + it('should return error if there is some missing arguments', () => { + delete req.body.fromTokenAddress; + + server.request = fakeRequest; + server.oneInchGetSwap(req).then(data => { + should.not.exist(data); + }).catch(err => { + should.exist(err); + err.message.should.equal('oneInchGetSwap request missing arguments'); + }); + }); + + it('should return error if request returns error', () => { + req.body.fromTokenAddress = 'fromTokenAddress1'; + const fakeRequest2 = { + post: (_url, _opts, _cb) => { return _cb(new Error('Error')) }, + get: (_url, _opts, _cb) => { return _cb(new Error('Error')) } + }; + + server.request = fakeRequest2; + server.oneInchGetSwap(req).then(data => { + should.not.exist(data); + }).catch(err => { + should.exist(err); + err.message.should.equal('Error'); + }); + }); + + it('should return error if oneInch is commented in config', () => { + config.oneInch = undefined; + + server.request = fakeRequest; + server.oneInchGetSwap(req).then(data => { + should.not.exist(data); + }).catch(err => { + should.exist(err); + err.message.should.equal('1Inch missing credentials'); + }); + }); + }); +}); \ No newline at end of file