joyso
Advanced tools
Comparing version 0.2.1 to 0.3.0
# Change Log | ||
## v0.3.0 / 2018-10-16 | ||
### Added | ||
- support for non-ETH quote pairs. | ||
- new option value `token` to `fee` of `withdraw` method. | ||
- get my trades api. | ||
### Change | ||
- pair format from ETH_XXX to XXX_ETH. | ||
- option `fee` to `feeByJoy` of `buy`, `sell` and `trade` methods. | ||
## v0.2.1 / 2018-10-08 | ||
@@ -4,0 +14,0 @@ ### Fixed |
{ | ||
"name": "joyso", | ||
"version": "0.2.1", | ||
"version": "0.3.0", | ||
"description": "JOYSO API client library for trading.", | ||
@@ -5,0 +5,0 @@ "main": "src/joyso.js", |
# JOYSO | ||
JOYSO API client library for trading. | ||
## Notice | ||
v0.3.0 has breaking changes. Please see [CHANGELOG v0.3.0](https://github.com/Joyso-io/joyso-api/blob/master/CHANGELOG.md#v030--2018-10-16) | ||
## Installation | ||
@@ -28,3 +31,3 @@ You can use this command to install: | ||
```JavaScript | ||
const subscription = joyso.subscribeOrderBook('ETH_JOY', orderBook => { | ||
const subscription = joyso.subscribeOrderBook('JOY_ETH', orderBook => { | ||
console.log(JSON.stringify(orderBook)); | ||
@@ -63,3 +66,3 @@ }); | ||
```JavaScript | ||
const subscription = joyso.subscribeTrades('ETH_JOY', trades => { | ||
const subscription = joyso.subscribeTrades('JOY_ETH', trades => { | ||
console.log(JSON.stringify(trades.slice(0, 2))); | ||
@@ -76,3 +79,3 @@ }); | ||
"amount":"2", | ||
"pair":"ETH_JOY" | ||
"pair":"JOY_ETH" | ||
}, | ||
@@ -84,3 +87,3 @@ { | ||
"amount":"1", | ||
"pair":"ETH_JOY" | ||
"pair":"JOY_ETH" | ||
} | ||
@@ -130,3 +133,3 @@ ] | ||
"fill":"0", | ||
"pair":"ETH_T00" | ||
"pair":"T00_ETH" | ||
}, | ||
@@ -140,3 +143,3 @@ { | ||
"fill":"3.5", | ||
"pair":"ETH_JOY" | ||
"pair":"JOY_ETH" | ||
} | ||
@@ -165,3 +168,3 @@ ] | ||
"amount":"2", | ||
"pair":"ETH_JOY", | ||
"pair":"JOY_ETH", | ||
"fee":"ETH", | ||
@@ -178,3 +181,3 @@ "gasFee":"0", | ||
"amount":"1", | ||
"pair":"ETH_JOY", | ||
"pair":"JOY_ETH", | ||
"fee":"ETH", | ||
@@ -228,3 +231,3 @@ "gasFee":"0.000105", | ||
### buy({ pair, price, amount, fee }) | ||
### buy({ pair, price, amount, feeByJoy }) | ||
Place buying order | ||
@@ -234,6 +237,6 @@ ```JavaScript | ||
let order = await joyso.buy({ | ||
pair: 'ETH_JOY', | ||
pair: 'JOY_ETH', | ||
price: '0.000123481', | ||
amount: 1, | ||
fee: 'base' | ||
feeByJoy: true | ||
}); | ||
@@ -253,6 +256,6 @@ console.log(JSON.stringify(order)); | ||
|---|---|---| | ||
|pair|O|Pair to trade, format is `${base}_${quote}`, eg: ETH_JOY| | ||
|pair|O|Pair to trade, format is `${base}_${quote}`, eg: JOY_ETH| | ||
|price|O|Order price, minimum is 0.000000001| | ||
|amount|O|Quote amount| | ||
|fee|O|Specify how to pay fee. `base` or `joy`.| | ||
|feeByJoy||Specify how to pay fee. `true` will pay by JOY. `false` will pay by quote token(ETH if pair XXX_ETH). Default is `false`| | ||
@@ -268,3 +271,3 @@ Result | ||
"fill":"1", | ||
"pair":"ETH_JOY" | ||
"pair":"JOY_ETH" | ||
} | ||
@@ -275,10 +278,9 @@ ``` | ||
### sell({ pair, price, amount, fee }) | ||
### sell({ pair, price, amount, feeByJoy }) | ||
Place selling order | ||
```JavaScript | ||
let order = await joyso.sell({ | ||
pair: 'ETH_JOY', | ||
pair: 'JOY_ETH', | ||
price: '0.000123481', | ||
amount: 100, | ||
fee: 'base' | ||
amount: 100 | ||
}); | ||
@@ -288,3 +290,3 @@ ``` | ||
### trade({ pair, price, amount, fee, side }) | ||
### trade({ pair, price, amount, feeByJoy, side }) | ||
Place order | ||
@@ -294,6 +296,5 @@ ```JavaScript | ||
side: 'buy', | ||
pair: 'ETH_JOY', | ||
pair: 'JOY_ETH', | ||
price: '0.000123481', | ||
amount: 100, | ||
fee: 'joy' | ||
amount: 100 | ||
}); | ||
@@ -322,4 +323,31 @@ ``` | ||
|amount|O|Amount to withdraw| | ||
|fee|O|Specify how to pay fee. `eth` or `joy`.| | ||
|fee|O|Specify how to pay fee. `eth`, `joy` or `token`. `token` can only be used when token is quote token.| | ||
### getMyTrades({ from, to, quote, base, side, before, limit }) | ||
Get my trades | ||
```JavaScript | ||
await joyso.getMyTrades({ | ||
quote: 'ETH', | ||
base: 'JOY', | ||
side: 'sell', | ||
from: 1539129600, | ||
to: 1539216000, | ||
before: 123, | ||
limit: 10 | ||
}); | ||
``` | ||
Options | ||
|Name|Required|Description| | ||
|---|---|---| | ||
|quote||Quote token| | ||
|base||Base token| | ||
|side||Specify side. `buy`, `sell` or blank. Blank means both.| | ||
|from||From time (included). Unix timestamp| | ||
|to||To time (excluded). Unix timestamp| | ||
|before||Only return Trade ID before this. (excluded)| | ||
|limit||Specify size of records to return| | ||
Results are same with subscribeMyTrades. | ||
### disconnect() | ||
@@ -326,0 +354,0 @@ Disconnect from JOYSO. |
@@ -12,3 +12,3 @@ const Joyso = require('../src/joyso'); | ||
// subscribe order book, notify if change | ||
joyso.subscribeOrderBook('ETH_JOY', orderBook => { | ||
joyso.subscribeOrderBook('JOY_ETH', orderBook => { | ||
console.log(JSON.stringify(orderBook)); | ||
@@ -18,3 +18,3 @@ }); | ||
// subscribe market trades, notify if change | ||
joyso.subscribeTrades('ETH_JOY', trades => { | ||
joyso.subscribeTrades('JOY_ETH', trades => { | ||
console.log(JSON.stringify(trades.slice(0, 5))); | ||
@@ -48,6 +48,5 @@ }); | ||
order = await joyso.buy({ | ||
pair: 'ETH_JOY', | ||
pair: 'JOY_ETH', | ||
price: '0.000123481', | ||
amount: 1, | ||
fee: 'base' | ||
amount: 1 | ||
}); | ||
@@ -57,6 +56,5 @@ | ||
order = await joyso.sell({ | ||
pair: 'ETH_JOY', | ||
pair: 'JOY_ETH', | ||
price: '0.000123481', | ||
amount: 100, | ||
fee: 'base' | ||
amount: 100 | ||
}); | ||
@@ -67,6 +65,6 @@ | ||
side: 'buy', | ||
pair: 'ETH_JOY', | ||
pair: 'JOY_ETH', | ||
price: '0.000123481', | ||
amount: 100, | ||
fee: 'joy' | ||
feeByJoy: true | ||
}); | ||
@@ -83,2 +81,13 @@ | ||
}); | ||
// get my trades | ||
const trades = await joyso.getMyTrades({ | ||
quote: 'ETH', | ||
base: 'JOY', | ||
side: 'sell', | ||
from: 1539680000, | ||
to: 1539679000, | ||
before: 123, | ||
limit: 10 | ||
}); | ||
} catch (e) { | ||
@@ -85,0 +94,0 @@ if (e.statusCode === 400) { |
@@ -0,54 +1,67 @@ | ||
const EventEmitter = require('events'); | ||
const rp = require('request-promise'); | ||
class Account { | ||
class Account extends EventEmitter { | ||
constructor(client, address) { | ||
super(); | ||
this.client = client; | ||
this.address = address; | ||
this.advanceReal; | ||
this.advanceInOrder; | ||
this.init = true; | ||
} | ||
subscribe() { | ||
return new Promise(resolve => { | ||
this.cable = this.client.cable.subscriptions.create({ | ||
channel: 'AccountAdvanceChannel', | ||
contract: this.client.system.contract.substr(2), | ||
address: this.address.substr(2) | ||
}, { | ||
connected: async () => { | ||
await this.update(); | ||
if (this.init) { | ||
this.init = false; | ||
resolve(); | ||
} | ||
}, | ||
received: data => { | ||
switch (data.e) { | ||
case 'update': | ||
this.updateConfig(data.data); | ||
break; | ||
} | ||
} | ||
connect() { | ||
if (this.cable) { | ||
return Promise.reject(); | ||
} | ||
const promise = new Promise((resolve, reject) => { | ||
const timer = setTimeout(() => reject(new Error('timeout')), 15000); | ||
this.once('update', () => { | ||
clearTimeout(timer); | ||
resolve(); | ||
}); | ||
this.once('error', e => { | ||
clearTimeout(timer); | ||
reject(e); | ||
}); | ||
}); | ||
this.cable = this.client.cable.subscriptions.create({ | ||
channel: 'AccountAdvanceChannel', | ||
contract: this.client.system.contract.substr(2), | ||
address: this.address.substr(2) | ||
}, { | ||
connected: () => this.update(), | ||
received: data => { | ||
switch (data.e) { | ||
case 'update': | ||
this.updateConfig(data.data); | ||
break; | ||
} | ||
} | ||
}); | ||
return promise; | ||
} | ||
unsubscribe() { | ||
destroy() { | ||
clearTimeout(this.retryTimer); | ||
this.cable.unsubscribe(); | ||
delete this.cable; | ||
this.init = false; | ||
} | ||
async update() { | ||
try { | ||
clearTimeout(this.retryTimer); | ||
const json = await this.client.request('accounts/advance', { data: { user: this.address.substr(2) } }); | ||
this.updateConfig(json); | ||
} catch (e) { | ||
this.emit('error', e); | ||
this.retryTimer = setTimeout(() => this.update(), 5000); | ||
} | ||
} | ||
updateConfig(json) { | ||
this.advanceReal = json.advance_real; | ||
this.advanceInOrder = json.advance_in_order; | ||
this.emit('update', json); | ||
} | ||
async update() { | ||
const json = await rp(this.client.createRequest(`accounts/advance?user=${this.address.substr(2)}`)); | ||
this.updateConfig(json); | ||
} | ||
} | ||
module.exports = Account; |
@@ -51,8 +51,8 @@ const rp = require('request-promise'); | ||
async get() { | ||
const json = await rp(this.client.createRequest('balances', { | ||
qs: { | ||
const json = await this.client.request('balances', { | ||
data: { | ||
contract: this.client.system.contract.substr(2), | ||
user: this.address.substr(2) | ||
} | ||
})); | ||
}); | ||
json.balances.forEach(balance => this.updateBalance(balance)); | ||
@@ -59,0 +59,0 @@ } |
@@ -57,2 +57,3 @@ const rp = require('request-promise'); | ||
unsubscribe() { | ||
clearTimeout(this.retryTimer); | ||
this.cable.unsubscribe(); | ||
@@ -68,16 +69,23 @@ delete this.cable; | ||
this.requesting = this.requestId; | ||
clearTimeout(this.retryTimer); | ||
const after = this.funds.length ? this.funds[0].id : null; | ||
const json = await this.get(after); | ||
const funds = this.convert(json.funds, true); | ||
if (after) { | ||
this.funds.unshift(...funds); | ||
} else { | ||
this.funds = funds; | ||
} | ||
this.onReceived(this.funds); | ||
if (this.requesting !== this.requestId) { | ||
try { | ||
const json = await this.get(after); | ||
const funds = this.convert(json.funds, true); | ||
if (after) { | ||
this.funds.unshift(...funds); | ||
} else { | ||
this.funds = funds; | ||
} | ||
this.onReceived(this.funds); | ||
if (this.requesting !== this.requestId) { | ||
this.requesting = 0; | ||
this.update(); | ||
} else { | ||
this.requesting = 0; | ||
} | ||
} catch (e) { | ||
console.log(e); | ||
this.requesting = 0; | ||
this.update(); | ||
} else { | ||
this.requesting = 0; | ||
this.retryTimer = setTimeout(() => this.update(), 5000); | ||
} | ||
@@ -118,4 +126,4 @@ } | ||
get(after = null) { | ||
return rp(this.client.createRequest('funds', { | ||
qs: { | ||
return this.client.request('funds', { | ||
data: { | ||
contract: this.client.system.contract.substr(2), | ||
@@ -125,3 +133,3 @@ user: this.address.substr(2), | ||
} | ||
})); | ||
}); | ||
} | ||
@@ -128,0 +136,0 @@ } |
253
src/joyso.js
@@ -16,4 +16,6 @@ const rp = require('request-promise'); | ||
BigNumber.config({ DECIMAL_PLACES: 36, ROUNDING_MODE: 1 }); | ||
BigNumber.config({ DECIMAL_PLACES: 36 }); | ||
const ETH_MAX_FEE_PRICE = new BigNumber('100000000'); | ||
const NON_ETH_MAX_FEE_PRICE = new BigNumber('1000000000000000000000000000'); | ||
const keys = {}; | ||
@@ -43,8 +45,9 @@ | ||
this.system = new System(this); | ||
const json = await this.system.update(); | ||
this.system.subscribe(); | ||
this.tokenManager = new TokenManager(this, json.tokens); | ||
this.tokenManager.subscribe(); | ||
this.system.once('update', json => { | ||
this.tokenManager = new TokenManager(this, json); | ||
this.tokenManager.subscribe(); | ||
}); | ||
await this.system.connect(); | ||
this.account = new Account(this, this.address); | ||
await this.account.subscribe(); | ||
await this.account.connect(); | ||
this.balances = new Balances({ client: this, address: this.address }); | ||
@@ -81,9 +84,9 @@ await this.balances.subscribe(); | ||
try { | ||
const r = await rp(this.createRequest('accounts', { | ||
const r = await this.request('accounts', { | ||
method: 'POST', | ||
body: Object.assign({ | ||
data: Object.assign({ | ||
user: this.address.substr(2), | ||
nonce: nonce | ||
}, vrs) | ||
})); | ||
}); | ||
this.accessToken = r.access_token; | ||
@@ -98,6 +101,6 @@ } finally { | ||
try { | ||
const r = await rp(this.createRequest('withdraw_queues', { | ||
const r = await this.request('withdraw_queues', { | ||
method: 'POST', | ||
body: withdraw | ||
})); | ||
data: withdraw | ||
}); | ||
} catch (e) { | ||
@@ -184,8 +187,7 @@ if (!options.retry && e.statusCode === 400 && e.error.fee_changed) { | ||
delete order.hash; | ||
const r = await rp(this.createRequest('orders', { | ||
const r = await this.request('orders', { | ||
method: 'POST', | ||
body: order | ||
})); | ||
data: order | ||
}); | ||
this.updateHashTable(order.nonce, hash); | ||
order.is_buy = options.side === 'buy'; | ||
order.id = r.order.id; | ||
@@ -197,5 +199,7 @@ order.status = r.order.status; | ||
if (!options.retry && e.statusCode === 400 && e.error.fee_changed) { | ||
options.retry = true; | ||
await this.tokenManager.refresh(); | ||
return this.trade(options); | ||
if (e.error.length === 1) { | ||
options.retry = true; | ||
return this.trade(options); | ||
} | ||
} | ||
@@ -206,4 +210,40 @@ throw e; | ||
toAmountByPrice(token, quoteAmount, price, method) { | ||
const amount = this.tokenManager.toRawAmount(token, quoteAmount.mul(price), method); | ||
async getMyTrades({ from, to, quote, base, side, before, limit } = {}) { | ||
const options = {}; | ||
quote = this.tokenManager.symbolMap[quote]; | ||
if (quote) { | ||
if (this.tokenManager.quotes.find(t => t === quote)) { | ||
options.token_base = quote.address.substr(2); | ||
} else { | ||
throw new Error('invalid quote'); | ||
} | ||
} | ||
base = this.tokenManager.symbolMap[base]; | ||
if (base) { | ||
options.token_target = base.address.substr(2); | ||
} | ||
if (side === 'buy') { | ||
options.is_buy = true; | ||
} else if (side === 'sell') { | ||
options.is_buy = false | ||
} | ||
if (from) { | ||
options.from = from; | ||
} | ||
if (to) { | ||
options.to = to; | ||
} | ||
if (before) { | ||
options.before = before; | ||
} | ||
if (limit) { | ||
options.limit = limit; | ||
} | ||
options.user = this.address.substr(2); | ||
const json = await this.request('trades/history', { data: options }); | ||
return this.myTrades.convert(json.trades); | ||
} | ||
toAmountByPrice(token, baseAmount, price, method) { | ||
const amount = this.tokenManager.toRawAmount(token, baseAmount.mul(price), method); | ||
return this.tokenManager.toAmount(token, amount); | ||
@@ -213,3 +253,3 @@ } | ||
toPrice(baseAmount, quoteAmount) { | ||
return baseAmount.div(quoteAmount).round(9); | ||
return quoteAmount.div(baseAmount).round(9); | ||
} | ||
@@ -231,11 +271,9 @@ | ||
validateOrder(price, amount, fee, side) { | ||
validateOrder(price, amount, side, quote) { | ||
this.validateAmount(amount); | ||
const v = new BigNumber(price).mul(1000000000); | ||
const precision = new BigNumber(10).pow(quote.precision); | ||
const v = new BigNumber(price).mul(precision); | ||
if (!v.truncated().equals(v)) { | ||
throw new Error('invalid price'); | ||
} | ||
if (fee !== 'base' && fee !== 'joy' && fee !== 'eth') { | ||
throw new Error('invalid fee'); | ||
} | ||
if (side !== 'buy' && side !== 'sell') { | ||
@@ -246,2 +284,8 @@ throw new Error('invalid side'); | ||
validatePair(base, quote) { | ||
if (!base || !quote || !this.tokenManager.quotes.find(t => t === quote)) { | ||
throw new Error('invalid pair'); | ||
} | ||
} | ||
repayGasFee(token) { | ||
@@ -252,9 +296,8 @@ const ratio = new BigNumber(this.tokenManager.eth.gasFee).div(token.gasFee); | ||
receivableGasFee(side, paymentMethod, token) { | ||
let ethBalance, gasFee; | ||
ethBalance = this.tokenManager.toRawAmount(this.tokenManager.eth, this.balances.balances.ETH.available || 0); | ||
gasFee = this.tokenManager.eth.gasFee; | ||
receivableGasFee(side, feeByJoy, tokenFee, quote) { | ||
let gasFee = quote.gasFee; | ||
let baseBalance = this.balances.balances[quote.symbol]; | ||
baseBalance = this.tokenManager.toRawAmount(quote, baseBalance && baseBalance.available || 0); | ||
if ( | ||
side !== 'buy' && paymentMethod === 'base' && gasFee.gt(ethBalance) | ||
side !== 'buy' && !feeByJoy && gasFee.gt(baseBalance) | ||
&& this.account.advanceReal === 0 && this.account.advanceInOrder === 0 | ||
@@ -264,20 +307,18 @@ ) { | ||
} else if (this.account.advanceReal !== 0 && this.account.advanceInOrder === 0) { | ||
return this.repayGasFee(token); | ||
return this.repayGasFee(tokenFee); | ||
} else { | ||
return token.gasFee; | ||
return tokenFee.gasFee; | ||
} | ||
} | ||
createOrder({ pair, price, amount, fee, side }) { | ||
this.validateOrder(price, amount, fee, side); | ||
createOrder({ pair, price, amount, feeByJoy, side }) { | ||
const [base, quote]= this.tokenManager.getPair(pair); | ||
if (!base || !quote || base !== this.tokenManager.eth) { | ||
throw new Error('invalid pair'); | ||
} | ||
let quoteAmount = new BigNumber(amount); | ||
this.validatePair(base, quote); | ||
this.validateOrder(price, amount, side, quote); | ||
let baseAmount = new BigNumber(amount); | ||
let method = side === 'buy' ? 'ceil' : 'floor'; | ||
let baseAmount = this.toAmountByPrice(base, quoteAmount, price, method); | ||
let quoteAmount = this.toAmountByPrice(quote, baseAmount, price, method); | ||
if (!this.toPrice(baseAmount, quoteAmount).equals(price)) { | ||
method = method === 'floor' ? 'ceil' : 'floor'; | ||
baseAmount = this.toAmountByPrice(base, quoteAmount, price, method); | ||
quoteAmount = this.toAmountByPrice(quote, baseAmount, price, method); | ||
if (!this.toPrice(baseAmount, quoteAmount).equals(price)) { | ||
@@ -287,9 +328,9 @@ throw new Error('invalid amount, too small'); | ||
} | ||
baseAmount = this.tokenManager.toRawAmount(base, baseAmount); | ||
quoteAmount = this.tokenManager.toRawAmount(quote, quoteAmount); | ||
baseAmount = this.tokenManager.toRawAmount(base, baseAmount); | ||
let takerFee, makerFee, tokenFee, feePrice, custom = false; | ||
if (quote.taker_fee && quote.maker_fee) { | ||
takerFee = quote.taker_fee; | ||
makerFee = quote.maker_fee; | ||
if (base.taker_fee && base.maker_fee) { | ||
takerFee = base.taker_fee; | ||
makerFee = base.maker_fee; | ||
custom = true; | ||
@@ -301,3 +342,3 @@ } else { | ||
if (fee === 'joy') { | ||
if (feeByJoy) { | ||
tokenFee = this.tokenManager.joy; | ||
@@ -308,5 +349,21 @@ if (!custom) { | ||
} | ||
feePrice = tokenFee.price.mul(10000000).truncated(); | ||
if (feePrice.gt(100000000)) { | ||
feePrice = new BigNumber(100000000); | ||
feePrice = tokenFee.price; | ||
let maxFeePrice, offset; | ||
if (quote === this.tokenManager.eth) { | ||
maxFeePrice = ETH_MAX_FEE_PRICE; | ||
offset = new BigNumber('10000000'); | ||
} else { | ||
const priceToEth = quote.price; | ||
if (feePrice && priceToEth) { | ||
const decimalsOffset = new BigNumber(10).pow(tokenFee.decimals - quote.decimals); | ||
offset = new BigNumber('1000000000000').div(decimalsOffset); | ||
feePrice = new BigNumber(feePrice).div(priceToEth); | ||
} else { | ||
throw new Error('fee price invalid'); | ||
} | ||
maxFeePrice = NON_ETH_MAX_FEE_PRICE; | ||
} | ||
feePrice = feePrice.mul(offset).truncated(); | ||
if (feePrice.gt(maxFeePrice)) { | ||
feePrice = maxFeePrice; | ||
} else if (feePrice.lt(1)) { | ||
@@ -316,9 +373,14 @@ feePrice = new BigNumber(1); | ||
} else { | ||
tokenFee = this.tokenManager.eth; | ||
tokenFee = quote; | ||
feePrice = 0; | ||
} | ||
const gasFee = this.receivableGasFee(side, fee, tokenFee); | ||
const gasFee = this.receivableGasFee(side, feeByJoy, tokenFee, quote); | ||
let amountSell, amountBuy, tokenSell, tokenBuy; | ||
if (side === 'buy') { | ||
amountSell = quoteAmount; | ||
amountBuy = baseAmount; | ||
tokenSell = quote.address; | ||
tokenBuy = base.address; | ||
} else { | ||
amountSell = baseAmount; | ||
@@ -328,22 +390,35 @@ amountBuy = quoteAmount; | ||
tokenBuy = quote.address; | ||
} else { | ||
amountSell = quoteAmount; | ||
amountBuy = baseAmount; | ||
tokenSell = quote.address; | ||
tokenBuy = base.address; | ||
} | ||
const createHash = (nonce) => { | ||
let data = _.padStart(nonce.toString(16), 8, '0'); | ||
data += _.padStart(takerFee.toString(16), 4, '0'); | ||
data += _.padStart(makerFee.toString(16), 4, '0'); | ||
data += _.padStart(feePrice.toString(16), 7, '0'); | ||
data += side === 'buy' ? '1' : '0'; | ||
let input; | ||
if (quote === this.tokenManager.eth) { | ||
let data = _.padStart(nonce.toString(16), 8, '0'); | ||
data += _.padStart(takerFee.toString(16), 4, '0'); | ||
data += _.padStart(makerFee.toString(16), 4, '0'); | ||
data += _.padStart(feePrice.toString(16), 7, '0'); | ||
data += side === 'buy' ? '1' : '0'; | ||
let input = this.system.contract.substr(2); | ||
input += _.padStart(amountSell.toString(16), 64, '0'); | ||
input += _.padStart(amountBuy.toString(16), 64, '0'); | ||
input += _.padStart(gasFee.toString(16), 64, '0'); | ||
input += data; | ||
input += quote.address.substr(2); | ||
input = this.system.contract.substr(2); | ||
input += _.padStart(amountSell.toString(16), 64, '0'); | ||
input += _.padStart(amountBuy.toString(16), 64, '0'); | ||
input += _.padStart(gasFee.toString(16), 64, '0'); | ||
input += data; | ||
input += base.address.substr(2); | ||
} else { | ||
let data = _.padStart(nonce.toString(16), 8, '0'); | ||
data += _.padStart(takerFee.toString(16), 4, '0'); | ||
data += _.padStart(makerFee.toString(16), 4, '0'); | ||
data += '0000000'; | ||
data += side === 'buy' ? '1' : '0'; | ||
input = this.system.contract.substr(2); | ||
input += _.padStart(amountSell.toString(16), 64, '0'); | ||
input += _.padStart(amountBuy.toString(16), 64, '0'); | ||
input += _.padStart(gasFee.toString(16), 64, '0'); | ||
input += data; | ||
input += base.address.substr(2); | ||
input += quote.address.substr(2); | ||
input += _.padStart(feePrice.toString(16), 64, '0'); | ||
} | ||
return ethUtil.keccak256(new Buffer(input, 'hex')); | ||
@@ -364,3 +439,3 @@ }; | ||
taker_fee: takerFee, | ||
fee_price: feePrice, | ||
fee_price: feePrice.toString(10), | ||
token_sell: tokenSell.substr(2), | ||
@@ -373,4 +448,5 @@ token_buy: tokenBuy.substr(2), | ||
amount_buy: amountBuy.toString(10), | ||
payment_method: fee, | ||
hash: hash.toString('hex') | ||
payment_method: feeByJoy ? 'joy' : 'base', | ||
hash: hash.toString('hex'), | ||
is_buy: side === 'buy' | ||
}, vrs); | ||
@@ -380,8 +456,6 @@ } | ||
cancel(orderId) { | ||
return rp(this.createRequest(`orders/${orderId}`, { | ||
return this.request(`orders/${orderId}`, { | ||
method: 'DELETE', | ||
headers: { | ||
Authorization: `Bearer ${this.accessToken}` | ||
} | ||
})); | ||
auth: true | ||
}); | ||
} | ||
@@ -394,5 +468,3 @@ | ||
const [base, quote] = this.tokenManager.getPair(pair); | ||
if (!base || !quote || base !== this.tokenManager.eth) { | ||
throw new Error('invalid pair'); | ||
} | ||
this.validatePair(base, quote); | ||
if (this.orderBooks[pair]) { | ||
@@ -416,5 +488,3 @@ this.orderBooks[pair].unsubscribe(); | ||
const [base, quote] = this.tokenManager.getPair(pair); | ||
if (!base || !quote || base !== this.tokenManager.eth) { | ||
throw new Error('invalid pair'); | ||
} | ||
this.validatePair(base, quote); | ||
if (this.trades[pair]) { | ||
@@ -515,7 +585,22 @@ this.trades[pair].unsubscribe(); | ||
createRequest(path, options) { | ||
return Object.assign({ | ||
request(path, options = {}) { | ||
let { headers, method, auth, data = {} } = options; | ||
delete options.data; | ||
delete options.auth; | ||
if (auth) { | ||
headers = headers || {}; | ||
headers['Authorization'] = `Bearer ${this.accessToken}`; | ||
options.headers = headers; | ||
} | ||
const attr = (method || 'GET').toUpperCase() == 'GET' ? 'qs' : 'body'; | ||
options[attr] = options[attr] || {}; | ||
Object.assign(options[attr], data); | ||
options = Object.assign({ | ||
uri: `${this.apiUrl}/${path}`, | ||
json: true | ||
}, options); | ||
return rp(options); | ||
} | ||
@@ -522,0 +607,0 @@ } |
@@ -54,2 +54,3 @@ const rp = require('request-promise'); | ||
unsubscribe() { | ||
clearTimeout(this.retryTimer); | ||
this.cable.unsubscribe(); | ||
@@ -65,16 +66,23 @@ delete this.cable; | ||
this.requesting = this.requestId; | ||
clearTimeout(this.retryTimer); | ||
const after = this.trades.length ? this.trades[0].id : null; | ||
const json = await this.get(after); | ||
const trades = this.convert(json.trades, true); | ||
if (after) { | ||
this.trades.unshift(...trades); | ||
} else { | ||
this.trades = trades; | ||
} | ||
this.onReceived(this.trades); | ||
if (this.requesting !== this.requestId) { | ||
try { | ||
const json = await this.get(after); | ||
const trades = this.convert(json.trades, true); | ||
if (after) { | ||
this.trades.unshift(...trades); | ||
} else { | ||
this.trades = trades; | ||
} | ||
this.onReceived(this.trades); | ||
if (this.requesting !== this.requestId) { | ||
this.requesting = 0; | ||
this.update(); | ||
} else { | ||
this.requesting = 0; | ||
} | ||
} catch (e) { | ||
console.log(e); | ||
this.requesting = 0; | ||
this.update(); | ||
} else { | ||
this.requesting = 0; | ||
this.retryTimer = setTimeout(() => this.update(), 5000); | ||
} | ||
@@ -85,7 +93,7 @@ } | ||
return trades.map(trade => { | ||
const base = this.client.tokenManager.addressMap[`0x${trade.token_base}`], | ||
quote = this.client.tokenManager.addressMap[`0x${trade.token_target}`], | ||
const quote = this.client.tokenManager.addressMap[`0x${trade.token_base}`], | ||
base = this.client.tokenManager.addressMap[`0x${trade.token_target}`], | ||
tokenFee = this.client.tokenManager.addressMap[`0x${trade.token_fee}`], | ||
baseAmount = this.client.tokenManager.toAmount(base, trade.amount_base), | ||
quoteAmount = this.client.tokenManager.toAmount(quote, trade.amount_target), | ||
baseAmount = this.client.tokenManager.toAmount(base, trade.amount_target), | ||
quoteAmount = this.client.tokenManager.toAmount(quote, trade.amount_base), | ||
gasFee = this.client.tokenManager.toAmount(tokenFee, trade.gas_fee), | ||
@@ -98,4 +106,4 @@ txFee = this.client.tokenManager.toAmount(tokenFee, trade.tx_fee); | ||
side: trade.is_buy ? 'sell' : 'buy', | ||
price: baseAmount.div(quoteAmount).round(9).toNumber(), | ||
amount: quoteAmount, | ||
price: quoteAmount.div(baseAmount).round(9).toNumber(), | ||
amount: baseAmount, | ||
pair: `${base.symbol}_${quote.symbol}`, | ||
@@ -111,8 +119,8 @@ fee: tokenFee.symbol, | ||
get(after = null) { | ||
return rp(this.client.createRequest('trades/mine', { | ||
qs: { | ||
return this.client.request('trades/mine', { | ||
data: { | ||
user: this.address.substr(2), | ||
after: after | ||
} | ||
})); | ||
}); | ||
} | ||
@@ -119,0 +127,0 @@ } |
@@ -17,4 +17,4 @@ const rp = require('request-promise'); | ||
contract: this.client.system.contract.substr(2), | ||
base: this.base.address.substr(2), | ||
token: this.quote.address.substr(2) | ||
token: this.base.address.substr(2), | ||
base: this.quote.address.substr(2) | ||
}, { | ||
@@ -41,2 +41,3 @@ connected: () => this.update(), | ||
unsubscribe() { | ||
clearTimeout(this.retryTimer); | ||
this.cable.unsubscribe(); | ||
@@ -48,10 +49,16 @@ delete this.cable; | ||
async update() { | ||
const json = await this.get(); | ||
['buy', 'sell'].forEach(t => { | ||
Object.keys(json[t]).forEach(k => { | ||
json[t][k] = new BigNumber(json[t][k]); | ||
try { | ||
clearTimeout(this.retryTimer); | ||
const json = await this.get(); | ||
['buy', 'sell'].forEach(t => { | ||
Object.keys(json[t]).forEach(k => { | ||
json[t][k] = new BigNumber(json[t][k]); | ||
}); | ||
}); | ||
}); | ||
this.orderBook = json; | ||
this.notify(); | ||
this.orderBook = json; | ||
this.notify(); | ||
} catch (e) { | ||
console.log(e); | ||
this.retryTimer = setTimeout(() => this.update(), 5000); | ||
} | ||
} | ||
@@ -73,15 +80,15 @@ | ||
convert(key, amount) { | ||
const quote = this.client.tokenManager.toAmount(this.quote, amount); | ||
const base = this.client.tokenManager.toAmount(this.base, amount); | ||
const price = parseFloat(key); | ||
return { price, amount: quote }; | ||
return { price, amount: base }; | ||
} | ||
get() { | ||
return rp(this.client.createRequest('orders', { | ||
qs: { | ||
return this.client.request('orders', { | ||
data: { | ||
contract: this.client.system.contract.substr(2), | ||
base: this.base.address.substr(2), | ||
token: this.quote.address.substr(2) | ||
token: this.base.address.substr(2), | ||
base: this.quote.address.substr(2) | ||
} | ||
})); | ||
}); | ||
} | ||
@@ -88,0 +95,0 @@ } |
@@ -52,2 +52,3 @@ const rp = require('request-promise'); | ||
unsubscribe() { | ||
clearTimeout(this.retryTimer); | ||
this.cable.unsubscribe(); | ||
@@ -63,16 +64,23 @@ delete this.cable; | ||
this.requesting = this.requestId; | ||
clearTimeout(this.retryTimer); | ||
const after = this.orders.length ? this.orders[0].id : null; | ||
const result = await this.get(after); | ||
const orders = this.convert(result.orders); | ||
if (after) { | ||
this.orders.unshift(...orders); | ||
} else { | ||
this.orders = orders; | ||
} | ||
this.onReceived(this.orders); | ||
if (this.requesting !== this.requestId) { | ||
try { | ||
const result = await this.get(after); | ||
const orders = this.convert(result.orders); | ||
if (after) { | ||
this.orders.unshift(...orders); | ||
} else { | ||
this.orders = orders; | ||
} | ||
this.onReceived(this.orders); | ||
if (this.requesting !== this.requestId) { | ||
this.requesting = 0; | ||
this.update(); | ||
} else { | ||
this.requesting = 0; | ||
} | ||
} catch (e) { | ||
console.log(e); | ||
this.requesting = 0; | ||
this.update(); | ||
} else { | ||
this.requesting = 0; | ||
this.retryTimer = setTimeout(() => this.update(), 5000); | ||
} | ||
@@ -87,3 +95,3 @@ } | ||
amountBuy = this.client.tokenManager.toAmount(tokenBuy, o.amount_buy); | ||
const [base, quote] = o.is_buy ? [tokenSell, tokenBuy] : [tokenBuy, tokenSell]; | ||
const [quote, base] = o.is_buy ? [tokenSell, tokenBuy] : [tokenBuy, tokenSell]; | ||
return { | ||
@@ -95,3 +103,3 @@ id: o.id, | ||
amount: o.is_buy ? amountBuy : amountSell, | ||
fill: this.client.tokenManager.toAmount(quote, o.amount_fill), | ||
fill: this.client.tokenManager.toAmount(base, o.amount_fill), | ||
pair: `${base.symbol}_${quote.symbol}` | ||
@@ -103,4 +111,4 @@ }; | ||
get(after = null) { | ||
return rp(this.client.createRequest('orders/mine', { | ||
qs: { | ||
return this.client.request('orders/mine', { | ||
data: { | ||
contract: this.client.system.contract.substr(2), | ||
@@ -110,3 +118,3 @@ user: this.address.substr(2), | ||
} | ||
})); | ||
}); | ||
} | ||
@@ -113,0 +121,0 @@ } |
@@ -0,12 +1,29 @@ | ||
const EventEmitter = require('events'); | ||
const rp = require('request-promise'); | ||
class System { | ||
class System extends EventEmitter { | ||
constructor(client) { | ||
super(); | ||
this.client = client; | ||
} | ||
subscribe() { | ||
connect() { | ||
if (this.cable) { | ||
return Promise.reject(); | ||
} | ||
const promise = new Promise((resolve, reject) => { | ||
const timer = setTimeout(() => reject(new Error('timeout')), 15000); | ||
this.once('update', () => { | ||
clearTimeout(timer); | ||
resolve(); | ||
}); | ||
this.once('error', e => { | ||
clearTimeout(timer); | ||
reject(e); | ||
}); | ||
}); | ||
this.cable = this.client.cable.subscriptions.create({ | ||
channel: 'SystemChannel' | ||
}, { | ||
connected: () => this.update(), | ||
received: data => { | ||
@@ -20,5 +37,7 @@ switch (data.e) { | ||
}); | ||
return promise; | ||
} | ||
unsubscribe() { | ||
destroy() { | ||
clearTimeout(this.retryTimer); | ||
this.cable.unsubscribe(); | ||
@@ -29,6 +48,13 @@ delete this.cable; | ||
async update() { | ||
const json = await rp(this.client.createRequest('system')); | ||
this.contract = `0x${json.contracts[0]}`; | ||
this.updateConfig(json); | ||
return json; | ||
try { | ||
clearTimeout(this.retryTimer); | ||
const json = await this.client.request('system'); | ||
this.connected = true; | ||
this.contract = `0x${json.contracts[0]}`; | ||
this.updateConfig(json); | ||
return json; | ||
} catch (e) { | ||
this.emit('error', e); | ||
this.retryTimer = setTimeout(() => this.update(), 5000); | ||
} | ||
} | ||
@@ -41,2 +67,3 @@ | ||
this.withdrawable = json.withdrawable; | ||
this.emit('update', json); | ||
} | ||
@@ -43,0 +70,0 @@ } |
const BigNumber = require('bignumber.js'); | ||
class TokenManager { | ||
constructor(client, tokens) { | ||
constructor(client, options) { | ||
this.client = client; | ||
this.reload(tokens); | ||
this.reload(options); | ||
} | ||
@@ -17,3 +17,2 @@ | ||
case 'update': | ||
console.log(data.e); | ||
const token = this.addressMap[`0x${data.data.address}`]; | ||
@@ -42,10 +41,12 @@ if (token) { | ||
const json = await this.client.system.update(); | ||
this.reload(json.tokens); | ||
if (json) { | ||
this.reload(json); | ||
} | ||
} | ||
reload(tokens) { | ||
this.tokens = tokens; | ||
reload(json) { | ||
this.tokens = json.tokens; | ||
this.symbolMap = {}; | ||
this.addressMap = {}; | ||
tokens.forEach(t => { | ||
json.tokens.forEach(t => { | ||
t.address = `0x${t.address}`; | ||
@@ -62,5 +63,19 @@ if (t.price) { | ||
this.joy = this.symbolMap.JOY; | ||
this.quotes = json.quotes.map(t => { | ||
const token = this.addressMap[`0x${t.address}`]; | ||
token.precision = t.precision; | ||
return token; | ||
}); | ||
this.hiddenPairs = {}; | ||
Object.keys(json.hidden_pairs).forEach(quote => { | ||
json.hidden_pairs[quote].forEach(base => { | ||
this.hiddenPairs[`${base}_${quote}`] = true; | ||
}); | ||
}); | ||
} | ||
getPair(pair) { | ||
if (this.hiddenPairs[pair]) { | ||
return []; | ||
} | ||
let [base, quote] = pair.split('_'); | ||
@@ -67,0 +82,0 @@ return [this.symbolMap[base], this.symbolMap[quote]]; |
@@ -20,4 +20,4 @@ const rp = require('request-promise'); | ||
contract: this.client.system.contract.substr(2), | ||
base: this.base.address.substr(2), | ||
token: this.quote.address.substr(2) | ||
token: this.base.address.substr(2), | ||
base: this.quote.address.substr(2) | ||
}, { | ||
@@ -33,2 +33,3 @@ connected: () => this.update(), | ||
unsubscribe() { | ||
clearTimeout(this.retryTimer); | ||
this.cable.unsubscribe(); | ||
@@ -44,16 +45,23 @@ delete this.cable; | ||
this.requesting = this.requestId; | ||
clearTimeout(this.retryTimer); | ||
const after = this.trades.length ? this.trades[0].id : null; | ||
const json = await this.get(after); | ||
const trades = this.convert(json.trades, false); | ||
if (after) { | ||
this.trades.unshift(...trades); | ||
} else { | ||
this.trades = trades; | ||
} | ||
this.onReceived(this.trades); | ||
if (this.requesting !== this.requestId) { | ||
try { | ||
const json = await this.get(after); | ||
const trades = this.convert(json.trades, false); | ||
if (after) { | ||
this.trades.unshift(...trades); | ||
} else { | ||
this.trades = trades; | ||
} | ||
this.onReceived(this.trades); | ||
if (this.requesting !== this.requestId) { | ||
this.requesting = 0; | ||
this.update(); | ||
} else { | ||
this.requesting = 0; | ||
} | ||
} catch (e) { | ||
console.log(e); | ||
this.requesting = 0; | ||
this.update(); | ||
} else { | ||
this.requesting = 0; | ||
this.retryTimer = setTimeout(() => this.update(), 5000); | ||
} | ||
@@ -64,11 +72,11 @@ } | ||
return trades.map(trade => { | ||
const base = this.client.tokenManager.addressMap[`0x${trade.token_base}`], | ||
quote = this.client.tokenManager.addressMap[`0x${trade.token_target}`], | ||
baseAmount = this.client.tokenManager.toAmount(base, trade.amount_base), | ||
quoteAmount = this.client.tokenManager.toAmount(quote, trade.amount_target); | ||
const quote = this.client.tokenManager.addressMap[`0x${trade.token_base}`], | ||
base = this.client.tokenManager.addressMap[`0x${trade.token_target}`], | ||
baseAmount = this.client.tokenManager.toAmount(base, trade.amount_target), | ||
quoteAmount = this.client.tokenManager.toAmount(quote, trade.amount_base); | ||
return { | ||
id: trade.id, | ||
side: trade.is_buy ? 'sell' : 'buy', | ||
price: baseAmount.div(quoteAmount).round(9).toNumber(), | ||
amount: quoteAmount, | ||
price: quoteAmount.div(baseAmount).round(9).toNumber(), | ||
amount: baseAmount, | ||
pair: `${base.symbol}_${quote.symbol}`, | ||
@@ -81,9 +89,9 @@ timestamp: trade.timestamp | ||
get(after = null) { | ||
return rp(this.client.createRequest('trades', { | ||
qs: { | ||
base: this.base.address.substr(2), | ||
token: this.quote.address.substr(2), | ||
return this.client.request('trades', { | ||
data: { | ||
token: this.base.address.substr(2), | ||
base: this.quote.address.substr(2), | ||
after: after | ||
} | ||
})); | ||
}); | ||
} | ||
@@ -90,0 +98,0 @@ } |
73956
1956
352