Skip to content
This repository has been archived by the owner on Feb 8, 2024. It is now read-only.

Commit

Permalink
Add support for authorized withdraws / deposits (#84)
Browse files Browse the repository at this point in the history
Addresses #72, though this isn't quite tested yet.

- Provider classes now track info locally. So you don't have to pass `supportedAssets` to `fetchFinalFee`, but you do have to call `fetchSupportedAssets` ASAP.
- Switch back to all camelCase function inputs, since the Transfer stuff already outputs stuff in camelCase. Unfortunately, it means that devs might have to convert names from `info.fields` from snake_case to camelCase (and I think we should stop short of rewriting those values). But I think, better to be consistent and be clear about that consistency than to be correct but confusing.
- Add a function to set bearer tokens.
- Set Authorization headers when deposit/withdrawing an asset that's auth-required.
  • Loading branch information
Morley Zhi authored Jul 18, 2019
1 parent 78dba6a commit 799638c
Show file tree
Hide file tree
Showing 8 changed files with 281 additions and 34 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@stellar/wallet-sdk",
"version": "0.0.3-rc.14",
"version": "0.0.3-rc.15",
"description": "Libraries to help you write Stellar-enabled wallets in Javascript",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down Expand Up @@ -64,6 +64,7 @@
"@types/jest": "^24.0.11",
"@types/stellar-base": "^0.10.2",
"bignumber.js": "^8.1.1",
"change-case": "^3.1.0",
"lodash": "^4.17.14",
"query-string": "^6.4.2",
"scrypt-async": "^2.0.1",
Expand Down
42 changes: 38 additions & 4 deletions src/transfers/DepositProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,37 @@ import { DepositProvider } from "./DepositProvider";

import { DepositInfo } from "../types";

describe("makeSnakeCase", () => {
test("changes camelcase things", () => {
const provider = new DepositProvider("test");
const req = {
type: "type",
assetCode: "assetCode",
dest: "dest",
destExtra: "destExtra",
account: "account",
};
expect(provider.makeSnakeCase(req)).toEqual({
type: "type",
asset_code: "assetCode",
dest: "dest",
dest_extra: "destExtra",
account: "account",
});
});
test("doesn't change snakeCase stuff", () => {
const provider = new DepositProvider("test");
const req = {
type: "type",
asset_code: "assetCode",
dest: "dest",
dest_extra: "destExtra",
account: "account",
};
expect(provider.makeSnakeCase(req)).toEqual(req);
});
});

describe("fetchFinalFee", () => {
test("AnchorUSD", async () => {
const info: DepositInfo = {
Expand All @@ -24,10 +55,12 @@ describe("fetchFinalFee", () => {

const provider = new DepositProvider("test");

// manually set info
provider.info = { deposit: info, withdraw: {} };

expect(
await provider.fetchFinalFee({
supported_assets: info,
asset_code: info.USD.assetCode,
assetCode: info.USD.assetCode,
amount: "15",
type: "",
}),
Expand All @@ -53,10 +86,11 @@ describe("fetchFinalFee", () => {

const provider = new DepositProvider("test");

provider.info = { deposit: info, withdraw: {} };

expect(
await provider.fetchFinalFee({
supported_assets: info,
asset_code: info.EUR.assetCode,
assetCode: info.EUR.assetCode,
amount: "10",
type: "",
}),
Expand Down
20 changes: 15 additions & 5 deletions src/transfers/DepositProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,25 @@ import { TransferProvider } from "./TransferProvider";
*/
export class DepositProvider extends TransferProvider {
public async deposit(args: DepositRequest): Promise<TransferResponse> {
// warn about camel-cased props
if (args.assetCode && !args.asset_code) {
if (!this.info || !this.info.deposit) {
throw new Error("Run fetchSupportedAssets before running deposit!");
}

const search = queryString.stringify(this.makeSnakeCase(args));
const isAuthRequired = this.info.withdraw[args.assetCode]
.authenticationRequired;

if (isAuthRequired && !this.bearerToken) {
throw new Error(
"You provided `assetCode` instead of the correct `asset_code`",
`${
args.assetCode
} requires authentication, please authorize with setBearerToken.`,
);
}

const search = queryString.stringify(args);
const response = await fetch(`${this.transferServer}/deposit?${search}`);
const response = await fetch(`${this.transferServer}/deposit?${search}`, {
headers: isAuthRequired ? this.getHeaders() : undefined,
});
const json = (await response.json()) as TransferResponse;

if (json.error) {
Expand Down
46 changes: 40 additions & 6 deletions src/transfers/TransferProvider.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import changeCase from "change-case";
import queryString from "query-string";

import {
Expand All @@ -17,6 +18,8 @@ import { parseInfo } from "./parseInfo";
*/
export abstract class TransferProvider {
public transferServer: string;
public info?: Info;
public bearerToken?: string;

constructor(transferServer: string) {
this.transferServer = transferServer;
Expand All @@ -25,23 +28,52 @@ export abstract class TransferProvider {
protected async fetchInfo(): Promise<Info> {
const response = await fetch(`${this.transferServer}/info`);
const rawInfo = (await response.json()) as RawInfoResponse;
const info = parseInfo(rawInfo);
this.info = info;
return info;
}

protected getHeaders(): Headers {
return new Headers(
this.bearerToken
? {
Authorization: `Bearer ${this.bearerToken}`,
}
: {},
);
}

return parseInfo(rawInfo);
public makeSnakeCase(args: any) {
return Object.keys(args).reduce(
(memo: object, name: string) => ({
...memo,
[changeCase.snakeCase(name)]: args[name],
}),
{},
);
}

public setBearerToken(token: string) {
this.bearerToken = token;
}

public abstract fetchSupportedAssets():
| Promise<WithdrawInfo>
| Promise<DepositInfo>;

protected async fetchFinalFee(args: FeeArgs): Promise<number> {
const { supported_assets, ...rest } = args;
if (!this.info || !this.info[args.operation]) {
throw new Error("Run fetchSupportedAssets before running fetchFinalFee!");
}

const assetInfo = this.info[args.operation][args.assetCode];

if (!supported_assets[args.asset_code]) {
if (!assetInfo) {
throw new Error(
`Can't get fee for an unsupported asset, '${args.asset_code}`,
`Can't get fee for an unsupported asset, '${args.assetCode}`,
);
}
const { fee } = supported_assets[args.asset_code];
const { fee } = assetInfo;
switch (fee.type) {
case "none":
return 0;
Expand All @@ -53,7 +85,9 @@ export abstract class TransferProvider {
);
case "complex":
const response = await fetch(
`${this.transferServer}/fee?${queryString.stringify(rest)}`,
`${this.transferServer}/fee?${queryString.stringify(
this.makeSnakeCase(args as any),
)}`,
);
const { fee: feeResponse } = await response.json();
return feeResponse as number;
Expand Down
30 changes: 20 additions & 10 deletions src/transfers/WithdrawProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,22 +74,32 @@ import { TransferProvider } from "./TransferProvider";
* serverside/native apps, `getKycUrl`.
*/
export class WithdrawProvider extends TransferProvider {
/**
* Make a withdraw request.
*
* Note that all arguments must be in camelCase, even though they're sent to
* the server in snake_case!
*/
public async withdraw(args: WithdrawRequest): Promise<TransferResponse> {
// warn about camel-cased props
if (args.assetCode && !args.asset_code) {
throw new Error(
"You provided `assetCode` instead of the correct `asset_code`",
);
if (!this.info || !this.info.withdraw) {
throw new Error("Run fetchSupportedAssets before running withdraw!");
}
if (args.destExtra && !args.dest_extra) {

const search = queryString.stringify(this.makeSnakeCase(args));
const isAuthRequired = this.info.withdraw[args.assetCode]
.authenticationRequired;

if (isAuthRequired && !this.bearerToken) {
throw new Error(
"You provided `destExtra` instead of the correct `dest_extra`",
`${
args.assetCode
} requires authentication, please authorize with setBearerToken.`,
);
}

const search = queryString.stringify(args);

const response = await fetch(`${this.transferServer}/withdraw?${search}`);
const response = await fetch(`${this.transferServer}/withdraw?${search}`, {
headers: isAuthRequired ? this.getHeaders() : undefined,
});
const json = (await response.json()) as TransferResponse;

if (json.error) {
Expand Down
2 changes: 2 additions & 0 deletions src/transfers/parseInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export function parseWithdraw(info: RawInfoResponse): WithdrawInfo {
fee,
minAmount: entry.min_amount,
maxAmount: entry.max_amount,
authenticationRequired: !!entry.authentication_required,
types: Object.entries(entry.types || {}).map(parseType),
};
return accum;
Expand All @@ -100,6 +101,7 @@ export function parseDeposit(info: RawInfoResponse): DepositInfo {
fee,
minAmount: entry.min_amount,
maxAmount: entry.max_amount,
authenticationRequired: !!entry.authentication_required,
fields: Object.entries(entry.fields || {}).map(parseField),
};
return accum;
Expand Down
19 changes: 11 additions & 8 deletions src/types/transfers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ export interface GetKycArgs {
}

export interface FeeArgs {
supported_assets: WithdrawInfo | DepositInfo;
asset_code: string;
assetCode: string;
amount: string;
operation: "withdraw" | "deposit";
type: string;
Expand All @@ -22,6 +21,7 @@ export interface RawInfoResponse {
fee_percent: number;
min_amount: number;
max_amount: number;
authentication_required?: boolean;
types?: {
[type: string]: RawType;
};
Expand All @@ -34,6 +34,7 @@ export interface RawInfoResponse {
fee_percent: number;
min_amount: number;
max_amount: number;
authentication_required?: boolean;
fields?: {
[field: string]: RawField;
};
Expand Down Expand Up @@ -82,6 +83,7 @@ export interface WithdrawAssetInfo {
minAmount: number;
maxAmount: number;
types: WithdrawType[];
authenticationRequired?: boolean;
}

export interface WithdrawInfo {
Expand All @@ -94,6 +96,7 @@ export interface DepositAssetInfo {
minAmount: number;
maxAmount?: number;
fields: Field[];
authenticationRequired?: boolean;
}

export interface DepositInfo {
Expand All @@ -102,21 +105,21 @@ export interface DepositInfo {

export interface WithdrawRequest {
type: string;
asset_code: string;
assetCode: string;
dest: string;
dest_extra: string;
destExtra: string;
account?: string;
memo?: Memo;
memo_type?: string;
memoType?: string;
[key: string]: any;
}

export interface DepositRequest {
asset_code: string;
assetCode: string;
account: string;
memo?: Memo;
memo_type?: string;
email_address?: string;
memoType?: string;
emailAddress?: string;
type?: string;
[key: string]: any;
}
Expand Down
Loading

0 comments on commit 799638c

Please sign in to comment.