Skip to content

Commit

Permalink
add Customer / SEP-12 code (#71)
Browse files Browse the repository at this point in the history
* add type aliases

* make example use env vars

* finish tests

* add types

* cleanup

* comment update

* use getCustomer

* jsdoc

* for binary data

* clean up

* cleanup

* use sep9BinaryInfo

* cleanup
  • Loading branch information
acharb authored Oct 11, 2023
1 parent 56a95ea commit f0523ec
Show file tree
Hide file tree
Showing 8 changed files with 379 additions and 14 deletions.
31 changes: 19 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
# Stellar Typescript Wallet SDK [![npm version](https://badge.fury.io/js/@stellar%2Ftypescript-wallet-sdk.svg)](https://badge.fury.io/js/@stellar%2Ftypescript-wallet-sdk)
# Stellar Typescript Wallet SDK [![npm version](https://badge.fury.io/js/@stellar%2Ftypescript-wallet-sdk.svg)](https://badge.fury.io/js/@stellar%2Ftypescript-wallet-sdk)

Typescript Wallet SDK is a library that allows developers to build wallet applications on the Stellar network faster. It
utilizes [Javascript Stellar SDK](https://github.com/stellar/js-stellar-sdk) to communicate with a Stellar Horizon server.
It offers wide range of functionality to simplify integration with the Stellar network, and connect to the anchors easily, utilizing
various Stellar protocols (SEPs)
Typescript Wallet SDK is a library that allows developers to build wallet
applications on the Stellar network faster. It utilizes
[Javascript Stellar SDK](https://github.com/stellar/js-stellar-sdk) to
communicate with a Stellar Horizon server.
It offers wide range of functionality to simplify integration with the Stellar
network, and connect to the anchors easily, utilizing various Stellar protocols
(SEPs)

## Dependency

The library is available via npm.
To import `typescript-wallet-sdk` library you need to add it as a dependency to your code:
The library is available via npm. To import `typescript-wallet-sdk` library you
need to add it as a dependency to your code:

yarn:

Expand All @@ -24,21 +27,25 @@ npm install @stellar/typescript-wallet-sdk

## Introduction

Here's a small example creating main wallet class with default configuration connected to testnet network:
Here's a small example creating main wallet class with default configuration
connected to testnet network:

```typescript
let wallet = walletSdk.Wallet.TestNet();
```

It should later be re-used across the code, as it has access to various useful children classes. For example, you can
authenticate with the `testanchor` as simple as:
It should later be re-used across the code, as it has access to various useful
children classes. For example, you can authenticate with the `testanchor` as
simple as:

```typescript
const authKey = SigningKeypair.fromSecret("my secret key");
const anchor = wallet.anchor({ homeDomain: "testanchor.stellar.org" });
const sep10 = await anchor.sep10();

const authToken = await sep10.authenticate({accountKp: authKey});
const authToken = await sep10.authenticate({ accountKp: authKey });
```

Read [full wallet guide](https://developers.stellar.org/docs/category/build-a-wallet) for more info
Read
[full wallet guide](https://developers.stellar.org/docs/category/build-a-wallet-with-the-wallet-sdk)
for more info
32 changes: 31 additions & 1 deletion src/walletSdk/Anchor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { StellarTomlResolver } from "stellar-sdk";

import { Config } from "walletSdk";
import { Sep10 } from "../Auth";
import { ServerRequestFailedError } from "../Exceptions";
import { Sep12 } from "../Customer";
import {
ServerRequestFailedError,
KYCServerNotFoundError,
} from "../Exceptions";
import { Sep24 } from "./Sep24";
import { AnchorServiceInfo, TomlInfo } from "../Types";
import { parseToml } from "../Utils";
Expand All @@ -21,6 +25,8 @@ export type Interactive = Sep24;

export type Auth = Sep10;

export type Customer = Sep12;

// Do not create this object directly, use the Wallet class.
export class Anchor {
public language: string;
Expand Down Expand Up @@ -89,6 +95,30 @@ export class Anchor {
return this.sep10();
}

/**
* Create new customer object to handle customer records with the anchor using SEP-12.
* @param {string} authToken - The authentication token.
* @returns {Promise<Sep12>} - A Promise that resolves to a Sep12 instance.
* @throws {KYCServerNotFoundError} - If the KYC server information is not available.
*/
async sep12(authToken: string): Promise<Sep12> {
const tomlInfo = await this.sep1();
const kycServer = tomlInfo?.kycServer;
if (!kycServer) {
throw new KYCServerNotFoundError();
}
return new Sep12(authToken, kycServer, this.httpClient);
}

/**
* Create new customer object to handle customer records using the `sep12` method.
* @param {string} authToken - The authentication token.
* @returns {Promise<Customer>} - A Promise that resolves to a Customer instance.
*/
async customer(authToken: string): Promise<Customer> {
return this.sep12(authToken);
}

/**
* Creates new interactive flow for given anchor. It can be used for withdrawal or deposit.
* @returns {Sep24} - interactive flow service.
Expand Down
149 changes: 149 additions & 0 deletions src/walletSdk/Customer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { AxiosInstance } from "axios";
import queryString from "query-string";
import { Sep9InfoRequiredError, CustomerNotFoundError } from "../Exceptions";
import {
CustomerInfoMap,
Sep12Status,
Sep12Type,
Field,
ProvidedField,
GetCustomerParams,
GetCustomerResponse,
AddCustomerResponse,
AddCustomerParams,
} from "../Types";

export class Sep12 {
private token;
private baseUrl;
private httpClient;
private headers;

constructor(token: string, baseUrl: string, httpClient: AxiosInstance) {
this.token = token;
this.baseUrl = baseUrl;
this.httpClient = httpClient;
this.headers = {
"Content-Type": "application/json",
Authorization: `Bearer ${this.token}`,
};
}

/**
* Retrieve customer information. All arguments are optional, but at least one
* must be given. For more information:
* @see {@link https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0012.md#request}
* @param {object} params - The parameters for retrieving customer information.
* @param {string} [params.id] - The id of the customer .
* @param {string} [params.type] - The type of action the customer is being KYCd for.
* @see {@link https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0012.md#type-specification}
* @param {string} [params.memo] - A memo associated with the customer.
* @param {string} [params.lang] - The desired language. Defaults to "en".
* @return {Promise<GetCustomerResponse>} The customer information.
* @throws {CustomerNotFoundError} If the customer is not found.
*/
async getCustomer(params: GetCustomerParams): Promise<GetCustomerResponse> {
const qs = queryString.stringify(params);
const resp = await this.httpClient.get(`${this.baseUrl}/customer?${qs}`, {
headers: this.headers,
});
if (!resp.data.id) {
throw new CustomerNotFoundError(params);
}
return resp;
}

/**
* Add a new customer. Customer info is given in sep9Info param. If it
* is binary type (eg. Buffer of an image) include it in sep9BinaryInfo.
* @param {AddCustomerParams} params - The parameters for adding a customer.
* @param {CustomerInfoMap} params.sep9Info - Customer information. What fields you should
* give is indicated by the anchor.
* @param {CustomerInfoMap} params.sep9BinaryInfo - Customer information that is in binary
* format (eg. Buffer of an image).
* @param {string} [params.type] - The type of the customer.
* @param {string} [params.memo] - A memo associated with the customer.
* @return {Promise<AddCustomerResponse>} Add customer response.
*/
async add({
sep9Info,
sep9BinaryInfo,
type,
memo,
}: AddCustomerParams): Promise<AddCustomerResponse> {
let customerMap: CustomerInfoMap = { ...sep9Info, ...sep9BinaryInfo };
if (type) {
customerMap = { type, ...customerMap };
}

// Check if binary data given so can adjust headers
let includesBinary = sep9BinaryInfo && Object.keys(sep9BinaryInfo).length;
const resp = await this.httpClient.put(
`${this.baseUrl}/customer`,
customerMap,
{
headers: includesBinary
? { ...this.headers, "Content-Type": "multipart/form-data" }
: this.headers,
},
);
return resp;
}

/**
* Updates an existing customer. Customer info is given in sep9Info param. If it
* is binary type (eg. Buffer of an image) include it in sep9BinaryInfo.
* @param {AddCustomerParams} params - The parameters for adding a customer.
* @param {CustomerInfoMap} params.sep9Info - Customer information. What fields you should
* give is indicated by the anchor.
* @param {CustomerInfoMap} params.sep9BinaryInfo - Customer information that is in binary
* format (eg. Buffer of an image).
* @param {string} [params.id] - The id of the customer.
* @param {string} [params.type] - The type of the customer.
* @param {string} [params.memo] - A memo associated with the customer.
* @return {Promise<AddCustomerResponse>} Add customer response.
* @throws {Sep9InfoRequiredError} If no SEP-9 info is given.
*/
async update({
sep9Info,
sep9BinaryInfo,
id,
type,
memo,
}: AddCustomerParams): Promise<AddCustomerResponse> {
let customerMap: CustomerInfoMap = {};
if (id) {
customerMap["id"] = id;
}
if (type) {
customerMap["type"] = type;
}
if (memo) {
customerMap["memo"] = memo;
}
if (!Object.keys({ ...sep9Info, ...sep9BinaryInfo }).length) {
throw new Sep9InfoRequiredError();
}
customerMap = { ...customerMap, ...sep9Info, ...sep9BinaryInfo };

// Check if binary data given so can adjust headers
let includesBinary = sep9BinaryInfo && Object.keys(sep9BinaryInfo).length;
const resp = await this.httpClient.put(
`${this.baseUrl}/customer`,
customerMap,
{
headers: includesBinary
? { ...this.headers, "Content-Type": "multipart/form-data" }
: this.headers,
},
);
return resp;
}

async delete(accountAddress: string, memo?: string) {
await this.httpClient.delete(`${this.baseUrl}/customer/${accountAddress}`, {
data: { memo },
headers: this.headers,
});
}
}
23 changes: 22 additions & 1 deletion src/walletSdk/Exceptions/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Networks, Horizon } from "stellar-sdk";
import { AnchorTransaction, FLOW_TYPE } from "../Types";
import { AnchorTransaction, FLOW_TYPE, GetCustomerParams } from "../Types";

export class ServerRequestFailedError extends Error {
constructor(e: Error) {
Expand Down Expand Up @@ -161,6 +161,27 @@ export class WithdrawalTxMemoError extends Error {
}
}

export class Sep9InfoRequiredError extends Error {
constructor() {
super(`Sep-9 info required`);
Object.setPrototypeOf(this, Sep9InfoRequiredError.prototype);
}
}

export class CustomerNotFoundError extends Error {
constructor(params: GetCustomerParams) {
super(`Customer not found using params ${JSON.stringify(params)}`);
Object.setPrototypeOf(this, CustomerNotFoundError.prototype);
}
}

export class KYCServerNotFoundError extends Error {
constructor() {
super(`Required KYC server URL not found`);
Object.setPrototypeOf(this, KYCServerNotFoundError.prototype);
}
}

export class RecoveryServerNotFoundError extends Error {
constructor(serverKey: string) {
super(`Server with key ${serverKey} was not found`);
Expand Down
1 change: 1 addition & 0 deletions src/walletSdk/Types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export * from "./anchor";
export * from "./auth";
export * from "./horizon";
export * from "./recovery";
export * from "./sep12";
export * from "./sep24";
export * from "./utils";
export * from "./watcher";
61 changes: 61 additions & 0 deletions src/walletSdk/Types/sep12.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
export type CustomerInfoMap = {
[key: string]: string;
};

export enum Sep12Status {
ACCEPTED = "ACCEPTED",
PROCESSING = "PROCESSING",
NEEDS_INFO = "NEEDS_INFO",
REJECTED = "REJECTED",
VERIFICATION_REQUIRED = "VERIFICATION_REQUIRED",
}

export enum Sep12Type {
string = "string",
binary = "binary",
number = "number",
date = "date",
}

export type Field = {
type: Sep12Type;
description: string;
choices?: Array<string>;
optional?: boolean;
};

export type ProvidedField = {
type: Sep12Type;
description: string;
choices?: Array<string>;
optional?: boolean;
status?: Sep12Status;
error?: string;
};

export type GetCustomerParams = {
id?: string;
type?: string;
memo?: string;
lang?: string;
};

export type GetCustomerResponse = {
id?: string;
status: Sep12Status;
fields?: { [key: string]: Field };
provided_fields?: { [key: string]: ProvidedField };
message?: string;
};

export type AddCustomerParams = {
sep9Info?: CustomerInfoMap;
sep9BinaryInfo?: CustomerInfoMap;
id?: string;
memo?: string;
type?: string;
};

export type AddCustomerResponse = {
id: string;
};
Loading

0 comments on commit f0523ec

Please sign in to comment.