Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add polling to sep6 #79

Merged
merged 1 commit into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 26 additions & 58 deletions src/walletSdk/Anchor/Sep24.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { AxiosInstance } from "axios";
import queryString from "query-string";

import { Anchor } from "../Anchor";
import {
AssetNotSupportedError,
ServerRequestFailedError,
MissingTransactionIdError,
InvalidTransactionResponseError,
InvalidTransactionsResponseError,
} from "../Exceptions";
import {
FLOW_TYPE,
Expand All @@ -17,8 +14,13 @@ import {
GetTransactionParams,
GetTransactionsParams,
AnchorServiceInfo,
WatcherSepType,
} from "../Types";
import { Watcher } from "../Watcher";
import {
Watcher,
_getTransactionsForAsset,
_getTransactionBy,
} from "../Watcher";
import { camelToSnakeCaseObject } from "../Utils";

// Let's prevent exporting this constructor type as
Expand Down Expand Up @@ -182,7 +184,7 @@ export class Sep24 {
* @returns {Watcher} A new Watcher instance.
*/
watcher(): Watcher {
return new Watcher(this.anchor);
return new Watcher(this.anchor, WatcherSepType.SEP24);
}

/**
Expand Down Expand Up @@ -212,38 +214,20 @@ export class Sep24 {
const toml = await this.anchor.sep1();
const transferServerEndpoint = toml.transferServerSep24;

let qs: { [name: string]: string } = {};
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

basically just moving this code to a helper function so can be used for both seps


if (id) {
qs = { id };
} else if (stellarTransactionId) {
qs = { stellar_transaction_id: stellarTransactionId };
} else if (externalTransactionId) {
qs = { external_transaction_id: externalTransactionId };
}

qs = { lang, ...qs };

try {
const resp = await this.httpClient.get(
`${transferServerEndpoint}/transaction?${queryString.stringify(qs)}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
},
);

const transaction: AnchorTransaction = resp.data.transaction;

if (!transaction || Object.keys(transaction).length === 0) {
throw new InvalidTransactionResponseError(transaction);
}
// Let's convert all params to snake case for the API call
const apiParams = camelToSnakeCaseObject({
id,
stellarTransactionId,
externalTransactionId,
lang,
});

return transaction;
} catch (e) {
throw new ServerRequestFailedError(e);
}
return _getTransactionBy<AnchorTransaction>(
authToken,
apiParams,
transferServerEndpoint,
this.httpClient,
);
}

/**
Expand Down Expand Up @@ -282,27 +266,11 @@ export class Sep24 {
lang,
});

try {
const resp = await this.httpClient.get(
`${transferServerEndpoint}/transactions?${queryString.stringify(
apiParams,
)}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
},
);

const transactions: AnchorTransaction[] = resp.data.transactions;

if (!transactions || !Array.isArray(transactions)) {
throw new InvalidTransactionsResponseError(transactions);
}

return transactions;
} catch (e) {
throw new ServerRequestFailedError(e);
}
return _getTransactionsForAsset<AnchorTransaction>(
authToken,
apiParams,
transferServerEndpoint,
this.httpClient,
);
}
}
115 changes: 114 additions & 1 deletion src/walletSdk/Anchor/Sep6.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { AxiosInstance } from "axios";
import queryString from "query-string";

import { Anchor } from "../Anchor";
import { ServerRequestFailedError } from "../Exceptions";
import {
ServerRequestFailedError,
MissingTransactionIdError,
} from "../Exceptions";
import {
Sep6Info,
Sep6Params,
Expand All @@ -11,7 +14,17 @@ import {
Sep6DepositResponse,
Sep6WithdrawResponse,
Sep6ExchangeParams,
Sep6Transaction,
GetTransactionParams,
GetTransactionsParams,
WatcherSepType,
} from "../Types";
import {
Watcher,
_getTransactionsForAsset,
_getTransactionBy,
} from "../Watcher";
import { camelToSnakeCaseObject } from "../Utils";

/**
* Flow for creating deposits and withdrawals with an anchor using SEP-6.
Expand Down Expand Up @@ -177,4 +190,104 @@ export class Sep6 {
throw e;
}
}

/**
* Creates a new instance of the Watcher class, to watch sep6 transactions.
* @returns {Watcher} A new Watcher instance.
*/
watcher(): Watcher {
return new Watcher(this.anchor, WatcherSepType.SEP6);
}

/**
* Get account's sep6 transactions specified by asset and other params.
* @param {GetTransactionsParams} params - The Get Transactions params.
* @param {AuthToken} params.authToken - The authentication token for the account authenticated with the anchor.
* @param {string} params.assetCode - The target asset to query for.
* @param {string} params.account - The stellar account public key involved in the transactions. If the service requires SEP-10
* authentication, this parameter must match the authenticated account.
* @param {string} [params.noOlderThan] - The response should contain transactions starting on or after this date & time.
* @param {string} [params.limit] - The response should contain at most 'limit' transactions.
* @param {string} [params.kind] - The kind of transaction that is desired. E.g.: 'deposit', 'withdrawal', 'depo
* -exchange', 'withdrawal-exchange'.
* @param {string} [params.pagingId] - The response should contain transactions starting prior to this ID (exclusive).
* @param {string} [params.lang] - The desired language (localization), it can also accept locale in the format 'en-US'.
* @returns {Promise<Sep6Transaction[]>} A list of transactions as requested by the client, sorted in time-descending order.
* @throws {InvalidTransactionsResponseError} Anchor returns an invalid response.
* @throws {ServerRequestFailedError} If server request fails.
*/
async getTransactionsForAsset({
authToken,
assetCode,
account,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need an account here? Can't we get it from the auth token? I think in real implementations there are no use cases where account != SEP-10 account

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sep 6 requires it. If you don't give an account that matches the auth token account it gives a 403.

I'm not with my laptop right now but I can link to it on the sep 6 spec tonight

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm suggesting to omit it in the method parameter, and instead always pass account from the auth token to the anchor. (that's how anchor platform and polaris work, as they require authentication in /transaction endpoint, therefore account must always match authenticated account)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahh I see. Ya I agree that's better. We have this ticket for tying the account to the auth token (right now it's just a string) https://stellarorg.atlassian.net/browse/WAL-1152?atlOrigin=eyJpIjoiYjMxYWY2NDRkY2IyNGE4MGE4MjlkNWM3NDFmNzVlMDQiLCJwIjoiaiJ9

I'll make a note on the ticket to change this as well to always pull from the auth token

noOlderThan,
limit,
kind,
pagingId,
lang = this.anchor.language,
}: GetTransactionsParams & { account: string }): Promise<Sep6Transaction[]> {
const toml = await this.anchor.sep1();
const transferServerEndpoint = toml.transferServer;

// Let's convert all params to snake case for the API call
const apiParams = camelToSnakeCaseObject({
assetCode,
account,
noOlderThan,
limit,
kind,
pagingId,
lang,
});

return _getTransactionsForAsset<Sep6Transaction>(
authToken,
apiParams,
transferServerEndpoint,
this.httpClient,
);
}

/**
* Get single sep6 transaction's current status and details from the anchor.
* @param {GetTransactionParams} params - The Get Transactions params.
* @param {AuthToken} params.authToken - The authentication token for the account authenticated with the anchor.
* @param {string} [params.id] - The transaction ID.
* @param {string} [params.stellarTransactionId] - The Stellar transaction ID.
* @param {string} [params.externalTransactionId] - The external transaction ID.
* @param {string} [params.lang] - The language setting.
* @returns {Promise<Sep6Transaction>} The transaction object.
* @throws {MissingTransactionIdError} If none of the ID parameters is provided.
* @throws {InvalidTransactionResponseError} If the anchor returns an invalid transaction response.
* @throws {ServerRequestFailedError} If the server request fails.
*/
async getTransactionBy({
authToken,
id,
stellarTransactionId,
externalTransactionId,
lang = this.anchor.language,
}: GetTransactionParams): Promise<Sep6Transaction> {
if (!id && !stellarTransactionId && !externalTransactionId) {
throw new MissingTransactionIdError();
}

const toml = await this.anchor.sep1();
const transferServerEndpoint = toml.transferServer;

// Let's convert all params to snake case for the API call
const apiParams = camelToSnakeCaseObject({
id,
stellarTransactionId,
externalTransactionId,
lang,
});

return _getTransactionBy<Sep6Transaction>(
authToken,
apiParams,
transferServerEndpoint,
this.httpClient,
);
}
}
18 changes: 18 additions & 0 deletions src/walletSdk/Types/anchor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export interface ProcessingAnchorTransaction extends BaseTransaction {
amount_out_asset?: string;
amount_out: string;
amount_fee_asset?: string;
quote_id?: string;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note sure why this was left off (I may have just missed it before)

amount_fee: string;
completed_at?: string;
stellar_transaction_id?: string;
Expand All @@ -60,6 +61,23 @@ export interface WithdrawTransaction extends ProcessingAnchorTransaction {
withdraw_anchor_account: string;
}

export type Sep6Transaction = DepositTransaction &
WithdrawTransaction & {
from?: string;
external_extra?: string;
external_extra_text?: string;
required_info_message?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
required_info_updates?: any;
required_customer_info_message?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
required_customer_info_updates?: any;
instructions?: {
value: string;
description: string;
};
};

// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface ErrorTransaction
extends Optional<DepositTransaction & WithdrawTransaction> {}
Expand Down
6 changes: 6 additions & 0 deletions src/walletSdk/Types/watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type WatchTransactionsParams = {
assetCode: string;
onMessage: (transaction: AnchorTransaction) => void;
onError: (error: AnchorTransaction | Error) => void;
account?: string;
watchlist?: string[];
timeout?: number;
isRetry?: boolean;
Expand Down Expand Up @@ -32,3 +33,8 @@ export interface WatcherResponse {
refresh: WatcherRefreshFunction;
stop: WatcherStopFunction;
}

export enum WatcherSepType {
SEP6 = "SEP6",
SEP24 = "SEP24",
}
64 changes: 64 additions & 0 deletions src/walletSdk/Watcher/getTransactions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { AxiosInstance } from "axios";
import queryString from "query-string";

import {
ServerRequestFailedError,
InvalidTransactionsResponseError,
InvalidTransactionResponseError,
} from "../Exceptions";

export const _getTransactionsForAsset = async <T>(
authToken: string,
params: { [key: string]: string | number },
endpoint: string,
client: AxiosInstance,
): Promise<T[]> => {
try {
const resp = await client.get(
`${endpoint}/transactions?${queryString.stringify(params)}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
},
);

const transactions: T[] = resp.data.transactions;

if (!transactions || !Array.isArray(transactions)) {
throw new InvalidTransactionsResponseError(transactions);
}

return transactions;
} catch (e) {
throw new ServerRequestFailedError(e);
}
};

export const _getTransactionBy = async <T>(
authToken: string,
params: { [key: string]: string | number },
endpoint: string,
client: AxiosInstance,
): Promise<T> => {
try {
const resp = await client.get(
`${endpoint}/transaction?${queryString.stringify(params)}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
},
);

const transaction: T = resp.data.transaction;

if (!transaction || Object.keys(transaction).length === 0) {
throw new InvalidTransactionResponseError(transaction);
}

return transaction;
} catch (e) {
throw new ServerRequestFailedError(e);
}
};
Loading
Loading