Skip to content

Commit

Permalink
DP-719: SDK now handles binaries properly
Browse files Browse the repository at this point in the history
Support for uploading binaries (trade-chat/image/upload) and downloading binaries (trade-chat/image) has been added.

Co-authored-by: Flavio Prado <[email protected]>
  • Loading branch information
Bakrog and Flavio Prado authored Sep 29, 2021
1 parent 74bf790 commit 9da0747
Show file tree
Hide file tree
Showing 8 changed files with 179 additions and 31 deletions.
44 changes: 31 additions & 13 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"test": "jest --ci --testResultsProcessor='jest-junit' --passWithNoTests --colors --coverage",
"lint": "eslint . --ext .ts",
"lint-and-fix": "eslint . --ext .ts --fix",
"doc": "typedoc --out public --readme README.md --exclude \"**/__tests__/**/*.+(ts|tsx|js)\" --exclude \"**/jest.config.js\" --excludeNotDocumented --name \"Paxful SDK (Javascript)\" --hideGenerator --disableSources --highlightTheme light-plus src/"
"doc": "typedoc --out public --readme README.md --exclude \"**/__tests__/**/*.+(ts|tsx|js)\" --exclude \"**/jest.config.js\" --excludeNotDocumented --name \"Paxful SDK (Javascript)\" --hideGenerator --disableSources --highlightTheme light-plus src/",
"postinstall": "tsc"
},
"keywords": [],
"author": "Paxful Inc",
Expand Down Expand Up @@ -42,6 +43,7 @@
},
"dependencies": {
"dotenv": "^8.2.0",
"form-data": "^4.0.0",
"node-fetch": "^2.6.1",
"q-flat": "^1.0.7",
"query-string": "^7.0.1",
Expand Down
11 changes: 9 additions & 2 deletions src/PaxfulApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ import { Http2ServerResponse } from "http2";

import { Profile, Credentials, CredentialStorage } from "./oauth";
import { ApiConfiguration } from "./ApiConfiguration";
import { authorize, retrieveImpersonatedCredentials, retrievePersonalCredentials, getProfile, invoke } from "./commands";
import {
authorize,
retrieveImpersonatedCredentials,
retrievePersonalCredentials,
getProfile,
invoke,
InvokeBody
} from "./commands";

/**
* Interface responsable for exposing Paxful API integration.
Expand Down Expand Up @@ -62,7 +69,7 @@ export class PaxfulApi {
* @param payload - (Optional) Payload of the request
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
public invoke(url: string, payload?: Record<string, unknown> | []): Promise<any> {
public invoke(url: string, payload?: InvokeBody): Promise<any> {
return invoke(url, this.apiConfiguration, this.credentialStorage, payload);
}

Expand Down
Binary file added src/__tests__/paxful.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/__tests__/paxful.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This is a test file
97 changes: 92 additions & 5 deletions src/__tests__/paxfulapi_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { v4 as UUID } from "uuid";
import fetch from "node-fetch";
import { mock } from "jest-mock-extended";
import ProxyAgent from "simple-proxy-agent";
import { createReadStream, readFileSync, ReadStream } from "fs";
import { resolve } from "path";

import usePaxful from "../";
import { CredentialStorage } from "../oauth";
Expand Down Expand Up @@ -36,8 +38,14 @@ const userProfile = {
email_verified: true
};

const headers = {
"Content-Type": "application/json; charset=UTF-8"
};

const credentialStorage = mock<CredentialStorage>();
const paxfulTradeUrl = '/paxful/v1/trade/get';
const paxfulTradeChatImageDownloadUrl = '/paxful/v1/trade-chat/image';
const paxfulTradeChatImageUploadUrl = '/paxful/v1/trade-chat/image/upload';

function mockCredentialsStorageReturnValue() {
credentialStorage.getCredentials.mockReturnValueOnce({
Expand All @@ -46,6 +54,41 @@ function mockCredentialsStorageReturnValue() {
});
}

async function upload_trade_chat_attachment(file: ReadStream | Buffer) {
credentialStorage.getCredentials.mockReturnValueOnce({
...expectedTokenAnswer,
expiresAt: new Date()
});

const expectedAnswer = {
_data: {
id: "<id_here>",
success: true
},
status: "success",
timestamp: 1455032576
};

(fetch as unknown as FetchMockSandbox).once({
url: /https:\/\/api\.paxful\.com\/paxful\/v1\/trade-chat\/image\/upload/,
method: "POST",
}, {
status: 200,
body: JSON.stringify(expectedAnswer),
headers
}, {
sendAsJson: false
});

const paxfulApi = usePaxful(credentials, credentialStorage);
const answer = await paxfulApi.invoke(paxfulTradeChatImageUploadUrl, {
trade_hash: "random_hash",
file
});

expect(answer).toMatchObject(expectedAnswer);
}

describe("With the Paxful API SDK", function () {

beforeEach(() => {
Expand All @@ -61,7 +104,8 @@ describe("With the Paxful API SDK", function () {
access_token: expectedTokenAnswer.accessToken,
refresh_token: expectedTokenAnswer.refreshToken,
expires_in: ttl
})
}),
headers
}, {
sendAsJson: false,
});
Expand Down Expand Up @@ -227,7 +271,8 @@ describe("With the Paxful API SDK", function () {
method: "POST"
}, {
status: 200,
body: JSON.stringify(expectedTrades)
body: JSON.stringify(expectedTrades),
headers
}, {
sendAsJson: false
});
Expand All @@ -252,7 +297,8 @@ describe("With the Paxful API SDK", function () {
method: "POST"
}, {
status: 200,
body: JSON.stringify(expectedTrades)
body: JSON.stringify(expectedTrades),
headers
}, {
sendAsJson: false
});
Expand All @@ -264,6 +310,45 @@ describe("With the Paxful API SDK", function () {
expect(trades).toMatchObject(expectedTrades);
});

it('I can get my trade chat images', async function () {
credentialStorage.getCredentials.mockReturnValueOnce({
...expectedTokenAnswer,
expiresAt: new Date()
});

const expectedImage = new ArrayBuffer(10);

(fetch as unknown as FetchMockSandbox).once({
url: /https:\/\/api\.paxful\.com\/paxful\/v1\/trade-chat\/image/,
method: "POST"
}, {
status: 200,
body: expectedImage,
headers: {
"Content-Type": "image/png; charset=UTF-8"
}
}, {
sendAsJson: false
});

const paxfulApi = usePaxful(credentials, credentialStorage);

const image = await paxfulApi.invoke(paxfulTradeChatImageDownloadUrl);

expect(image).toMatchObject(expectedImage);
});

it('I can upload my trade chat images as a ReadStream', async function () {
const image = createReadStream(resolve(__dirname, 'paxful.png'));

await upload_trade_chat_attachment(image);
});

it('I can upload my trade chat files as a Buffer content', async function () {
const file = readFileSync(resolve(__dirname, 'paxful.txt'));
await upload_trade_chat_attachment(file);
});

it('I can get my trades using a proxy', async function () {
credentialStorage.getCredentials.mockReturnValueOnce({
...expectedTokenAnswer,
Expand All @@ -277,7 +362,8 @@ describe("With the Paxful API SDK", function () {
method: "POST"
}, {
status: 200,
body: JSON.stringify(expectedTrades)
body: JSON.stringify(expectedTrades),
headers
}, {
sendAsJson: false
});
Expand Down Expand Up @@ -361,7 +447,8 @@ describe("With the Paxful API SDK", function () {
}
}, {
status: 200,
body: JSON.stringify(expectedTrades)
body: JSON.stringify(expectedTrades),
headers
}, {
sendAsJson: false
});
Expand Down
48 changes: 40 additions & 8 deletions src/commands/Invoke.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,62 @@
import fetch, { Request } from "node-fetch";
import fetch, { BodyInit, Request, Response } from "node-fetch";
import { ReadStream } from "fs";
import queryString from "query-string";
import { flatten } from "q-flat";
import FormData from "form-data";

import validateAndRefresh from "./RefreshIfNeeded";
import retrieveImpersonatedCredentials from "./ImpersonateCredentials";

import { Credentials, CredentialStorage } from "../oauth";
import { ApiConfiguration } from "../ApiConfiguration";
import queryString from 'query-string';
import { flatten } from 'q-flat';

export type InvokeBody = Record<string, unknown> | [] | ReadStream | Buffer;

// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
const createRequest = (url: string, config: ApiConfiguration, credentials: Credentials, payload?: Record<string, unknown> | []): Request => {
const createRequest = (url: string, config: ApiConfiguration, credentials: Credentials, payload?: InvokeBody): Request => {
const headers = {
"Accept": "application/json",
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": `Bearer ${credentials.accessToken}`
};
let body: BodyInit | undefined;
if (url.endsWith('/trade-chat/image/upload') && payload) {
const form: FormData = new FormData();
Object.keys(payload).forEach((key) => {
form.append(key, payload[key]);
})
body = form;
} else {
headers["Content-Type"] = "application/x-www-form-urlencoded";
body = queryString.stringify(flatten(payload), { encode:false });
}
return new Request({
href: `${process.env.PAXFUL_DATA_HOST}${url}`
}, {
method: "POST",
headers,
agent: config.proxyAgent,
body: queryString.stringify(flatten(payload), { encode:false })
body
});
}

const convertResponse = (response: Response) => {
const contentType = response.headers.get("Content-Type");
if (contentType) {
if( contentType.startsWith("text/") ) {
return response.text()
} else if (contentType.indexOf("json") > 0) {
return response.json()
}
}
return response.blob()
.catch(() => (
response.arrayBuffer()
.catch(() => (
response.buffer()
))
))
}

/**
* Retrieves personal access token and refresh token.
*
Expand All @@ -34,11 +66,11 @@ const createRequest = (url: string, config: ApiConfiguration, credentials: Crede
* @param config
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
export default async function invoke(url: string, config: ApiConfiguration, credentialStorage?: CredentialStorage, payload?: Record<string, unknown> | []): Promise<any> {
export default async function invoke(url: string, config: ApiConfiguration, credentialStorage?: CredentialStorage, payload?: InvokeBody): Promise<any> {
const credentials = credentialStorage?.getCredentials() || await retrieveImpersonatedCredentials(config);
const request = createRequest(url, config, credentials, payload);
return fetch(request)
.then(response => validateAndRefresh(request, response, config, credentialStorage))
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.then(response => response.json() as Promise<any>);
.then(convertResponse);
}
Loading

0 comments on commit 9da0747

Please sign in to comment.