diff --git a/README.md b/README.md index 05bf0627..5a71f0a6 100644 --- a/README.md +++ b/README.md @@ -25,31 +25,30 @@ npm install ig-trading-api yarn add ig-trading-api ``` -## Setup +## Usage You can set the API gateway, when initializing the API client. Use `APIClient.URL_DEMO` (demo-api.ig.com) for demo accounts and `APIClient.URL_LIVE` (api.ig.com) for live account access. -**JavaScript / Node.js** - -```javascript -const {APIClient} = require('ig-trading-api'); -const client = new APIClient(APIClient.URL_LIVE, 'your-api-key'); -``` +### TypeScript -**TypeScript** +Recommended: ```typescript import {APIClient} from 'ig-trading-api'; const client = new APIClient(APIClient.URL_LIVE, 'your-api-key'); +const session = await client.rest.login.createSession('your-username', 'your-password'); +console.info(`Your client ID is "${session.clientId}".`); ``` -## Usage - -### Login +Alternative: ```typescript -const session = await client.rest.login.createSession('your-username', 'your-password'); -console.info(`Your client ID is "${session.clientId}".`); +import {APIClient} from 'ig-trading-api'; +const client = new APIClient(APIClient.URL_LIVE, { + apiKey: 'your-api-key', + username: 'your-username', + password: 'your-password', +}); ``` ## Resources diff --git a/src/client/RESTClient.ts b/src/client/RESTClient.ts index e138ddd5..b47c8f08 100644 --- a/src/client/RESTClient.ts +++ b/src/client/RESTClient.ts @@ -50,32 +50,34 @@ export class RESTClient { this.auth = this.apiKey; } - function randomNum(min: number, max: number): number { - return Math.floor(Math.random() * (max - min + 1) + min); - } - axiosRetry(this.httpClient, { retries: Infinity, retryCondition: (error: AxiosError) => { const errorCode = error.response?.data?.errorCode; - const gotRateLimited = errorCode === 'error.public-api.exceeded-api-key-allowance'; - const expiredSecurityToken = errorCode === 'error.security.oauth-token-invalid'; - const missingToken = errorCode === 'error.security.client-token-missing'; - - if (gotRateLimited) { - return true; - } else if (expiredSecurityToken || missingToken) { - void this.login.refreshToken(); - return true; + switch (errorCode) { + case 'error.public-api.exceeded-api-key-allowance': + // Got rate limited + return true; + case 'error.security.oauth-token-invalid': + // Security token expired + void this.login.refreshToken(); + return true; + case 'error.security.client-token-missing': + // Trading session has not been initialized + const {username, password} = this.auth; + if (username && password) { + void this.login.createSession(this.auth.username, this.auth.password); + return true; + } + throw new Error( + `Cannot fulfill request because there is no active session and username & password have not been provided.` + ); + default: + return true; } - - return true; - }, - retryDelay: (retryCount: number) => { - /** Rate limits: https://labs.ig.com/faq */ - return randomNum(1000, 3000) * retryCount; }, + retryDelay: axiosRetry.exponentialDelay, }); this.httpClient.interceptors.request.use(async config => { @@ -92,7 +94,7 @@ export class RESTClient { updatedHeaders.CST = clientSessionToken; } else { if (accessToken) { - updatedHeaders.Authorization = 'Bearer ' + accessToken; + updatedHeaders.Authorization = `Bearer ${accessToken}`; } else if (securityToken && clientSessionToken) { updatedHeaders['X-SECURITY-TOKEN'] = securityToken; updatedHeaders.CST = clientSessionToken; diff --git a/src/dealing/DealingAPI.test.ts b/src/dealing/DealingAPI.test.ts index e4992eb5..ee28615b 100644 --- a/src/dealing/DealingAPI.test.ts +++ b/src/dealing/DealingAPI.test.ts @@ -15,6 +15,7 @@ import { PositionOrderType, PositionUpdateRequest, } from './DealingAPI'; +import {LoginAPI} from '../login'; describe('DealingAPI', () => { describe('getAllOpenPositions', () => { @@ -424,6 +425,91 @@ describe('DealingAPI', () => { expect(error.config['axios-retry'].retryCount).toBe(amountOfRetries); } }, 10_000); + + it('tries to init a trading session when no session was created', async () => { + const dealId = '12345'; + + nock(APIClient.URL_DEMO) + .post( + DealingAPI.URL.WORKINGORDERS_OTC + dealId, + {}, + { + reqheaders: { + _method: 'DELETE', + }, + } + ) + .reply( + 401, + JSON.stringify({ + errorCode: 'error.security.client-token-missing', + }) + ); + + const amountOfRetries = 0; + + const apiClient = new APIClient(APIClient.URL_DEMO, { + apiKey: 'local-demo-api-key', + password: 'local-demo-password', + username: 'local-demo-username', + }); + + apiClient.rest.defaults['axios-retry'] = { + retries: amountOfRetries, + }; + + const createSession = spyOn(apiClient.rest.login, 'createSession').and.callFake(() => {}); + + try { + await apiClient.rest.dealing.deleteOrder(dealId); + fail('Expected error'); + } catch (error) { + expect(createSession).toHaveBeenCalledTimes(1); + } + }); + + it('fails to automatically init a trading session if username and password are not supplied', async () => { + const dealId = '12345'; + + nock(APIClient.URL_DEMO) + .post( + DealingAPI.URL.WORKINGORDERS_OTC + dealId, + {}, + { + reqheaders: { + _method: 'DELETE', + }, + } + ) + .reply( + 401, + JSON.stringify({ + errorCode: 'error.security.client-token-missing', + }) + ); + + const amountOfRetries = 0; + + const apiClient = new APIClient(APIClient.URL_DEMO, { + apiKey: 'local-demo-api-key', + }); + + apiClient.rest.defaults['axios-retry'] = { + retries: amountOfRetries, + }; + + const createSession = spyOn(apiClient.rest.login, 'createSession').and.callFake(() => {}); + + try { + await apiClient.rest.dealing.deleteOrder(dealId); + fail('Expected error'); + } catch (error) { + expect(createSession).not.toHaveBeenCalled(); + expect(error.message).toBe( + 'Cannot fulfill request because there is no active session and username & password have not been provided.' + ); + } + }); }); describe('updateOrder', () => { diff --git a/src/demo/account.ts b/src/demo/account.ts index 7dfa3147..6ce1ecc4 100644 --- a/src/demo/account.ts +++ b/src/demo/account.ts @@ -27,4 +27,7 @@ async function main(): Promise { }); } -main().catch(console.error); +main().catch(error => { + console.error(error); + process.exit(1); +}); diff --git a/src/demo/login.ts b/src/demo/login.ts index a3c4cc84..c93fb7d7 100644 --- a/src/demo/login.ts +++ b/src/demo/login.ts @@ -4,4 +4,7 @@ async function main(): Promise { await initClient(); } -main().catch(console.error); +main().catch(error => { + console.error(error); + process.exit(1); +}); diff --git a/src/demo/order.ts b/src/demo/order.ts index d86ac353..e1a5ef0d 100644 --- a/src/demo/order.ts +++ b/src/demo/order.ts @@ -43,4 +43,7 @@ async function main(): Promise { console.info(`Your deleted order deal reference is "${closeOrderSession.dealReference}".`); } -main().catch(console.error); +main().catch(error => { + console.error(error); + process.exit(1); +}); diff --git a/src/demo/position.ts b/src/demo/position.ts index 9488508e..32c17658 100644 --- a/src/demo/position.ts +++ b/src/demo/position.ts @@ -58,4 +58,7 @@ async function main(): Promise { process.exit(0); } -main().catch(console.error); +main().catch(error => { + console.error(error); + process.exit(1); +}); diff --git a/src/demo/start.ts b/src/demo/start.ts index 6c85e34b..42d7dae3 100644 --- a/src/demo/start.ts +++ b/src/demo/start.ts @@ -14,4 +14,7 @@ async function main(): Promise { global.IGClient = client; } -main().catch(console.error); +main().catch(error => { + console.error(error); + process.exit(1); +}); diff --git a/src/demo/stream.ts b/src/demo/stream.ts index 863839e4..cb6abba8 100644 --- a/src/demo/stream.ts +++ b/src/demo/stream.ts @@ -14,4 +14,7 @@ async function main(): Promise { ); } -main().catch(console.error); +main().catch(error => { + console.error(error); + process.exit(1); +}); diff --git a/src/test/helpers/setupIG.ts b/src/test/helpers/setupIG.ts index 87bfc459..5a0d2d98 100644 --- a/src/test/helpers/setupIG.ts +++ b/src/test/helpers/setupIG.ts @@ -10,7 +10,7 @@ declare global { } beforeEach(() => { - global.client = new APIClient(APIClient.URL_DEMO, ''); + global.client = new APIClient(APIClient.URL_DEMO, 'global-demo-api-key'); }); afterEach(() => nock.cleanAll());