import Decimal from "decimal.js"; import * as rp from 'request-promise-native'; import { AuthenticationProvider } from "./authentication-provider"; import { IAccount, IAccountBalance, IApiKeyPermissions, IApiKeys, IStrategy, ITicker, ITrade, ITradeChanges, IUser, } from "../models"; import { IAccountBalanceDto, IGuidIdResultDto, INumberIdResultDto, IRebalancePeriodResultDto, IStrategyDto, ITickerDto, ITradeChangesDto, ITradeDto, IUserDto, } from "../dtos"; import { AccountBalanceDtoConverter, DecimalDtoConverter, StrategyDtoConverter, TickerDtoConverter, TradeChangesDtoConverter, TradeDtoConverter, UserDtoConverter, } from "../dto-converters"; export class ShrimpyApiClient { private _accountBalanceDtoConverter = new AccountBalanceDtoConverter(); private _decimalDtoConverter = new DecimalDtoConverter(); private _userDtoConverter = new UserDtoConverter(); private _strategyDtoConverter = new StrategyDtoConverter(); private _tickerDtoConverter = new TickerDtoConverter(); private _tradeChangesDtoConverter = new TradeChangesDtoConverter(); private _tradeDtoConverter = new TradeDtoConverter(); private _authenticationProvider: AuthenticationProvider | null = null; constructor( publicKey?: string, privateKey?: string, ) { if (publicKey && privateKey) { // keys were supplied, use them this._setApiCredentials(publicKey, privateKey); } } public async getTicker(exchange: string): Promise { const endpoint = `${exchange}/ticker`; const tickerDtos = await this._callEndpoint(endpoint, 'GET', null, false); return tickerDtos.map((tickerDto) => { return this._tickerDtoConverter.convertFromDto(tickerDto); }); } public async getUsers(): Promise { const endpoint = 'users'; const userDtos = await this._callEndpoint(endpoint, 'GET', null, true); return userDtos.map((userDto) => { return this._userDtoConverter.convertFromDto(userDto); }); } public async getUser(userId: string): Promise { const endpoint = `users/${userId}`; const userDto = await this._callEndpoint(endpoint, 'GET', null, true); return this._userDtoConverter.convertFromDto(userDto); } public async createUser(): Promise { const endpoint = 'users'; const result = await this._callEndpoint(endpoint, 'POST', null, true); return result.id; } public async enableUser(userId: string): Promise { const endpoint = `users/${userId}/enable`; await this._callEndpoint(endpoint, 'POST', null, true); } public async disableUser(userId: string): Promise { const endpoint = `users/${userId}/disable`; await this._callEndpoint(endpoint, 'POST', null, true); } public async createApiKeys(userId: string): Promise { const endpoint = `users/${userId}/keys`; return await this._callEndpoint(endpoint, 'POST', null, true); } public async deleteApiKeys(userId: string, publicKey: string): Promise { const endpoint = `users/${userId}/keys/${publicKey}`; return await this._callEndpoint(endpoint, 'DELETE', null, true); } public async getApiKeys(userId: string): Promise { const endpoint = `users/${userId}/keys`; return await this._callEndpoint(endpoint, 'GET', null, true); } public async setPermissions( userId: string, publicKey: string, account: boolean, trade: boolean, ): Promise { const endpoint = `users/${userId}/keys/${publicKey}/permissions`; const parameters: IApiKeyPermissions = { account: account, trade: trade }; await this._callEndpoint(endpoint, 'POST', parameters, true); } public async getPermissions(userId: string, publicKey: string): Promise { const endpoint = `users/${userId}/keys/${publicKey}/permissions`; return await this._callEndpoint(endpoint, 'GET', null, true); } public async getAccounts(userId: string): Promise { const endpoint = `users/${userId}/accounts`; return await this._callEndpoint(endpoint, 'GET', null, true); } public async getAccount(userId: string, accountId: number): Promise { const endpoint = `users/${userId}/accounts/${accountId}`; return await this._callEndpoint(endpoint, 'GET', null, true); } public async createAccount( userId: string, exchange: string, publicKey: string, privateKey: string, passphrase: string, ): Promise { const endpoint = `users/${userId}/accounts`; const parameters: { [key: string]: any } = { exchange: exchange, publicKey: publicKey, privateKey: privateKey, passphrase: passphrase, }; const result = await this._callEndpoint(endpoint, 'POST', parameters, true); return result.id; } public async deleteAccount(userId: string, accountId: number): Promise { const endpoint = `users/${userId}/accounts/${accountId}`; await this._callEndpoint(endpoint, 'DELETE', null, true); } public async rebalance(userId: string, accountId: number): Promise { const endpoint = `users/${userId}/accounts/${accountId}/rebalance`; await this._callEndpoint(endpoint, 'POST', null, true); } public async getRebalancePeriod(userId: string, accountId: number): Promise { const endpoint = `users/${userId}/accounts/${accountId}/rebalance_period`; const result = await this._callEndpoint(endpoint, 'GET', null, true); return result.rebalancePeriod; } public async setRebalancePeriod( userId: string, accountId: number, rebalancePeriodHours: number ): Promise { // verify rebalancePeriodHours is an integer if (!Number.isInteger(rebalancePeriodHours)) { throw new Error("Invalid rebalance period. Rebalance period must be an integer"); } const endpoint = `users/${userId}/accounts/${accountId}/rebalance_period`; const parameters: { rebalancePeriod: number } = { rebalancePeriod: rebalancePeriodHours }; await this._callEndpoint(endpoint, 'POST', parameters, true); } public async setStrategy( userId: string, accountId: number, strategy: IStrategy, ): Promise { const endpoint = `users/${userId}/accounts/${accountId}/strategy`; let parameters: IStrategyDto = this._strategyDtoConverter.convertToDto(strategy); await this._callEndpoint(endpoint, 'POST', parameters, true); } public async getStrategy( userId: string, accountId: number, ): Promise { const endpoint = `users/${userId}/accounts/${accountId}/strategy`; const strategyDto = await this._callEndpoint(endpoint, 'GET', null, true); return this._strategyDtoConverter.convertFromDto(strategyDto); } public async allocate( userId: string, accountId: number, strategy: IStrategy, ): Promise { const endpoint = `users/${userId}/accounts/${accountId}/allocate`; let parameters: IStrategyDto = this._strategyDtoConverter.convertToDto(strategy); await this._callEndpoint(endpoint, 'POST', parameters, true); } public async createTrade( userId: string, accountId: number, fromSymbol: string, toSymbol: string, amount: Decimal ): Promise { const endpoint = `users/${userId}/accounts/${accountId}/trades`; let parameters: { fromSymbol: string, toSymbol: string, amount: string } = { fromSymbol: fromSymbol, toSymbol: toSymbol, amount: this._decimalDtoConverter.convertToDto(amount), }; const result = await this._callEndpoint(endpoint, 'POST', parameters, true); return result.id; } public async getActiveTrades(userId: string, accountId: number): Promise { const endpoint = `users/${userId}/accounts/${accountId}/trades`; const tradeDtos = await this._callEndpoint(endpoint, 'GET', null, true); return tradeDtos.map((tradeDto) => { return this._tradeDtoConverter.convertFromDto(tradeDto); }); } public async getTrade( userId: string, accountId: number, tradeId: string, ): Promise { const endpoint = `users/${userId}/accounts/${accountId}/trades/${tradeId}`; const tradeChangesDto = await this._callEndpoint(endpoint, 'GET', null, true); return this._tradeChangesDtoConverter.convertFromDto(tradeChangesDto); } public async getBalance(userId: string, accountId: number): Promise { const endpoint = `users/${userId}/accounts/${accountId}/balance`; const accountBalanceDto = await this._callEndpoint(endpoint, 'GET', null, true); return this._accountBalanceDtoConverter.convertFromDto(accountBalanceDto); } private _setApiCredentials(publicKey: string, privateKey: string): void { this._authenticationProvider = new AuthenticationProvider(publicKey, privateKey); } private async _callEndpoint( endPoint: string, method: 'GET' | 'POST' | 'DELETE', parameters: { [key: string]: any } | null, signed: boolean ): Promise { const pathname = "/v1/" + endPoint; let options: rp.OptionsWithUri & { headers: { [key: string]: any }} = { uri: "https://dev-api.shrimpy.io" + pathname, headers: { 'content-type': 'application/json', }, method: method }; if (method == 'POST') { if (parameters) { // only attach the body if there are parameters // no parameters, send empty body options.body = JSON.stringify(parameters); } else { options.body = ""; } } if (signed) { // this method is signed, create the signature if (!this._authenticationProvider) { throw new Error(`Cannot send a request to ${endPoint} without api keys. Make sure to pass api keys to the ShrimpyApiClient constructor.`); } const nonce = Date.now(); const bodyString = options.body ? options.body : ""; const prehashString = pathname + method + nonce + bodyString; const signature = this._authenticationProvider.sign(prehashString); options.headers['DEV-SHRIMPY-API-KEY'] = this._authenticationProvider.publicKey; options.headers['DEV-SHRIMPY-API-NONCE'] = nonce; options.headers['DEV-SHRIMPY-API-SIGNATURE'] = signature; } const response: string = await rp(options); return JSON.parse(response); } }