diff --git a/package.json b/package.json index 116d725..32d0a5a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@blindpay/node", - "version": "3.2.0", + "version": "4.0.0", "description": "Official Node.js SDK for Blindpay API - Stablecoin API for global payments", "keywords": [ "blindpay", diff --git a/src/client.ts b/src/client.ts index 8562e7a..d1e854d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -6,6 +6,7 @@ import { BlindPayError } from "./internal/blindpay-error"; import { createApiKeysResource } from "./resources/api-keys"; import { createAvailableResource } from "./resources/available"; import { createBankAccountsResource } from "./resources/bank-accounts"; +import { createFeesResource } from "./resources/fees"; import { createInstancesResource } from "./resources/instances"; import { createPartnerFeesResource } from "./resources/partner-fees"; import { createPayinsResource } from "./resources/payins"; @@ -14,8 +15,11 @@ import { createPayoutsResource } from "./resources/payouts"; import { createQuotesResource } from "./resources/quotes"; import { createReceiversResource } from "./resources/receivers"; import { createTermsOfServiceResource } from "./resources/terms-of-service"; +import { createTransfersResource } from "./resources/transfers"; +import { createUploadResource } from "./resources/upload"; import { createVirtualAccountsResource } from "./resources/virtual-accounts"; import { createBlockchainWalletsResource } from "./resources/wallets/blockchain"; +import { createCustodialWalletsResource } from "./resources/wallets/custodial"; import { createOfframpWalletsResource } from "./resources/wallets/offramp"; import { createWebhookEndpointsResource } from "./resources/webhooks"; @@ -35,6 +39,9 @@ export class BlindPay { }; readonly quotes: ReturnType; readonly payouts: ReturnType; + readonly transfers: ReturnType; + readonly fees: ReturnType; + readonly upload: ReturnType; readonly virtualAccounts: ReturnType; readonly receivers: ReturnType & { bankAccounts: ReturnType; @@ -47,6 +54,7 @@ export class BlindPay { readonly wallets: { blockchain: ReturnType; offramp: ReturnType; + custodial: ReturnType; }; constructor({ @@ -105,6 +113,12 @@ export class BlindPay { this.payouts = createPayoutsResource(this.instanceId, this.api); + this.transfers = createTransfersResource(this.instanceId, this.api); + + this.fees = createFeesResource(this.instanceId, this.api); + + this.upload = createUploadResource(this.baseUrl, this.headers); + this.receivers = { ...createReceiversResource(this.instanceId, this.api), bankAccounts: createBankAccountsResource(this.instanceId, this.api), @@ -115,6 +129,7 @@ export class BlindPay { this.wallets = { blockchain: createBlockchainWalletsResource(this.instanceId, this.api), offramp: createOfframpWalletsResource(this.instanceId, this.api), + custodial: createCustodialWalletsResource(this.instanceId, this.api), }; } diff --git a/src/index.ts b/src/index.ts index 23d729b..dd6a504 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ export * from "./client"; export * from "./resources/api-keys"; export * from "./resources/available"; export * from "./resources/bank-accounts"; +export * from "./resources/fees"; export * from "./resources/instances"; export * from "./resources/partner-fees"; export * from "./resources/payins"; @@ -9,7 +10,10 @@ export * from "./resources/payins/quotes"; export * from "./resources/payouts"; export * from "./resources/quotes"; export * from "./resources/receivers"; +export * from "./resources/transfers"; +export * from "./resources/upload"; export * from "./resources/virtual-accounts"; export * from "./resources/wallets/blockchain"; +export * from "./resources/wallets/custodial"; export * from "./resources/wallets/offramp"; export * from "./resources/webhooks"; diff --git a/src/resources/available/available.test.ts b/src/resources/available/available.test.ts index b78bf95..6581ba9 100644 --- a/src/resources/available/available.test.ts +++ b/src/resources/available/available.test.ts @@ -1,6 +1,11 @@ import { afterEach, describe, expect, it } from "vitest"; import { BlindPay } from "../../client"; -import type { GetBankDetailsResponse, GetRailsResponse, GetSwiftCodeBankDetailsResponse } from "."; +import type { + GetBankDetailsResponse, + GetNaicsCodesResponse, + GetRailsResponse, + GetSwiftCodeBankDetailsResponse, +} from "."; describe("Available", () => { afterEach(() => fetchMock.resetMocks()); @@ -95,6 +100,34 @@ describe("Available", () => { }); }); + describe("Get NAICS codes", () => { + it("should get available NAICS codes", async () => { + const mockedNaicsCodes: GetNaicsCodesResponse = [ + { + label: "Software Publishers", + value: "511210", + }, + { + label: "Computer Systems Design Services", + value: "541512", + }, + { + label: "Data Processing, Hosting, and Related Services", + value: "518210", + }, + ]; + + fetchMock.mockResponseOnce(JSON.stringify(mockedNaicsCodes), { + headers: { "Content-Type": "application/json" }, + }); + + const { data, error } = await blindpay.available.getNaicsCodes(); + + expect(error).toBeNull(); + expect(data).toEqual(mockedNaicsCodes); + }); + }); + describe("Get swift code bank details", () => { it("should get bank details of a swift code", async () => { const mockedBankDetails: GetSwiftCodeBankDetailsResponse = [ diff --git a/src/resources/available/index.ts b/src/resources/available/index.ts index d1d6e5b..3f2159a 100644 --- a/src/resources/available/index.ts +++ b/src/resources/available/index.ts @@ -69,6 +69,11 @@ export type GetRailsResponse = Array<{ country: string; }>; +export type GetNaicsCodesResponse = Array<{ + label: string; + value: string; +}>; + export type GetSwiftCodeBankDetailsInput = string; export type GetSwiftCodeBankDetailsResponse = Array<{ @@ -90,6 +95,9 @@ export function createAvailableResource(client: InternalApiClient) { getRails(): Promise> { return client.get("/available/rails"); }, + getNaicsCodes(): Promise> { + return client.get("/available/naics"); + }, getSwiftCodeBankDetails( swift: GetSwiftCodeBankDetailsInput ): Promise> { diff --git a/src/resources/bank-accounts/bank-accounts.test.ts b/src/resources/bank-accounts/bank-accounts.test.ts index c39866b..bf3371c 100644 --- a/src/resources/bank-accounts/bank-accounts.test.ts +++ b/src/resources/bank-accounts/bank-accounts.test.ts @@ -6,6 +6,7 @@ import type { CreateColombiaAchResponse, CreateInternationalSwiftResponse, CreatePixResponse, + CreatePixSafeResponse, CreateRtpResponse, CreateSpeiResponse, CreateWireResponse, @@ -153,6 +154,7 @@ describe("Bank accounts", () => { account_number: "1001001234", account_type: "checking", account_class: "individual", + recipient_relationship: "first_party", address_line_1: null, address_line_2: null, city: null, @@ -181,6 +183,12 @@ describe("Bank accounts", () => { account_type: "checking", beneficiary_name: "Individual full name or business name", routing_number: "012345678", + recipient_relationship: "first_party", + address_line_1: "123 Main St", + city: "New York", + state_province_region: "NY", + country: "US", + postal_code: "10001", }); expect(error).toBeNull(); @@ -197,6 +205,8 @@ describe("Bank accounts", () => { beneficiary_name: "Individual full name or business name", routing_number: "012345678", account_number: "1001001234", + account_class: "individual", + recipient_relationship: "first_party", address_line_1: "Address line 1", address_line_2: "Address line 2", city: "City", @@ -213,9 +223,11 @@ describe("Bank accounts", () => { const { data, error } = await blindpay.receivers.bankAccounts.createWire({ receiver_id: "re_000000000000", name: "Wire Account", + account_class: "individual", account_number: "1001001234", beneficiary_name: "Individual full name or business name", routing_number: "012345678", + recipient_relationship: "first_party", address_line_1: "Address line 1", address_line_2: "Address line 2", city: "City", @@ -236,6 +248,8 @@ describe("Bank accounts", () => { type: "international_swift", name: "International Swift Account", beneficiary_name: null, + account_class: "individual", + recipient_relationship: "first_party", address_line_1: null, address_line_2: null, city: null, @@ -264,6 +278,7 @@ describe("Bank accounts", () => { swift_intermediary_bank_account_number_iban: null, swift_intermediary_bank_name: null, swift_intermediary_bank_country: null, + swift_payment_code: null, created_at: "2021-01-01T00:00:00Z", }; @@ -274,6 +289,8 @@ describe("Bank accounts", () => { const { data, error } = await blindpay.receivers.bankAccounts.createInternationalSwift({ receiver_id: "re_000000000000", name: "International Swift Account", + account_class: "individual", + recipient_relationship: "first_party", swift_account_holder_name: "John Doe", swift_account_number_iban: "123456789", swift_bank_address_line_1: @@ -310,6 +327,8 @@ describe("Bank accounts", () => { beneficiary_name: "John Doe", routing_number: "121000358", account_number: "325203027578", + account_class: "individual", + recipient_relationship: "first_party", address_line_1: "Street of the fools", address_line_2: null, city: "Fools City", @@ -326,9 +345,11 @@ describe("Bank accounts", () => { const { data, error } = await blindpay.receivers.bankAccounts.createRtp({ receiver_id: "re_000000000000", name: "John Doe RTP", + account_class: "individual", beneficiary_name: "John Doe", routing_number: "121000358", account_number: "325203027578", + recipient_relationship: "first_party", address_line_1: "Street of the fools", city: "Fools City", state_province_region: "FL", @@ -446,13 +467,50 @@ describe("Bank accounts", () => { headers: { "Content-Type": "application/json" }, }); - const { data, error } = await blindpay.receivers.bankAccounts.list("re_000000000000"); + const { data, error } = await blindpay.receivers.bankAccounts.list({ + receiver_id: "re_000000000000", + }); expect(error).toBeNull(); expect(data).toEqual(mockedBankAccounts); }); }); + describe("Create pix safe bank account", () => { + it("should create a pix safe bank account", async () => { + const mockedPixSafeAccount: CreatePixSafeResponse = { + id: "ba_000000000000", + type: "pix_safe", + name: "PIX Safe Account", + beneficiary_name: "Maria Silva", + account_number: "123456789", + account_type: "checking", + pix_safe_bank_code: "001", + pix_safe_branch_code: "0001", + pix_safe_cpf_cnpj: "12345678901", + created_at: "2021-01-01T00:00:00Z", + }; + + fetchMock.mockResponseOnce(JSON.stringify(mockedPixSafeAccount), { + headers: { "Content-Type": "application/json" }, + }); + + const { data, error } = await blindpay.receivers.bankAccounts.createPixSafe({ + receiver_id: "re_000000000000", + name: "PIX Safe Account", + beneficiary_name: "Maria Silva", + account_number: "123456789", + account_type: "checking", + pix_safe_bank_code: "001", + pix_safe_branch_code: "0001", + pix_safe_cpf_cnpj: "12345678901", + }); + + expect(error).toBeNull(); + expect(data).toEqual(mockedPixSafeAccount); + }); + }); + describe("Delete bank account", () => { it("should delete a bank account", async () => { fetchMock.mockResponseOnce(JSON.stringify({ data: null }), { diff --git a/src/resources/bank-accounts/index.ts b/src/resources/bank-accounts/index.ts index df49aee..802b68b 100644 --- a/src/resources/bank-accounts/index.ts +++ b/src/resources/bank-accounts/index.ts @@ -9,7 +9,28 @@ import type { } from "../../../types"; import type { InternalApiClient } from "../../internal/api-client"; import type { SpeiProtocol } from "../payouts"; -export type ListBankAccountsInput = string; +import type { BusinessIndustry } from "../receivers"; + +export type RecipientRelationship = + | "first_party" + | "employee" + | "independent_contractor" + | "vendor_or_supplier" + | "subsidiary_or_affiliate" + | "merchant_or_partner" + | "customer" + | "landlord" + | "family" + | "other"; + +export type ListBankAccountsInput = { + receiver_id: string; + status?: string; + type?: Rail; + name?: string; + bank_account_id?: string; + country?: Country; +}; export type ListBankAccountsResponse = { data: Array<{ @@ -60,6 +81,9 @@ export type ListBankAccountsResponse = { swift_intermediary_bank_account_number_iban?: string; swift_intermediary_bank_name?: string; swift_intermediary_bank_country?: Country; + pix_safe_bank_code?: string; + pix_safe_branch_code?: string; + pix_safe_cpf_cnpj?: string; tron_wallet_hash?: string; offramp_wallets?: Array<{ address: string; @@ -110,6 +134,30 @@ export type CreatePixResponse = { created_at: string; }; +export type CreatePixSafeInput = { + receiver_id: string; + name: string; + beneficiary_name: string; + account_number: string; + account_type: BankAccountType; + pix_safe_bank_code: string; + pix_safe_branch_code: string; + pix_safe_cpf_cnpj: string; +}; + +export type CreatePixSafeResponse = { + id: string; + type: "pix_safe"; + name: string; + beneficiary_name: string; + account_number: string; + account_type: BankAccountType; + pix_safe_bank_code: string; + pix_safe_branch_code: string; + pix_safe_cpf_cnpj: string; + created_at: string; +}; + export type CreateArgentinaTransfersInput = { receiver_id: string; name: string; @@ -184,6 +232,17 @@ export type CreateAchInput = { account_type: BankAccountType; beneficiary_name: string; routing_number: string; + recipient_relationship: RecipientRelationship; + address_line_1: string; + city: string; + state_province_region: string; + country: Country; + postal_code: string; + address_line_2?: string; + business_industry?: BusinessIndustry; + phone_number?: string; + tax_id?: string; + date_of_birth?: string; }; export type CreateAchResponse = { @@ -195,6 +254,7 @@ export type CreateAchResponse = { account_number: string; account_type: BankAccountType; account_class: AccountClass; + recipient_relationship: RecipientRelationship; address_line_1: string | null; address_line_2: string | null; city: string | null; @@ -214,15 +274,21 @@ export type CreateAchResponse = { export type CreateWireInput = { receiver_id: string; name: string; + account_class: AccountClass; account_number: string; beneficiary_name: string; routing_number: string; + recipient_relationship: RecipientRelationship; address_line_1: string; address_line_2?: string; city: string; state_province_region: string; country: Country; postal_code: string; + business_industry?: BusinessIndustry; + phone_number?: string; + tax_id?: string; + date_of_birth?: string; }; export type CreateWireResponse = { @@ -232,8 +298,10 @@ export type CreateWireResponse = { beneficiary_name: string; routing_number: string; account_number: string; + account_class: AccountClass; + recipient_relationship: RecipientRelationship; address_line_1: string; - address_line_2: string; + address_line_2: string | null; city: string; state_province_region: string; country: Country; @@ -244,6 +312,8 @@ export type CreateWireResponse = { export type CreateInternationalSwiftInput = { receiver_id: string; name: string; + account_class: AccountClass; + recipient_relationship: RecipientRelationship; swift_account_holder_name: string; swift_account_number_iban: string; swift_bank_address_line_1: string; @@ -260,10 +330,15 @@ export type CreateInternationalSwiftInput = { swift_beneficiary_postal_code: string; swift_beneficiary_state_province_region: string; swift_code_bic: string; - swift_intermediary_bank_account_number_iban: string | null; - swift_intermediary_bank_country: Country | null; - swift_intermediary_bank_name: string | null; - swift_intermediary_bank_swift_code_bic: string | null; + swift_intermediary_bank_account_number_iban?: string | null; + swift_intermediary_bank_country?: Country | null; + swift_intermediary_bank_name?: string | null; + swift_intermediary_bank_swift_code_bic?: string | null; + swift_payment_code?: string; + business_industry?: BusinessIndustry; + phone_number?: string; + tax_id?: string; + date_of_birth?: string; }; export type CreateInternationalSwiftResponse = { @@ -271,6 +346,8 @@ export type CreateInternationalSwiftResponse = { type: "international_swift"; name: string; beneficiary_name: string | null; + account_class: AccountClass; + recipient_relationship: RecipientRelationship; address_line_1: string | null; address_line_2: string | null; city: string | null; @@ -297,21 +374,28 @@ export type CreateInternationalSwiftResponse = { swift_intermediary_bank_account_number_iban: string | null; swift_intermediary_bank_name: string | null; swift_intermediary_bank_country: Country | null; + swift_payment_code: string | null; created_at: string; }; export type CreateRtpInput = { receiver_id: string; name: string; + account_class: AccountClass; beneficiary_name: string; routing_number: string; account_number: string; + recipient_relationship: RecipientRelationship; address_line_1: string; address_line_2?: string; city: string; state_province_region: string; country: Country; postal_code: string; + business_industry?: BusinessIndustry; + phone_number?: string; + tax_id?: string; + date_of_birth?: string; }; export type CreateRtpResponse = { @@ -321,6 +405,8 @@ export type CreateRtpResponse = { beneficiary_name: string; routing_number: string; account_number: string; + account_class: AccountClass; + recipient_relationship: RecipientRelationship; address_line_1: string; address_line_2: string | null; city: string; @@ -332,10 +418,16 @@ export type CreateRtpResponse = { export function createBankAccountsResource(instanceId: string, client: InternalApiClient) { return { - list( - receiver_id: ListBankAccountsInput - ): Promise> { - return client.get(`/instances/${instanceId}/receivers/${receiver_id}/bank-accounts`); + list({ + receiver_id, + ...params + }: ListBankAccountsInput): Promise> { + const queryParams = Object.keys(params).length + ? `?${new URLSearchParams(params as Record)}` + : ""; + return client.get( + `/instances/${instanceId}/receivers/${receiver_id}/bank-accounts${queryParams}` + ); }, get({ receiver_id, @@ -359,6 +451,15 @@ export function createBankAccountsResource(instanceId: string, client: InternalA ...data, }); }, + createPixSafe({ + receiver_id, + ...data + }: CreatePixSafeInput): Promise> { + return client.post(`/instances/${instanceId}/receivers/${receiver_id}/bank-accounts`, { + type: "pix_safe", + ...data, + }); + }, createArgentinaTransfers({ receiver_id, ...data diff --git a/src/resources/fees/fees.test.ts b/src/resources/fees/fees.test.ts new file mode 100644 index 0000000..f3b51c3 --- /dev/null +++ b/src/resources/fees/fees.test.ts @@ -0,0 +1,52 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { BlindPay } from "../../client"; +import type { GetFeesResponse } from "."; + +const mockFeeOptions = { + payin_flat: 0, + payin_percentage: 0, + payout_flat: 100, + payout_percentage: 0, +}; + +describe("Fees", () => { + afterEach(() => fetchMock.resetMocks()); + + const blindpay = new BlindPay({ apiKey: "test-key", instanceId: "in_000000000000" }); + + describe("Get fees", () => { + it("should get instance fees", async () => { + const mockedFees: GetFeesResponse = { + id: "fe_000000000000", + instance_id: "in_000000000000", + ach: mockFeeOptions, + domestic_wire: mockFeeOptions, + rtp: mockFeeOptions, + international_swift: mockFeeOptions, + pix: mockFeeOptions, + pix_safe: mockFeeOptions, + ach_colombia: mockFeeOptions, + transfers_3: mockFeeOptions, + spei: mockFeeOptions, + tron: mockFeeOptions, + ethereum: mockFeeOptions, + polygon: mockFeeOptions, + base: mockFeeOptions, + arbitrum: mockFeeOptions, + stellar: mockFeeOptions, + solana: mockFeeOptions, + created_at: "2025-01-01T00:00:00Z", + updated_at: "2025-01-01T00:00:00Z", + }; + + fetchMock.mockResponseOnce(JSON.stringify(mockedFees), { + headers: { "Content-Type": "application/json" }, + }); + + const { data, error } = await blindpay.fees.get(); + + expect(error).toBeNull(); + expect(data).toEqual(mockedFees); + }); + }); +}); diff --git a/src/resources/fees/index.ts b/src/resources/fees/index.ts new file mode 100644 index 0000000..3160110 --- /dev/null +++ b/src/resources/fees/index.ts @@ -0,0 +1,40 @@ +import type { BlindpayApiResponse } from "../../../types"; +import type { InternalApiClient } from "../../internal/api-client"; + +export type FeeOptions = { + payin_flat: number; + payin_percentage: number; + payout_flat: number; + payout_percentage: number; +}; + +export type GetFeesResponse = { + id: string; + instance_id: string; + ach: FeeOptions; + domestic_wire: FeeOptions; + rtp: FeeOptions; + international_swift: FeeOptions; + pix: FeeOptions; + pix_safe: FeeOptions; + ach_colombia: FeeOptions; + transfers_3: FeeOptions; + spei: FeeOptions; + tron: FeeOptions; + ethereum: FeeOptions; + polygon: FeeOptions; + base: FeeOptions; + arbitrum: FeeOptions; + stellar: FeeOptions; + solana: FeeOptions; + created_at: string; + updated_at: string; +}; + +export function createFeesResource(instanceId: string, client: InternalApiClient) { + return { + get(): Promise> { + return client.get(`/instances/${instanceId}/billing/fees`); + }, + }; +} diff --git a/src/resources/payouts/index.ts b/src/resources/payouts/index.ts index e187bf3..2501c97 100644 --- a/src/resources/payouts/index.ts +++ b/src/resources/payouts/index.ts @@ -100,13 +100,25 @@ export type CreatePayoutInput = { receiver_id: string; bank_account_id: string; amount: number; - currency: string; + currency: Currency; description?: string | null; reference?: string | null; network?: Network | null; token?: StablecoinToken | null; }; +export type SubmitPayoutDocumentsInput = { + payout_id: string; + transaction_document_type: TransactionDocumentType; + transaction_document_id: string; + transaction_document_file: string; + description?: string; +}; + +export type SubmitPayoutDocumentsResponse = { + id: string; +}; + export type GetPayoutInput = string; export type GetPayoutResponse = Payout; @@ -213,5 +225,13 @@ export function createPayoutsResource(instanceId: string, client: InternalApiCli }: CreateSolanaPayoutInput): Promise> { return client.post(`/instances/${instanceId}/payouts/solana`, data); }, + submitDocuments({ + payout_id, + ...data + }: SubmitPayoutDocumentsInput): Promise< + BlindpayApiResponse + > { + return client.post(`/instances/${instanceId}/payouts/${payout_id}/documents`, data); + }, }; } diff --git a/src/resources/payouts/payouts.test.ts b/src/resources/payouts/payouts.test.ts index 64f2645..bb58903 100644 --- a/src/resources/payouts/payouts.test.ts +++ b/src/resources/payouts/payouts.test.ts @@ -8,6 +8,7 @@ import type { GetPayoutResponse, GetPayoutTrackResponse, ListPayoutsResponse, + SubmitPayoutDocumentsResponse, } from "."; describe("Payouts", () => { @@ -563,4 +564,27 @@ describe("Payouts", () => { expect(data).toEqual(mockedSolanaPayout); }); }); + + describe("Submit payout documents", () => { + it("should submit payout documents", async () => { + const mockedResponse: SubmitPayoutDocumentsResponse = { + id: "pd_000000000000", + }; + + fetchMock.mockResponseOnce(JSON.stringify(mockedResponse), { + headers: { "Content-Type": "application/json" }, + }); + + const { data, error } = await blindpay.payouts.submitDocuments({ + payout_id: "pa_000000000000", + transaction_document_type: "invoice", + transaction_document_id: "INV-2025-001", + transaction_document_file: "https://storage.blindpay.com/uploads/invoice.pdf", + description: "Invoice for services", + }); + + expect(error).toBeNull(); + expect(data).toEqual(mockedResponse); + }); + }); }); diff --git a/src/resources/receivers/index.ts b/src/resources/receivers/index.ts index af1df5f..b838307 100644 --- a/src/resources/receivers/index.ts +++ b/src/resources/receivers/index.ts @@ -1,4 +1,10 @@ -import type { AccountClass, BlindpayApiResponse, Country } from "../../../types"; +import type { + AccountClass, + BlindpayApiResponse, + Country, + PaginationMetadata, + PaginationParams, +} from "../../../types"; import type { InternalApiClient } from "../../internal/api-client"; import type { StrictOmit } from "../../internal/helpers/strict-omit"; @@ -396,9 +402,19 @@ export type CreateBusinessWithStandardKYBResponse = { id: string; }; -export type ListReceiversResponse = Array< - IndividualWithStandardKYC | IndividualWithEnhancedKYC | BusinessWithStandardKYB ->; +export type ListReceiversInput = PaginationParams & { + full_name?: string; + receiver_name?: string; + status?: string; + receiver_id?: string; + bank_account_id?: string; + country?: Country; +}; + +export type ListReceiversResponse = { + data: Array; + pagination: PaginationMetadata; +}; export type GetReceiverInput = string; @@ -509,8 +525,9 @@ export type RequestLimitIncreaseResponse = { export function createReceiversResource(instanceId: string, client: InternalApiClient) { return { - list(): Promise> { - return client.get(`/instances/${instanceId}/receivers`); + list(params?: ListReceiversInput): Promise> { + const queryParams = params ? `?${new URLSearchParams(params)}` : ""; + return client.get(`/instances/${instanceId}/receivers${queryParams}`); }, createIndividualWithStandardKYC( data: CreateIndividualWithStandardKYCInput @@ -543,7 +560,7 @@ export function createReceiversResource(instanceId: string, client: InternalApiC return client.get(`/instances/${instanceId}/receivers/${receiver_id}`); }, update({ receiver_id, ...data }: UpdateReceiverInput): Promise> { - return client.patch(`/instances/${instanceId}/receivers/${receiver_id}`, data); + return client.put(`/instances/${instanceId}/receivers/${receiver_id}`, data); }, delete(receiver_id: DeleteReceiverInput): Promise> { return client.delete(`/instances/${instanceId}/receivers/${receiver_id}`); diff --git a/src/resources/receivers/receivers.test.ts b/src/resources/receivers/receivers.test.ts index ad75740..f96bd61 100644 --- a/src/resources/receivers/receivers.test.ts +++ b/src/resources/receivers/receivers.test.ts @@ -18,159 +18,166 @@ describe("Receivers", () => { describe("List receivers", () => { it("should list receivers", async () => { - const mockedReceivers: ListReceiversResponse = [ - { - id: "re_Euw7HN4OdxPn", - type: "individual", - kyc_type: "standard", - kyc_status: "verifying", - is_tos_accepted: true, - kyc_warnings: [ - { - code: null, - message: null, - resolution_status: null, - warning_id: null, + const mockedReceivers: ListReceiversResponse = { + data: [ + { + id: "re_Euw7HN4OdxPn", + type: "individual", + kyc_type: "standard", + kyc_status: "verifying", + is_tos_accepted: true, + kyc_warnings: [ + { + code: null, + message: null, + resolution_status: null, + warning_id: null, + }, + ], + email: "bernardo@gmail.com", + tax_id: "12345678900", + address_line_1: "Av. Paulista, 1000", + address_line_2: "Apto 101", + city: "São Paulo", + state_province_region: "SP", + country: "BR", + postal_code: "01310-100", + ip_address: "127.0.0.1", + image_url: "https://example.com/image.png", + phone_number: "+5511987654321", + proof_of_address_doc_type: "UTILITY_BILL", + proof_of_address_doc_file: "https://example.com/image.png", + first_name: "Bernardo", + last_name: "Simonassi", + date_of_birth: "1998-02-02T00:00:00.000Z", + id_doc_country: "BR", + id_doc_type: "PASSPORT", + id_doc_front_file: "https://example.com/image.png", + id_doc_back_file: "https://example.com/image.png", + aiprise_validation_key: "", + instance_id: "in_000000000000", + tos_id: "to_3ZZhllJkvo5Z", + created_at: "2021-01-01T00:00:00.000Z", + updated_at: "2021-01-01T00:00:00.000Z", + limit: { + per_transaction: 100000, + daily: 200000, + monthly: 1000000, }, - ], - email: "bernardo@gmail.com", - tax_id: "12345678900", - address_line_1: "Av. Paulista, 1000", - address_line_2: "Apto 101", - city: "São Paulo", - state_province_region: "SP", - country: "BR", - postal_code: "01310-100", - ip_address: "127.0.0.1", - image_url: "https://example.com/image.png", - phone_number: "+5511987654321", - proof_of_address_doc_type: "UTILITY_BILL", - proof_of_address_doc_file: "https://example.com/image.png", - first_name: "Bernardo", - last_name: "Simonassi", - date_of_birth: "1998-02-02T00:00:00.000Z", - id_doc_country: "BR", - id_doc_type: "PASSPORT", - id_doc_front_file: "https://example.com/image.png", - id_doc_back_file: "https://example.com/image.png", - aiprise_validation_key: "", - instance_id: "in_000000000000", - tos_id: "to_3ZZhllJkvo5Z", - created_at: "2021-01-01T00:00:00.000Z", - updated_at: "2021-01-01T00:00:00.000Z", - limit: { - per_transaction: 100000, - daily: 200000, - monthly: 1000000, }, - }, - { - id: "re_YuaMcI2B8zbQ", - type: "individual", - is_tos_accepted: true, - kyc_type: "enhanced", - kyc_status: "approved", - kyc_warnings: null, - email: "alice.johnson@example.com", - tax_id: "98765432100", - address_line_1: "123 Main St", - address_line_2: null, - city: "New York", - state_province_region: "NY", - country: "US", - postal_code: "10001", - ip_address: "192.168.1.1", - image_url: null, - phone_number: "+15555555555", - proof_of_address_doc_type: "BANK_STATEMENT", - proof_of_address_doc_file: "https://example.com/image.png", - first_name: "Alice", - last_name: "Johnson", - date_of_birth: "1990-05-10T00:00:00.000Z", - id_doc_country: "US", - id_doc_type: "PASSPORT", - id_doc_front_file: "https://example.com/image.png", - id_doc_back_file: null, - aiprise_validation_key: "enhanced-key", - instance_id: "in_000000000001", - source_of_funds_doc_type: "salary", - source_of_funds_doc_file: "https://example.com/image.png", - individual_holding_doc_front_file: "https://example.com/image.png", - purpose_of_transactions: "investment_purposes", - purpose_of_transactions_explanation: "Investing in stocks", - tos_id: "to_nppX66ntvtHs", - created_at: "2022-02-02T00:00:00.000Z", - updated_at: "2022-02-02T00:00:00.000Z", - limit: { - per_transaction: 50000, - daily: 100000, - monthly: 500000, + { + id: "re_YuaMcI2B8zbQ", + type: "individual", + is_tos_accepted: true, + kyc_type: "enhanced", + kyc_status: "approved", + kyc_warnings: null, + email: "alice.johnson@example.com", + tax_id: "98765432100", + address_line_1: "123 Main St", + address_line_2: null, + city: "New York", + state_province_region: "NY", + country: "US", + postal_code: "10001", + ip_address: "192.168.1.1", + image_url: null, + phone_number: "+15555555555", + proof_of_address_doc_type: "BANK_STATEMENT", + proof_of_address_doc_file: "https://example.com/image.png", + first_name: "Alice", + last_name: "Johnson", + date_of_birth: "1990-05-10T00:00:00.000Z", + id_doc_country: "US", + id_doc_type: "PASSPORT", + id_doc_front_file: "https://example.com/image.png", + id_doc_back_file: null, + aiprise_validation_key: "enhanced-key", + instance_id: "in_000000000001", + source_of_funds_doc_type: "salary", + source_of_funds_doc_file: "https://example.com/image.png", + individual_holding_doc_front_file: "https://example.com/image.png", + purpose_of_transactions: "investment_purposes", + purpose_of_transactions_explanation: "Investing in stocks", + tos_id: "to_nppX66ntvtHs", + created_at: "2022-02-02T00:00:00.000Z", + updated_at: "2022-02-02T00:00:00.000Z", + limit: { + per_transaction: 50000, + daily: 100000, + monthly: 500000, + }, }, - }, - { - id: "re_IOxAUL24LG7P", - type: "business", - kyc_type: "standard", - is_tos_accepted: true, - is_fbo: false, - kyc_status: "pending", - kyc_warnings: null, - email: "business@example.com", - tax_id: "20096178000195", - address_line_1: "1 King St W", - address_line_2: "Suite 100", - city: "Toronto", - state_province_region: "ON", - country: "CA", - postal_code: "M5H 1A1", - ip_address: null, - image_url: null, - phone_number: "+14165555555", - proof_of_address_doc_type: "UTILITY_BILL", - proof_of_address_doc_file: "https://example.com/image.png", - legal_name: "Business Corp", - alternate_name: "BizCo", - formation_date: "2010-01-01T00:00:00.000Z", - website: "https://businesscorp.com", - owners: [ - { - role: "beneficial_owner", - first_name: "Carlos", - last_name: "Silva", - date_of_birth: "1995-05-15T00:00:00.000Z", - tax_id: "12345678901", - address_line_1: "Rua Augusta, 1500", - address_line_2: null, - city: "São Paulo", - state_province_region: "SP", - country: "BR", - postal_code: "01304-001", - id_doc_country: "BR", - id_doc_type: "PASSPORT", - id_doc_front_file: "https://example.com/image.png", - id_doc_back_file: "https://example.com/image.png", - proof_of_address_doc_type: "UTILITY_BILL", - proof_of_address_doc_file: "https://example.com/image.png", - ownership_percentage: 25, - title: "CEO", - id: "ub_000000000000", + { + id: "re_IOxAUL24LG7P", + type: "business", + kyc_type: "standard", + is_tos_accepted: true, + is_fbo: false, + kyc_status: "pending", + kyc_warnings: null, + email: "business@example.com", + tax_id: "20096178000195", + address_line_1: "1 King St W", + address_line_2: "Suite 100", + city: "Toronto", + state_province_region: "ON", + country: "CA", + postal_code: "M5H 1A1", + ip_address: null, + image_url: null, + phone_number: "+14165555555", + proof_of_address_doc_type: "UTILITY_BILL", + proof_of_address_doc_file: "https://example.com/image.png", + legal_name: "Business Corp", + alternate_name: "BizCo", + formation_date: "2010-01-01T00:00:00.000Z", + website: "https://businesscorp.com", + owners: [ + { + role: "beneficial_owner", + first_name: "Carlos", + last_name: "Silva", + date_of_birth: "1995-05-15T00:00:00.000Z", + tax_id: "12345678901", + address_line_1: "Rua Augusta, 1500", + address_line_2: null, + city: "São Paulo", + state_province_region: "SP", + country: "BR", + postal_code: "01304-001", + id_doc_country: "BR", + id_doc_type: "PASSPORT", + id_doc_front_file: "https://example.com/image.png", + id_doc_back_file: "https://example.com/image.png", + proof_of_address_doc_type: "UTILITY_BILL", + proof_of_address_doc_file: "https://example.com/image.png", + ownership_percentage: 25, + title: "CEO", + id: "ub_000000000000", + }, + ], + incorporation_doc_file: "https://example.com/image.png", + proof_of_ownership_doc_file: "https://example.com/image.png", + external_id: null, + instance_id: "in_000000000002", + tos_id: "to_nppX66ntvtHs", + aiprise_validation_key: "", + created_at: "2015-03-15T00:00:00.000Z", + updated_at: "2015-03-15T00:00:00.000Z", + limit: { + per_transaction: 200000, + daily: 400000, + monthly: 2000000, }, - ], - incorporation_doc_file: "https://example.com/image.png", - proof_of_ownership_doc_file: "https://example.com/image.png", - external_id: null, - instance_id: "in_000000000002", - tos_id: "to_nppX66ntvtHs", - aiprise_validation_key: "", - created_at: "2015-03-15T00:00:00.000Z", - updated_at: "2015-03-15T00:00:00.000Z", - limit: { - per_transaction: 200000, - daily: 400000, - monthly: 2000000, }, + ], + pagination: { + has_more: false, + next_page: "", + prev_page: "", }, - ]; + }; fetchMock.mockResponseOnce(JSON.stringify(mockedReceivers), { headers: { "Content-Type": "application/json" }, diff --git a/src/resources/transfers/index.ts b/src/resources/transfers/index.ts new file mode 100644 index 0000000..52a309c --- /dev/null +++ b/src/resources/transfers/index.ts @@ -0,0 +1,134 @@ +import type { + BlindpayApiResponse, + Network, + PaginationMetadata, + PaginationParams, + StablecoinToken, + TrackingStatus, + TransactionStatus, +} from "../../../types"; +import type { InternalApiClient } from "../../internal/api-client"; + +export type TransferTrackingStep = { + step: TrackingStatus; + transaction_hash: string | null; + gas_fee: string | null; + completed_at: string | null; + error_message: string | null; +}; + +export type TransferTrackingTransactionMonitoring = { + step: TrackingStatus; + blockchain_screening: number | null; + risk_score: number | null; + completed_at: string | null; +}; + +export type Transfer = { + id: string; + status: TransactionStatus; + transfer_quote_id: string; + instance_id: string; + tracking_transaction_monitoring: TransferTrackingTransactionMonitoring; + tracking_paymaster: TransferTrackingStep; + tracking_bridge_swap: TransferTrackingStep; + tracking_complete: TransferTrackingStep; + tracking_partner_fee: TransferTrackingStep; + created_at: string; + updated_at: string; + image_url?: string; + first_name?: string; + last_name?: string; + legal_name?: string; + wallet_id: string; + sender_token: StablecoinToken; + sender_amount: number; + receiver_amount: number; + receiver_network: Network; + receiver_token: StablecoinToken; + receiver_wallet_address: string; + partner_fee_amount: number | null; +}; + +export type CreateTransferQuoteInput = { + wallet_id: string; + sender_token: StablecoinToken; + receiver_wallet_address: string; + receiver_token: StablecoinToken; + receiver_network: Network; + request_amount: number; + cover_fees: boolean; + amount_reference: "sender" | "receiver"; + partner_fee_id?: string | null; +}; + +export type CreateTransferQuoteResponse = { + id: string; + expires_at: number; + commercial_quotation: number; + blindpay_quotation: number; + receiver_amount: number; + sender_amount: number; + partner_fee_amount: number | null; + flat_fee: number; +}; + +export type CreateTransferInput = { + transfer_quote_id: string; +}; + +export type CreateTransferResponse = { + id: string; + status: TransactionStatus; + tracking_bridge_swap: TransferTrackingStep; + tracking_complete: TransferTrackingStep; + tracking_paymaster: TransferTrackingStep; + tracking_transaction_monitoring: TransferTrackingTransactionMonitoring; + tracking_partner_fee: TransferTrackingStep; +}; + +export type ListTransfersInput = PaginationParams; + +export type ListTransfersResponse = { + data: Transfer[]; + pagination: PaginationMetadata; +}; + +export type GetTransferInput = string; + +export type GetTransferResponse = Transfer; + +export type GetTransferTrackInput = string; + +export type GetTransferTrackResponse = Transfer; + +export function createTransfersResource(instanceId: string, client: InternalApiClient) { + return { + quotes: { + create({ + ...data + }: CreateTransferQuoteInput): Promise< + BlindpayApiResponse + > { + return client.post(`/instances/${instanceId}/transfer-quotes`, data); + }, + }, + create({ + ...data + }: CreateTransferInput): Promise> { + return client.post(`/instances/${instanceId}/transfers`, data); + }, + list(params?: ListTransfersInput): Promise> { + const queryParams = params ? `?${new URLSearchParams(params)}` : ""; + return client.get(`/instances/${instanceId}/transfers${queryParams}`); + }, + get(transferId: GetTransferInput): Promise> { + return client.get(`/instances/${instanceId}/transfers/${transferId}`); + }, + getTrack( + transferId: GetTransferTrackInput + ): Promise> { + return client.get(`/e/transfers/${transferId}`); + }, + }; +} diff --git a/src/resources/transfers/transfers.test.ts b/src/resources/transfers/transfers.test.ts new file mode 100644 index 0000000..2476797 --- /dev/null +++ b/src/resources/transfers/transfers.test.ts @@ -0,0 +1,163 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { BlindPay } from "../../client"; +import type { + CreateTransferQuoteResponse, + CreateTransferResponse, + GetTransferResponse, + GetTransferTrackResponse, + ListTransfersResponse, + Transfer, +} from "."; + +const mockTrackingStep = { + step: "processing" as const, + transaction_hash: null, + gas_fee: null, + completed_at: null, + error_message: null, +}; + +const mockTrackingMonitoring = { + step: "processing" as const, + blockchain_screening: null, + risk_score: null, + completed_at: null, +}; + +const mockTransfer: Transfer = { + id: "tr_000000000000", + status: "processing", + transfer_quote_id: "tq_000000000000", + instance_id: "in_000000000000", + tracking_transaction_monitoring: mockTrackingMonitoring, + tracking_paymaster: mockTrackingStep, + tracking_bridge_swap: mockTrackingStep, + tracking_complete: mockTrackingStep, + tracking_partner_fee: mockTrackingStep, + created_at: "2025-01-01T00:00:00Z", + updated_at: "2025-01-01T00:00:00Z", + wallet_id: "wa_000000000000", + sender_token: "USDC", + sender_amount: 10000, + receiver_amount: 9950, + receiver_network: "polygon", + receiver_token: "USDC", + receiver_wallet_address: "0xDD6a3aD0949396e57C7738ba8FC1A46A5a1C372C", + partner_fee_amount: null, +}; + +describe("Transfers", () => { + afterEach(() => fetchMock.resetMocks()); + + const blindpay = new BlindPay({ apiKey: "test-key", instanceId: "in_000000000000" }); + + describe("Create transfer quote", () => { + it("should create a transfer quote", async () => { + const mockedQuote: CreateTransferQuoteResponse = { + id: "tq_000000000000", + expires_at: 1700000000, + commercial_quotation: 100, + blindpay_quotation: 100, + receiver_amount: 9950, + sender_amount: 10000, + partner_fee_amount: null, + flat_fee: 50, + }; + + fetchMock.mockResponseOnce(JSON.stringify(mockedQuote), { + headers: { "Content-Type": "application/json" }, + }); + + const { data, error } = await blindpay.transfers.quotes.create({ + wallet_id: "wa_000000000000", + sender_token: "USDC", + receiver_wallet_address: "0xDD6a3aD0949396e57C7738ba8FC1A46A5a1C372C", + receiver_token: "USDC", + receiver_network: "polygon", + request_amount: 10000, + cover_fees: true, + amount_reference: "sender", + }); + + expect(error).toBeNull(); + expect(data).toEqual(mockedQuote); + }); + }); + + describe("Create transfer", () => { + it("should create a transfer", async () => { + const mockedTransfer: CreateTransferResponse = { + id: "tr_000000000000", + status: "processing", + tracking_bridge_swap: mockTrackingStep, + tracking_complete: mockTrackingStep, + tracking_paymaster: mockTrackingStep, + tracking_transaction_monitoring: mockTrackingMonitoring, + tracking_partner_fee: mockTrackingStep, + }; + + fetchMock.mockResponseOnce(JSON.stringify(mockedTransfer), { + headers: { "Content-Type": "application/json" }, + }); + + const { data, error } = await blindpay.transfers.create({ + transfer_quote_id: "tq_000000000000", + }); + + expect(error).toBeNull(); + expect(data).toEqual(mockedTransfer); + }); + }); + + describe("List transfers", () => { + it("should list transfers", async () => { + const mockedTransfers: ListTransfersResponse = { + data: [mockTransfer], + pagination: { + has_more: false, + next_page: "", + prev_page: "", + }, + }; + + fetchMock.mockResponseOnce(JSON.stringify(mockedTransfers), { + headers: { "Content-Type": "application/json" }, + }); + + const { data, error } = await blindpay.transfers.list(); + + expect(error).toBeNull(); + expect(data).toEqual(mockedTransfers); + }); + }); + + describe("Get transfer", () => { + it("should get a transfer", async () => { + const mockedTransfer: GetTransferResponse = mockTransfer; + + fetchMock.mockResponseOnce(JSON.stringify(mockedTransfer), { + headers: { "Content-Type": "application/json" }, + }); + + const { data, error } = await blindpay.transfers.get("tr_000000000000"); + + expect(error).toBeNull(); + expect(data).toEqual(mockedTransfer); + }); + }); + + describe("Get transfer track", () => { + it("should get transfer tracking information", async () => { + const mockedTrack: GetTransferTrackResponse = mockTransfer; + + fetchMock.mockResponseOnce(JSON.stringify(mockedTrack), { + headers: { "Content-Type": "application/json" }, + }); + + const { data, error } = await blindpay.transfers.getTrack("tr_000000000000"); + + expect(error).toBeNull(); + expect(data).toEqual(mockedTrack); + }); + }); +}); diff --git a/src/resources/upload/index.ts b/src/resources/upload/index.ts new file mode 100644 index 0000000..4b53517 --- /dev/null +++ b/src/resources/upload/index.ts @@ -0,0 +1,54 @@ +import type { BlindpayApiResponse } from "../../../types"; + +export type UploadBucket = "avatar" | "onboarding" | "limit_increase"; + +export type UploadInput = { + file: File | Blob; + bucket: UploadBucket; + instance_id?: string; +}; + +export type UploadResponse = { + file_url: string; +}; + +export function createUploadResource(baseUrl: string, headers: Record) { + return { + async upload({ + file, + bucket, + instance_id, + }: UploadInput): Promise> { + const formData = new FormData(); + formData.append("file", file); + formData.append("bucket", bucket); + + const queryParams = instance_id ? `?instance_id=${instance_id}` : ""; + + const { "Content-Type": _, ...headersWithoutContentType } = headers; + + const response = await fetch(`${baseUrl}/upload${queryParams}`, { + method: "POST", + headers: headersWithoutContentType, + body: formData, + }); + + if (!response.ok) { + const error = await response.json(); + return { + data: null, + error: { + message: error?.message ?? "Unknown error", + }, + }; + } + + const data = await response.json(); + + return { + data, + error: null, + }; + }, + }; +} diff --git a/src/resources/upload/upload.test.ts b/src/resources/upload/upload.test.ts new file mode 100644 index 0000000..0b330ad --- /dev/null +++ b/src/resources/upload/upload.test.ts @@ -0,0 +1,52 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { BlindPay } from "../../client"; +import type { UploadResponse } from "."; + +describe("Upload", () => { + afterEach(() => fetchMock.resetMocks()); + + const blindpay = new BlindPay({ apiKey: "test-key", instanceId: "in_000000000000" }); + + describe("Upload file", () => { + it("should upload a file", async () => { + const mockedUpload: UploadResponse = { + file_url: "https://storage.blindpay.com/uploads/abc123.pdf", + }; + + fetchMock.mockResponseOnce(JSON.stringify(mockedUpload), { + headers: { "Content-Type": "application/json" }, + }); + + const file = new Blob(["test content"], { type: "application/pdf" }); + + const { data, error } = await blindpay.upload.upload({ + file, + bucket: "onboarding", + instance_id: "in_000000000000", + }); + + expect(error).toBeNull(); + expect(data).toEqual(mockedUpload); + }); + + it("should upload a file without instance_id", async () => { + const mockedUpload: UploadResponse = { + file_url: "https://storage.blindpay.com/uploads/abc123.pdf", + }; + + fetchMock.mockResponseOnce(JSON.stringify(mockedUpload), { + headers: { "Content-Type": "application/json" }, + }); + + const file = new Blob(["test content"], { type: "image/png" }); + + const { data, error } = await blindpay.upload.upload({ + file, + bucket: "avatar", + }); + + expect(error).toBeNull(); + expect(data).toEqual(mockedUpload); + }); + }); +}); diff --git a/src/resources/wallets/custodial.test.ts b/src/resources/wallets/custodial.test.ts new file mode 100644 index 0000000..f3998dc --- /dev/null +++ b/src/resources/wallets/custodial.test.ts @@ -0,0 +1,129 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { BlindPay } from "../../client"; +import type { + CreateCustodialWalletResponse, + CustodialWallet, + GetCustodialWalletBalanceResponse, + GetCustodialWalletResponse, + ListCustodialWalletsResponse, +} from "./custodial"; + +const mockWallet: CustodialWallet = { + id: "wa_000000000000", + name: "Main Wallet", + external_id: null, + address: "0xDD6a3aD0949396e57C7738ba8FC1A46A5a1C372C", + network: "polygon", + created_at: "2025-01-01T00:00:00Z", +}; + +describe("Custodial Wallets", () => { + afterEach(() => fetchMock.resetMocks()); + + const blindpay = new BlindPay({ apiKey: "test-key", instanceId: "in_000000000000" }); + + describe("Create custodial wallet", () => { + it("should create a custodial wallet", async () => { + const mockedWallet: CreateCustodialWalletResponse = mockWallet; + + fetchMock.mockResponseOnce(JSON.stringify(mockedWallet), { + headers: { "Content-Type": "application/json" }, + }); + + const { data, error } = await blindpay.wallets.custodial.create({ + receiver_id: "re_000000000000", + name: "Main Wallet", + network: "polygon", + }); + + expect(error).toBeNull(); + expect(data).toEqual(mockedWallet); + }); + }); + + describe("List custodial wallets", () => { + it("should list custodial wallets", async () => { + const mockedWallets: ListCustodialWalletsResponse = [mockWallet]; + + fetchMock.mockResponseOnce(JSON.stringify(mockedWallets), { + headers: { "Content-Type": "application/json" }, + }); + + const { data, error } = await blindpay.wallets.custodial.list("re_000000000000"); + + expect(error).toBeNull(); + expect(data).toEqual(mockedWallets); + }); + }); + + describe("Get custodial wallet", () => { + it("should get a custodial wallet", async () => { + const mockedWallet: GetCustodialWalletResponse = mockWallet; + + fetchMock.mockResponseOnce(JSON.stringify(mockedWallet), { + headers: { "Content-Type": "application/json" }, + }); + + const { data, error } = await blindpay.wallets.custodial.get({ + receiver_id: "re_000000000000", + id: "wa_000000000000", + }); + + expect(error).toBeNull(); + expect(data).toEqual(mockedWallet); + }); + }); + + describe("Get custodial wallet balance", () => { + it("should get wallet balance", async () => { + const mockedBalance: GetCustodialWalletBalanceResponse = { + USDC: { + address: "0xDD6a3aD0949396e57C7738ba8FC1A46A5a1C372C", + id: "tok_000000000001", + symbol: "USDC", + amount: 10000, + }, + USDT: { + address: "0xDD6a3aD0949396e57C7738ba8FC1A46A5a1C372C", + id: "tok_000000000002", + symbol: "USDT", + amount: 5000, + }, + USDB: { + address: "0xDD6a3aD0949396e57C7738ba8FC1A46A5a1C372C", + id: "tok_000000000003", + symbol: "USDB", + amount: 0, + }, + }; + + fetchMock.mockResponseOnce(JSON.stringify(mockedBalance), { + headers: { "Content-Type": "application/json" }, + }); + + const { data, error } = await blindpay.wallets.custodial.getBalance({ + receiver_id: "re_000000000000", + id: "wa_000000000000", + }); + + expect(error).toBeNull(); + expect(data).toEqual(mockedBalance); + }); + }); + + describe("Delete custodial wallet", () => { + it("should delete a custodial wallet", async () => { + fetchMock.mockResponseOnce(JSON.stringify({ success: true }), { + headers: { "Content-Type": "application/json" }, + }); + + const { data, error } = await blindpay.wallets.custodial.delete({ + receiver_id: "re_000000000000", + id: "wa_000000000000", + }); + + expect(error).toBeNull(); + expect(data).toEqual({ success: true }); + }); + }); +}); diff --git a/src/resources/wallets/custodial.ts b/src/resources/wallets/custodial.ts new file mode 100644 index 0000000..b218335 --- /dev/null +++ b/src/resources/wallets/custodial.ts @@ -0,0 +1,94 @@ +import type { BlindpayApiResponse, Network, StablecoinToken } from "../../../types"; +import type { InternalApiClient } from "../../internal/api-client"; + +export type CustodialWallet = { + id: string; + name: string; + external_id: string | null; + address: string | null; + network: Network; + created_at: string; +}; + +export type CreateCustodialWalletInput = { + receiver_id: string; + name: string; + network: Network; + external_id?: string | null; +}; + +export type CreateCustodialWalletResponse = CustodialWallet; + +export type ListCustodialWalletsInput = string; + +export type ListCustodialWalletsResponse = CustodialWallet[]; + +export type GetCustodialWalletInput = { + receiver_id: string; + id: string; +}; + +export type GetCustodialWalletResponse = CustodialWallet; + +export type WalletTokenBalance = { + address: string; + id: string; + symbol: StablecoinToken; + amount: number; +}; + +export type GetCustodialWalletBalanceInput = { + receiver_id: string; + id: string; +}; + +export type GetCustodialWalletBalanceResponse = { + USDC: WalletTokenBalance; + USDT: WalletTokenBalance; + USDB: WalletTokenBalance; +}; + +export type DeleteCustodialWalletInput = { + receiver_id: string; + id: string; +}; + +export function createCustodialWalletsResource(instanceId: string, client: InternalApiClient) { + return { + list( + receiver_id: ListCustodialWalletsInput + ): Promise> { + return client.get(`/instances/${instanceId}/receivers/${receiver_id}/wallets`); + }, + get({ + receiver_id, + id, + }: GetCustodialWalletInput): Promise> { + return client.get(`/instances/${instanceId}/receivers/${receiver_id}/wallets/${id}`); + }, + create({ + receiver_id, + ...data + }: CreateCustodialWalletInput): Promise< + BlindpayApiResponse + > { + return client.post(`/instances/${instanceId}/receivers/${receiver_id}/wallets`, data); + }, + getBalance({ + receiver_id, + id, + }: GetCustodialWalletBalanceInput): Promise< + BlindpayApiResponse + > { + return client.get( + `/instances/${instanceId}/receivers/${receiver_id}/wallets/${id}/balance` + ); + }, + delete({ + receiver_id, + id, + }: DeleteCustodialWalletInput): Promise> { + return client.delete(`/instances/${instanceId}/receivers/${receiver_id}/wallets/${id}`); + }, + }; +} diff --git a/src/resources/webhooks/index.ts b/src/resources/webhooks/index.ts index de0c34d..8af6ee9 100644 --- a/src/resources/webhooks/index.ts +++ b/src/resources/webhooks/index.ts @@ -14,7 +14,16 @@ export type WebhookEvents = | "payin.update" | "payin.complete" | "payin.partnerFee" - | "tos.accept"; + | "tos.accept" + | "limitIncrease.new" + | "limitIncrease.update" + | "virtualAccount.new" + | "virtualAccount.complete" + | "transfer.new" + | "transfer.update" + | "transfer.complete" + | "wallet.new" + | "wallet.inbound"; export type CreateWebhookEndpointInput = { url: string; diff --git a/types/index.d.ts b/types/index.d.ts index fa70da6..391d1e1 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -26,7 +26,7 @@ export type BankAccountType = "checking" | "saving"; export type Currency = "USDC" | "USDT" | "USDB" | "BRL" | "USD" | "MXN" | "COP" | "ARS"; -export type Rail = "wire" | "ach" | "pix" | "spei_bitso" | "transfers_bitso" | "ach_cop_bitso" | "international_swift" | "rtp"; +export type Rail = "wire" | "ach" | "pix" | "pix_safe" | "spei_bitso" | "transfers_bitso" | "ach_cop_bitso" | "international_swift" | "rtp"; export type AccountClass = "individual" | "business"; @@ -284,8 +284,8 @@ export type Country = | "AX"; export type PaginationParams = { - limit?: '10' | '50' | '100' | '200' | '1000'; - offset?: '0' | '10' | '50' | '100' | '200' | '1000'; + limit?: '10' | '50' | '100' | '200' | '500' | '1000'; + offset?: '0' | '10' | '50' | '100' | '200' | '500' | '1000'; starting_after?: string; ending_before?: string; } @@ -347,7 +347,7 @@ export type PayinTrackingPartnerFee = { completed_at?: string | null } -export type PayinPaymentMethod = "ach" | "wire" | "pix" | "spei" | "transfers" | "pse" +export type PayinPaymentMethod = "ach" | "wire" | "pix" | "spei" | "transfers" | "pse" | "international_swift" export type PayerRules = { pix_allowed_tax_ids?: string[] | null; @@ -361,7 +361,7 @@ export type PayerRules = { pse_bank_code?: string | null; } -export type TrackingStatus = 'processing' | 'on_hold' | 'completed'; +export type TrackingStatus = 'processing' | 'on_hold' | 'completed' | 'pending_review'; export type EstimatedTimeOfArrival = '5_min' | '30_min' | '2_hours' | '1_business_day' | '2_business_days' | '5_business_days';