From 9920176fff4c649f388948058ce33e47eabff66b Mon Sep 17 00:00:00 2001 From: Alec Charbonneau Date: Mon, 30 Oct 2023 17:54:29 -0400 Subject: [PATCH 1/4] small fixes to Recovery code (#75) * small fixes * fix path payment test --- src/walletSdk/Recovery/index.ts | 11 +++++++---- test/transaction.test.ts | 10 +++------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/walletSdk/Recovery/index.ts b/src/walletSdk/Recovery/index.ts index 2bcd94f..7d7e325 100644 --- a/src/walletSdk/Recovery/index.ts +++ b/src/walletSdk/Recovery/index.ts @@ -33,6 +33,7 @@ import { SponsoringBuilder, Stellar, } from "../Horizon"; +import { camelToSnakeCaseObject } from "../Utils"; // Let's prevent exporting this constructor type as // we should not create this Recovery class directly. @@ -261,13 +262,13 @@ export class Recovery extends AccountRecover { Object.keys(this.servers).map(async (key) => { const server = this.servers[key]; - const accountIdentity = identityMap[key]; + const accountIdentities = identityMap[key]; - if (!accountIdentity) { + if (!accountIdentities) { throw new RecoveryIdentityNotFoundError(key); } - const authToken = this.sep10Auth(key).authenticate({ + const authToken = await this.sep10Auth(key).authenticate({ accountKp: account, walletSigner: server.walletSigner, clientDomain: server.clientDomain, @@ -280,7 +281,9 @@ export class Recovery extends AccountRecover { const resp = await this.httpClient.post( requestUrl, { - identities: accountIdentity, + identities: accountIdentities.map((ai) => + camelToSnakeCaseObject(ai), + ), }, { headers: { diff --git a/test/transaction.test.ts b/test/transaction.test.ts index caa760b..c02b9ce 100644 --- a/test/transaction.test.ts +++ b/test/transaction.test.ts @@ -291,9 +291,7 @@ describe("Path Payment", () => { sendAmount: "5", }) .build(); - sourceKp.sign(txn); - const success = await stellar.submitTransaction(txn); - expect(success).toBe(true); + expect(txn.operations[0].type).toBe("pathPaymentStrictSend"); }, 15000); it("should use path payment receive", async () => { @@ -308,16 +306,14 @@ describe("Path Payment", () => { destAmount: "5", }) .build(); - sourceKp.sign(txn); - const success = await stellar.submitTransaction(txn); - expect(success).toBe(true); + expect(txn.operations[0].type).toBe("pathPaymentStrictReceive"); }, 15000); it("should swap", async () => { const txBuilder = await stellar.transaction({ sourceAddress: sourceKp, }); - const txn = txBuilder.swap(new NativeAssetId(), usdcAsset, "1").build(); + const txn = txBuilder.swap(new NativeAssetId(), usdcAsset, ".1").build(); sourceKp.sign(txn); const success = await stellar.submitTransaction(txn); expect(success).toBe(true); From 016314dc8b6cd9bdc20ace8ba471ca7c590b4424 Mon Sep 17 00:00:00 2001 From: Alec Charbonneau Date: Tue, 31 Oct 2023 17:29:55 -0400 Subject: [PATCH 2/4] add sep6 get info (#76) --- src/walletSdk/Anchor/Sep6.ts | 56 +++++++++++++++++++++++++++++++++ src/walletSdk/Anchor/index.ts | 19 ++++++++++++ src/walletSdk/Types/index.ts | 1 + src/walletSdk/Types/sep6.ts | 58 +++++++++++++++++++++++++++++++++++ test/sep6.test.ts | 16 ++++++++++ 5 files changed, 150 insertions(+) create mode 100644 src/walletSdk/Anchor/Sep6.ts create mode 100644 src/walletSdk/Types/sep6.ts create mode 100644 test/sep6.test.ts diff --git a/src/walletSdk/Anchor/Sep6.ts b/src/walletSdk/Anchor/Sep6.ts new file mode 100644 index 0000000..bb4e9f3 --- /dev/null +++ b/src/walletSdk/Anchor/Sep6.ts @@ -0,0 +1,56 @@ +import { AxiosInstance } from "axios"; + +import { Anchor } from "../Anchor"; +import { ServerRequestFailedError } from "../Exceptions"; +import { Sep6Info } from "../Types"; + +type Sep6Params = { + anchor: Anchor; + httpClient: AxiosInstance; +}; + +/** + * Flow for creating deposits and withdrawals with an anchor using SEP-6. + * For an interactive flow use Sep24 instead. + * @see {@link https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0006.md} + * Do not create this object directly, use the Anchor class. + * @class + */ +export class Sep6 { + private anchor: Anchor; + private httpClient: AxiosInstance; + private anchorInfo: Sep6Info; + + /** + * Creates a new instance of the Sep6 class. + * @constructor + * @param {Sep6Params} params - Parameters to initialize the Sep6 instance. + */ + constructor(params: Sep6Params) { + const { anchor, httpClient } = params; + + this.anchor = anchor; + this.httpClient = httpClient; + } + + /** + * Get SEP-6 anchor information. + * If `shouldRefresh` is set to `true`, it fetches fresh values; otherwise, it returns cached values if available. + * @param {boolean} [shouldRefresh=false] - Flag to force a refresh of TOML values. + * @returns {Promise} - SEP-6 information about the anchor. + */ + async info(shouldRefresh?: boolean): Promise { + if (this.anchorInfo && !shouldRefresh) { + return this.anchorInfo; + } + + const { transferServer } = await this.anchor.sep1(); + try { + const resp = await this.httpClient.get(`${transferServer}/info`); + this.anchorInfo = resp.data; + return resp.data; + } catch (e) { + throw new ServerRequestFailedError(e); + } + } +} diff --git a/src/walletSdk/Anchor/index.ts b/src/walletSdk/Anchor/index.ts index 1699d86..4611e22 100644 --- a/src/walletSdk/Anchor/index.ts +++ b/src/walletSdk/Anchor/index.ts @@ -8,6 +8,7 @@ import { ServerRequestFailedError, KYCServerNotFoundError, } from "../Exceptions"; +import { Sep6 } from "./Sep6"; import { Sep24 } from "./Sep24"; import { AnchorServiceInfo, TomlInfo } from "../Types"; import { parseToml } from "../Utils"; @@ -21,6 +22,8 @@ type AnchorParams = { language: string; }; +export type Transfer = Sep6; + export type Interactive = Sep24; export type Auth = Sep10; @@ -82,6 +85,22 @@ export class Anchor { return this.sep1(shouldRefresh); } + /** + * Creates new transfer flow for given anchor. It can be used for withdrawal or deposit. + * @returns {Sep6} - flow service. + */ + sep6(): Sep6 { + return new Sep6({ anchor: this, httpClient: this.httpClient }); + } + + /** + * Creates new transfer flow using the `sep6` method. + * @returns {Transfer} - transfer flow service. + */ + transfer(): Transfer { + return this.sep6(); + } + /** * Create new auth object to authenticate account with the anchor using SEP-10. * @returns {Promise} - The SEP-10 authentication manager. diff --git a/src/walletSdk/Types/index.ts b/src/walletSdk/Types/index.ts index 8999a46..f7b998f 100644 --- a/src/walletSdk/Types/index.ts +++ b/src/walletSdk/Types/index.ts @@ -36,6 +36,7 @@ export * from "./anchor"; export * from "./auth"; export * from "./horizon"; export * from "./recovery"; +export * from "./sep6"; export * from "./sep12"; export * from "./sep24"; export * from "./utils"; diff --git a/src/walletSdk/Types/sep6.ts b/src/walletSdk/Types/sep6.ts new file mode 100644 index 0000000..3b79296 --- /dev/null +++ b/src/walletSdk/Types/sep6.ts @@ -0,0 +1,58 @@ +export interface Sep6EndpointInfo { + enabled: boolean; + authentication_required?: boolean; + description?: string; +} + +export interface Sep6DepositInfo { + enabled: boolean; + authentication_required?: boolean; + fee_fixed?: number; + fee_percent?: number; + min_amount?: number; + max_amount?: number; + fields?: { + [key: string]: { + description: string; + optional?: boolean; + choices?: string[]; + }; + }; +} + +export interface Sep6WithdrawInfo { + enabled: boolean; + authentication_required?: boolean; + fee_fixed?: number; + fee_percent?: number; + min_amount?: number; + max_amount?: number; + types?: { + [key: string]: { + fields?: { + [key: string]: { + description: string; + optional?: boolean; + choices?: string[]; + }; + }; + }; + }; +} + +export interface Sep6Info { + deposit: { [key: string]: Sep6DepositInfo }; + "deposit-exchange": { [key: string]: Sep6DepositInfo }; + withdraw: { [key: string]: Sep6WithdrawInfo }; + "withdraw-exchange": { [key: string]: Sep6WithdrawInfo }; + fee: { + enabled: boolean; + description: string; + }; + transactions: Sep6EndpointInfo; + transaction: Sep6EndpointInfo; + features: { + account_creation: boolean; + claimable_balances: boolean; + }; +} diff --git a/test/sep6.test.ts b/test/sep6.test.ts new file mode 100644 index 0000000..bdd3cfa --- /dev/null +++ b/test/sep6.test.ts @@ -0,0 +1,16 @@ +import { Wallet } from "../src"; + +describe("SEP-6", () => { + it("should get anchor info", async () => { + const wallet = Wallet.TestNet(); + const anchor = wallet.anchor({ homeDomain: "testanchor.stellar.org" }); + const sep6 = anchor.sep6(); + const resp = await sep6.info(); + expect(resp.deposit).toBeTruthy(); + expect(resp.withdraw).toBeTruthy(); + + const refreshed = await sep6.info(true); + expect(refreshed.deposit).toBeTruthy(); + expect(refreshed.withdraw).toBeTruthy(); + }); +}); From f56265a7eaffe975dd07a67399bb26a8b312a48a Mon Sep 17 00:00:00 2001 From: Alec Charbonneau Date: Wed, 8 Nov 2023 14:06:40 -0500 Subject: [PATCH 3/4] add SEP6 deposit and withdrawal (#77) * add sep6 deposit and withdrawal * comment * make better response * remove wrapper type --- src/walletSdk/Anchor/Sep6.ts | 89 ++++++++++++++++++++++++++++++++--- src/walletSdk/Types/sep6.ts | 91 ++++++++++++++++++++++++++++++++++++ test/sep6.test.ts | 86 ++++++++++++++++++++++++++++++++-- 3 files changed, 257 insertions(+), 9 deletions(-) diff --git a/src/walletSdk/Anchor/Sep6.ts b/src/walletSdk/Anchor/Sep6.ts index bb4e9f3..8660476 100644 --- a/src/walletSdk/Anchor/Sep6.ts +++ b/src/walletSdk/Anchor/Sep6.ts @@ -1,13 +1,16 @@ import { AxiosInstance } from "axios"; +import queryString from "query-string"; import { Anchor } from "../Anchor"; import { ServerRequestFailedError } from "../Exceptions"; -import { Sep6Info } from "../Types"; - -type Sep6Params = { - anchor: Anchor; - httpClient: AxiosInstance; -}; +import { + Sep6Info, + Sep6Params, + Sep6DepositParams, + Sep6WithdrawParams, + Sep6DepositResponse, + Sep6WithdrawResponse, +} from "../Types"; /** * Flow for creating deposits and withdrawals with an anchor using SEP-6. @@ -53,4 +56,78 @@ export class Sep6 { throw new ServerRequestFailedError(e); } } + + /** + * Deposits funds using the SEP-6 protocol. Next steps by + * the anchor are given in the response. + * + * @param {object} options - The options for the deposit. + * @param {string} options.authToken - The authentication token. + * @param {Sep6DepositParams} options.params - The parameters for the deposit request. + * + * @returns {Promise} Sep6 deposit response, containing next steps if needed + * to complete the deposit. + * + * @throws {Error} If an unexpected error occurs during the deposit operation. + */ + async deposit({ + authToken, + params, + }: { + authToken: string; + params: Sep6DepositParams; + }): Promise { + return this.flow({ type: "deposit", authToken, params }); + } + + /** + * Initiates a withdrawal using SEP-6. + * + * @param {object} options - The options for the withdrawal operation. + * @param {string} options.authToken - The authentication token. + * @param {Sep6WithdrawParams} options.params - The parameters for the withdrawal request. + * + * @returns {Promise} Sep6 withdraw response. + */ + async withdraw({ + authToken, + params, + }: { + authToken: string; + params: Sep6WithdrawParams; + }): Promise { + return this.flow({ type: "withdraw", authToken, params }); + } + + private async flow({ + type, + authToken, + params, + }: { + type: "deposit" | "withdraw"; + authToken: string; + params: Sep6DepositParams | Sep6WithdrawParams; + }) { + const { transferServer } = await this.anchor.sep1(); + + try { + const resp = await this.httpClient.get( + `${transferServer}/${type}?${queryString.stringify(params)}`, + { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${authToken}`, + }, + }, + ); + return resp.data; + } catch (e) { + if (e.response?.data?.type === "non_interactive_customer_info_needed") { + return e.response?.data; + } else if (e.response?.data?.type === "customer_info_status") { + return e.response?.data; + } + throw e; + } + } } diff --git a/src/walletSdk/Types/sep6.ts b/src/walletSdk/Types/sep6.ts index 3b79296..88deaec 100644 --- a/src/walletSdk/Types/sep6.ts +++ b/src/walletSdk/Types/sep6.ts @@ -1,3 +1,6 @@ +import { AxiosInstance } from "axios"; +import { Anchor } from "../Anchor"; + export interface Sep6EndpointInfo { enabled: boolean; authentication_required?: boolean; @@ -56,3 +59,91 @@ export interface Sep6Info { claimable_balances: boolean; }; } + +export type Sep6Params = { + anchor: Anchor; + httpClient: AxiosInstance; +}; + +export interface Sep6DepositParams { + asset_code: string; + account: string; + memo_type?: string; + memo?: string; + email_address?: string; + type?: string; + lang?: string; + on_change_callback?: string; + amount?: string; + country_code?: string; + claimable_balance_supported?: string; + customer_id?: string; +} + +export interface Sep6WithdrawParams { + asset_code: string; + type: string; + dest?: string; + dest_extra?: string; + account?: string; + memo?: string; + lang?: string; + on_change_callback?: string; + amount?: string; + country_code?: string; + refund_memo?: string; + refund_memo_type?: string; + customer_id?: string; +} + +export type Sep6DepositResponse = + | Sep6DepositSuccess + | Sep6MissingKYC + | Sep6Pending; + +export interface Sep6DepositSuccess { + how?: string; + instructions?: { + [key: string]: { + value: string; + description: string; + }; + }; + id?: string; + eta?: number; + min_amoun?: number; + max_amount?: number; + fee_fixed?: number; + fee_percent?: number; + extra_info?: { message?: string }; +} + +export interface Sep6MissingKYC { + type: string; + fields: Array; +} + +export interface Sep6Pending { + type: string; + status: string; + more_info_url?: string; + eta?: number; +} + +export type Sep6WithdrawResponse = + | Sep6WithdrawSuccess + | Sep6MissingKYC + | Sep6Pending; + +export interface Sep6WithdrawSuccess { + account_id?: string; + memo_type?: string; + memo?: string; + id?: string; + eta?: number; + min_amount?: number; + max_amount?: number; + fee_fixed?: number; + fee_percent?: number; + extra_info?: { message?: string }; +} diff --git a/test/sep6.test.ts b/test/sep6.test.ts index bdd3cfa..923a8e7 100644 --- a/test/sep6.test.ts +++ b/test/sep6.test.ts @@ -1,10 +1,23 @@ import { Wallet } from "../src"; +import axios from "axios"; + +let wallet; +let anchor; +let sep6; +let accountKp; describe("SEP-6", () => { + beforeAll(async () => { + wallet = Wallet.TestNet(); + anchor = wallet.anchor({ homeDomain: "testanchor.stellar.org" }); + sep6 = anchor.sep6(); + + accountKp = wallet.stellar().account().createKeypair(); + await axios.get( + "https://friendbot.stellar.org/?addr=" + accountKp.publicKey, + ); + }, 10000); it("should get anchor info", async () => { - const wallet = Wallet.TestNet(); - const anchor = wallet.anchor({ homeDomain: "testanchor.stellar.org" }); - const sep6 = anchor.sep6(); const resp = await sep6.info(); expect(resp.deposit).toBeTruthy(); expect(resp.withdraw).toBeTruthy(); @@ -13,4 +26,71 @@ describe("SEP-6", () => { expect(refreshed.deposit).toBeTruthy(); expect(refreshed.withdraw).toBeTruthy(); }); + it("should deposit", async () => { + const auth = await anchor.sep10(); + const authToken = await auth.authenticate({ accountKp }); + + const sep12 = await anchor.sep12(authToken); + + // Make first call with missing KYC info + let resp = await sep6.deposit({ + authToken, + params: { + asset_code: "SRT", + account: accountKp.publicKey, + type: "bank_account", + }, + }); + expect(resp.type).toBe("non_interactive_customer_info_needed"); + + // Add the missing KYC info + await sep12.add({ + sep9Info: { + first_name: "john", + last_name: "smith", + email_address: "123@gmail.com", + bank_number: "12345", + bank_account_number: "12345", + }, + }); + + // Make deposit call again with all info uploaded + resp = await sep6.deposit({ + authToken, + params: { + asset_code: "SRT", + account: accountKp.publicKey, + type: "bank_account", + }, + }); + expect(resp.id).toBeTruthy(); + }); + it("should withdraw", async () => { + const auth = await anchor.sep10(); + const authToken = await auth.authenticate({ accountKp }); + + const sep12 = await anchor.sep12(authToken); + + await sep12.add({ + sep9Info: { + first_name: "john", + last_name: "smith", + email_address: "123@gmail.com", + bank_number: "12345", + bank_account_number: "12345", + }, + }); + + const resp = await sep6.withdraw({ + authToken, + params: { + asset_code: "SRT", + account: accountKp.publicKey, + type: "bank_account", + dest: "123", + dest_extra: "12345", + }, + }); + expect(resp.id).toBeTruthy(); + }); }); From db60e083d75d79c26ad05eb408d73fe9e1a7a278 Mon Sep 17 00:00:00 2001 From: Alec Charbonneau Date: Tue, 21 Nov 2023 12:15:33 -0600 Subject: [PATCH 4/4] add exchange endpoints (#78) --- src/walletSdk/Anchor/Sep6.ts | 51 ++++++++++++++++++++++++++++++-- src/walletSdk/Types/sep6.ts | 19 ++++++++++++ test/sep6.test.ts | 57 ++++++++++++++++++++++++++++++++++++ 3 files changed, 125 insertions(+), 2 deletions(-) diff --git a/src/walletSdk/Anchor/Sep6.ts b/src/walletSdk/Anchor/Sep6.ts index 8660476..40dd685 100644 --- a/src/walletSdk/Anchor/Sep6.ts +++ b/src/walletSdk/Anchor/Sep6.ts @@ -10,6 +10,7 @@ import { Sep6WithdrawParams, Sep6DepositResponse, Sep6WithdrawResponse, + Sep6ExchangeParams, } from "../Types"; /** @@ -99,14 +100,60 @@ export class Sep6 { return this.flow({ type: "withdraw", authToken, params }); } + /** + * Similar to the SEP-6 deposit function, but for non-equivalent assets + * that require an exchange. + * + * @param {object} options - The options for the deposit exchange. + * @param {string} options.authToken - The authentication token. + * @param {Sep6ExchangeParams} options.params - The parameters for the deposit request. + * + * @returns {Promise} Sep6 deposit response, containing next steps if needed + * to complete the deposit. + * + * @throws {Error} If an unexpected error occurs during the deposit operation. + */ + async depositExchange({ + authToken, + params, + }: { + authToken: string; + params: Sep6ExchangeParams; + }): Promise { + return this.flow({ type: "deposit-exchange", authToken, params }); + } + + /** + * Similar to the SEP-6 withdraw function, but for non-equivalent assets + * that require an exchange. + * + * @param {object} options - The options for the deposit exchange. + * @param {string} options.authToken - The authentication token. + * @param {Sep6ExchangeParams} options.params - The parameters for the deposit request. + * + * @returns {Promise} Sep6 withdraw response, containing next steps if needed + * to complete the withdrawal. + * + * @throws {Error} If an unexpected error occurs during the deposit operation. + */ + async withdrawExchange({ + authToken, + params, + }: { + authToken: string; + params: Sep6ExchangeParams; + }): Promise { + return this.flow({ type: "withdraw-exchange", authToken, params }); + } + private async flow({ type, authToken, params, }: { - type: "deposit" | "withdraw"; + type: "deposit" | "withdraw" | "deposit-exchange" | "withdraw-exchange"; authToken: string; - params: Sep6DepositParams | Sep6WithdrawParams; + params: Sep6DepositParams | Sep6WithdrawParams | Sep6ExchangeParams; }) { const { transferServer } = await this.anchor.sep1(); diff --git a/src/walletSdk/Types/sep6.ts b/src/walletSdk/Types/sep6.ts index 88deaec..6e8bc02 100644 --- a/src/walletSdk/Types/sep6.ts +++ b/src/walletSdk/Types/sep6.ts @@ -147,3 +147,22 @@ export interface Sep6WithdrawSuccess { fee_percent?: number; extra_info?: { message?: string }; } + +export interface Sep6ExchangeParams { + destination_asset: string; + source_asset: string; + amount: string; + account?: string; + quote_id?: string; + memo_type?: string; + memo?: string; + email_address?: string; + type?: string; + lang?: string; + on_change_callback?: string; + country_code?: string; + claimable_balance_supported?: string; + customer_id?: string; + refund_memo?: string; + refund_memo_type?: string; +} diff --git a/test/sep6.test.ts b/test/sep6.test.ts index 923a8e7..cda0516 100644 --- a/test/sep6.test.ts +++ b/test/sep6.test.ts @@ -93,4 +93,61 @@ describe("SEP-6", () => { }); expect(resp.id).toBeTruthy(); }); + + it("deposit-exchange should work", async () => { + const auth = await anchor.sep10(); + const authToken = await auth.authenticate({ accountKp }); + + const sep12 = await anchor.sep12(authToken); + await sep12.add({ + sep9Info: { + first_name: "john", + last_name: "smith", + email_address: "123@gmail.com", + bank_number: "12345", + bank_account_number: "12345", + }, + }); + + const params = { + destination_asset: + "stellar:SRT:GCDNJUBQSX7AJWLJACMJ7I4BC3Z47BQUTMHEICZLE6MU4KQBRYG5JY6B", + source_asset: "iso4217:USD", + amount: "1", + account: accountKp.publicKey, + type: "bank_account", + }; + + const resp = await sep6.depositExchange({ authToken, params }); + expect(resp.id).toBeTruthy(); + }); + + it("withdraw-exchange should work", async () => { + const auth = await anchor.sep10(); + const authToken = await auth.authenticate({ accountKp }); + + const sep12 = await anchor.sep12(authToken); + await sep12.add({ + sep9Info: { + first_name: "john", + last_name: "smith", + email_address: "123@gmail.com", + bank_number: "12345", + bank_account_number: "12345", + }, + }); + + const params = { + destination_asset: "iso4217:USD", + source_asset: + "stellar:SRT:GCDNJUBQSX7AJWLJACMJ7I4BC3Z47BQUTMHEICZLE6MU4KQBRYG5JY6B", + amount: "1", + dest: accountKp.publicKey, + dest_extra: "1234", + type: "bank_account", + }; + + const resp = await sep6.withdrawExchange({ authToken, params }); + expect(resp.id).toBeTruthy(); + }); });