From 34b6b97d0d77b43498610acf84b8f12eea3b0ef6 Mon Sep 17 00:00:00 2001 From: alvseven Date: Thu, 9 Apr 2026 12:41:29 -0300 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20sync=20SDK=20with=20API=20=E2=80=94?= =?UTF-8?q?=20add=20missing=20resources,=20fix=20outdated=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add transfers, custodial wallets, fees, and upload resources. Fix bank account types with new required fields, update receiver endpoints, add payout document submission, and sync webhook events. --- src/client.ts | 15 + src/index.ts | 4 + src/resources/available/available.test.ts | 30 +- src/resources/available/index.ts | 8 + .../bank-accounts/bank-accounts.test.ts | 58 +++- src/resources/bank-accounts/index.ts | 121 ++++++- src/resources/fees/fees.test.ts | 52 +++ src/resources/fees/index.ts | 40 +++ src/resources/payouts/index.ts | 25 +- src/resources/payouts/payouts.test.ts | 24 ++ src/resources/receivers/index.ts | 31 +- src/resources/receivers/receivers.test.ts | 301 +++++++++--------- src/resources/transfers/index.ts | 140 ++++++++ src/resources/transfers/transfers.test.ts | 163 ++++++++++ src/resources/upload/index.ts | 73 +++++ src/resources/upload/upload.test.ts | 52 +++ src/resources/wallets/custodial.test.ts | 129 ++++++++ src/resources/wallets/custodial.ts | 103 ++++++ src/resources/webhooks/index.ts | 11 +- types/index.d.ts | 10 +- 20 files changed, 1217 insertions(+), 173 deletions(-) create mode 100644 src/resources/fees/fees.test.ts create mode 100644 src/resources/fees/index.ts create mode 100644 src/resources/transfers/index.ts create mode 100644 src/resources/transfers/transfers.test.ts create mode 100644 src/resources/upload/index.ts create mode 100644 src/resources/upload/upload.test.ts create mode 100644 src/resources/wallets/custodial.test.ts create mode 100644 src/resources/wallets/custodial.ts 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..e9d7fa9 100644 --- a/src/resources/available/available.test.ts +++ b/src/resources/available/available.test.ts @@ -1,6 +1,6 @@ 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 +95,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..76e7721 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,48 @@ 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..503b266 100644 --- a/src/resources/bank-accounts/index.ts +++ b/src/resources/bank-accounts/index.ts @@ -8,8 +8,29 @@ import type { Rail, } from "../../../types"; import type { InternalApiClient } from "../../internal/api-client"; +import type { BusinessIndustry } from "../receivers"; import type { SpeiProtocol } from "../payouts"; -export type ListBankAccountsInput = string; + +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..99205ba 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,16 @@ 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..1029e88 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 as Record)}` : ""; + 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..21b706a --- /dev/null +++ b/src/resources/transfers/index.ts @@ -0,0 +1,140 @@ +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 as Record)}` + : ""; + 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..f86824e --- /dev/null +++ b/src/resources/upload/index.ts @@ -0,0 +1,73 @@ +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> { + try { + 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, + }; + } catch (error) { + const errorMessage = + error instanceof Error + ? error.message + : error && + typeof error === "object" && + "message" in error && + typeof error.message === "string" + ? error.message + : "Unknown error"; + + return { + data: null, + error: { + message: errorMessage, + }, + }; + } + }, + }; +} 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..a4a92de --- /dev/null +++ b/src/resources/wallets/custodial.ts @@ -0,0 +1,103 @@ +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'; From 74b0e098fc3e2e43019ab463b7d08bbfe6ea4f8c Mon Sep 17 00:00:00 2001 From: alvseven Date: Thu, 9 Apr 2026 13:03:08 -0300 Subject: [PATCH 2/6] fix: remove type assertions, drop upload try/catch, bump to 4.0.0 Replace `as Record` casts with undefined filtering. Remove unnecessary try/catch from upload (errors propagate naturally). Bump version to 4.0.0 per semver (breaking type changes). --- package.json | 2 +- src/resources/bank-accounts/index.ts | 7 +++- src/resources/receivers/index.ts | 7 +++- src/resources/transfers/index.ts | 7 +++- src/resources/upload/index.ts | 59 ++++++++++------------------ 5 files changed, 37 insertions(+), 45 deletions(-) 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/resources/bank-accounts/index.ts b/src/resources/bank-accounts/index.ts index 503b266..91b4359 100644 --- a/src/resources/bank-accounts/index.ts +++ b/src/resources/bank-accounts/index.ts @@ -422,8 +422,11 @@ export function createBankAccountsResource(instanceId: string, client: InternalA receiver_id, ...params }: ListBankAccountsInput): Promise> { - const queryParams = Object.keys(params).length - ? `?${new URLSearchParams(params as Record)}` + const filtered = Object.fromEntries( + Object.entries(params).filter(([, v]) => v !== undefined) + ); + const queryParams = Object.keys(filtered).length + ? `?${new URLSearchParams(filtered)}` : ""; return client.get( `/instances/${instanceId}/receivers/${receiver_id}/bank-accounts${queryParams}` diff --git a/src/resources/receivers/index.ts b/src/resources/receivers/index.ts index 1029e88..8906a0d 100644 --- a/src/resources/receivers/index.ts +++ b/src/resources/receivers/index.ts @@ -526,7 +526,12 @@ export type RequestLimitIncreaseResponse = { export function createReceiversResource(instanceId: string, client: InternalApiClient) { return { list(params?: ListReceiversInput): Promise> { - const queryParams = params ? `?${new URLSearchParams(params as Record)}` : ""; + const filtered = params + ? Object.fromEntries(Object.entries(params).filter(([, v]) => v !== undefined)) + : {}; + const queryParams = Object.keys(filtered).length + ? `?${new URLSearchParams(filtered)}` + : ""; return client.get(`/instances/${instanceId}/receivers${queryParams}`); }, createIndividualWithStandardKYC( diff --git a/src/resources/transfers/index.ts b/src/resources/transfers/index.ts index 21b706a..508a1b1 100644 --- a/src/resources/transfers/index.ts +++ b/src/resources/transfers/index.ts @@ -121,8 +121,11 @@ export function createTransfersResource(instanceId: string, client: InternalApiC list( params?: ListTransfersInput ): Promise> { - const queryParams = params - ? `?${new URLSearchParams(params as Record)}` + const filtered = params + ? Object.fromEntries(Object.entries(params).filter(([, v]) => v !== undefined)) + : {}; + const queryParams = Object.keys(filtered).length + ? `?${new URLSearchParams(filtered)}` : ""; return client.get(`/instances/${instanceId}/transfers${queryParams}`); }, diff --git a/src/resources/upload/index.ts b/src/resources/upload/index.ts index f86824e..4b53517 100644 --- a/src/resources/upload/index.ts +++ b/src/resources/upload/index.ts @@ -19,55 +19,36 @@ export function createUploadResource(baseUrl: string, headers: Record> { - try { - const formData = new FormData(); - formData.append("file", file); - formData.append("bucket", bucket); + const formData = new FormData(); + formData.append("file", file); + formData.append("bucket", bucket); - const queryParams = instance_id ? `?instance_id=${instance_id}` : ""; + const queryParams = instance_id ? `?instance_id=${instance_id}` : ""; - const { "Content-Type": _, ...headersWithoutContentType } = headers; + 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, - }; - } catch (error) { - const errorMessage = - error instanceof Error - ? error.message - : error && - typeof error === "object" && - "message" in error && - typeof error.message === "string" - ? error.message - : "Unknown error"; + 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: errorMessage, + message: error?.message ?? "Unknown error", }, }; } + + const data = await response.json(); + + return { + data, + error: null, + }; }, }; } From a36b8b05c2c5738fad3eb14ab2c9746c93daf4b9 Mon Sep 17 00:00:00 2001 From: alvseven Date: Thu, 9 Apr 2026 13:13:08 -0300 Subject: [PATCH 3/6] refactor: extract buildQueryString utility for all list endpoints Replace inline URLSearchParams logic (assertions, filters, inconsistent patterns) with a shared helper that handles undefined values correctly. --- src/internal/query-string.ts | 7 +++++++ src/resources/bank-accounts/index.ts | 9 ++------- src/resources/payins/index.ts | 4 ++-- src/resources/payouts/index.ts | 4 ++-- src/resources/receivers/index.ts | 9 ++------- src/resources/transfers/index.ts | 9 ++------- 6 files changed, 17 insertions(+), 25 deletions(-) create mode 100644 src/internal/query-string.ts diff --git a/src/internal/query-string.ts b/src/internal/query-string.ts new file mode 100644 index 0000000..5b65539 --- /dev/null +++ b/src/internal/query-string.ts @@ -0,0 +1,7 @@ +export function buildQueryString(params: Record): string { + const searchParams = new URLSearchParams(); + for (const [key, value] of Object.entries(params)) { + if (value != null) searchParams.set(key, value); + } + return searchParams.size ? `?${searchParams}` : ""; +} diff --git a/src/resources/bank-accounts/index.ts b/src/resources/bank-accounts/index.ts index 91b4359..cba59f5 100644 --- a/src/resources/bank-accounts/index.ts +++ b/src/resources/bank-accounts/index.ts @@ -8,6 +8,7 @@ import type { Rail, } from "../../../types"; import type { InternalApiClient } from "../../internal/api-client"; +import { buildQueryString } from "../../internal/query-string"; import type { BusinessIndustry } from "../receivers"; import type { SpeiProtocol } from "../payouts"; @@ -422,14 +423,8 @@ export function createBankAccountsResource(instanceId: string, client: InternalA receiver_id, ...params }: ListBankAccountsInput): Promise> { - const filtered = Object.fromEntries( - Object.entries(params).filter(([, v]) => v !== undefined) - ); - const queryParams = Object.keys(filtered).length - ? `?${new URLSearchParams(filtered)}` - : ""; return client.get( - `/instances/${instanceId}/receivers/${receiver_id}/bank-accounts${queryParams}` + `/instances/${instanceId}/receivers/${receiver_id}/bank-accounts${buildQueryString(params)}` ); }, get({ diff --git a/src/resources/payins/index.ts b/src/resources/payins/index.ts index c4d2cde..7b5679b 100644 --- a/src/resources/payins/index.ts +++ b/src/resources/payins/index.ts @@ -16,6 +16,7 @@ import type { TransactionStatus, } from "../../../types"; import type { InternalApiClient } from "../../internal/api-client"; +import { buildQueryString } from "../../internal/query-string"; export type BlindpayBankDetails = { routing_number: string; @@ -132,8 +133,7 @@ export type CreateEvmPayinResponse = Pick< export function createPayinsResource(instanceId: string, client: InternalApiClient) { return { list(params?: ListPayinsInput): Promise> { - const queryParams = params ? `?${new URLSearchParams(params)}` : ""; - return client.get(`/instances/${instanceId}/payins${queryParams}`); + return client.get(`/instances/${instanceId}/payins${buildQueryString(params ?? {})}`); }, get(payinId: GetPayinInput): Promise> { return client.get(`/instances/${instanceId}/payins/${payinId}`); diff --git a/src/resources/payouts/index.ts b/src/resources/payouts/index.ts index 99205ba..e0ce008 100644 --- a/src/resources/payouts/index.ts +++ b/src/resources/payouts/index.ts @@ -18,6 +18,7 @@ import type { TransactionStatus, } from "../../../types"; import type { InternalApiClient } from "../../internal/api-client"; +import { buildQueryString } from "../../internal/query-string"; export type SpeiProtocol = "clabe" | "debitcard" | "phonenum"; @@ -192,8 +193,7 @@ export type CreateSolanaPayoutResponse = { export function createPayoutsResource(instanceId: string, client: InternalApiClient) { return { list(params?: ListPayoutsInput): Promise> { - const queryParams = params ? `?${new URLSearchParams(params)}` : ""; - return client.get(`/instances/${instanceId}/payouts${queryParams}`); + return client.get(`/instances/${instanceId}/payouts${buildQueryString(params ?? {})}`); }, get(payoutId: GetPayoutInput): Promise> { return client.get(`/instances/${instanceId}/payouts/${payoutId}`); diff --git a/src/resources/receivers/index.ts b/src/resources/receivers/index.ts index 8906a0d..58b507e 100644 --- a/src/resources/receivers/index.ts +++ b/src/resources/receivers/index.ts @@ -7,6 +7,7 @@ import type { } from "../../../types"; import type { InternalApiClient } from "../../internal/api-client"; import type { StrictOmit } from "../../internal/helpers/strict-omit"; +import { buildQueryString } from "../../internal/query-string"; export type ProofOfAddressDocType = | "UTILITY_BILL" @@ -526,13 +527,7 @@ export type RequestLimitIncreaseResponse = { export function createReceiversResource(instanceId: string, client: InternalApiClient) { return { list(params?: ListReceiversInput): Promise> { - const filtered = params - ? Object.fromEntries(Object.entries(params).filter(([, v]) => v !== undefined)) - : {}; - const queryParams = Object.keys(filtered).length - ? `?${new URLSearchParams(filtered)}` - : ""; - return client.get(`/instances/${instanceId}/receivers${queryParams}`); + return client.get(`/instances/${instanceId}/receivers${buildQueryString(params ?? {})}`); }, createIndividualWithStandardKYC( data: CreateIndividualWithStandardKYCInput diff --git a/src/resources/transfers/index.ts b/src/resources/transfers/index.ts index 508a1b1..5d3986f 100644 --- a/src/resources/transfers/index.ts +++ b/src/resources/transfers/index.ts @@ -8,6 +8,7 @@ import type { TransactionStatus, } from "../../../types"; import type { InternalApiClient } from "../../internal/api-client"; +import { buildQueryString } from "../../internal/query-string"; export type TransferTrackingStep = { step: TrackingStatus; @@ -121,13 +122,7 @@ export function createTransfersResource(instanceId: string, client: InternalApiC list( params?: ListTransfersInput ): Promise> { - const filtered = params - ? Object.fromEntries(Object.entries(params).filter(([, v]) => v !== undefined)) - : {}; - const queryParams = Object.keys(filtered).length - ? `?${new URLSearchParams(filtered)}` - : ""; - return client.get(`/instances/${instanceId}/transfers${queryParams}`); + return client.get(`/instances/${instanceId}/transfers${buildQueryString(params ?? {})}`); }, get( transferId: GetTransferInput From 9685a18c155fcfca73cf472719e86a1149f55b26 Mon Sep 17 00:00:00 2001 From: alvseven Date: Thu, 9 Apr 2026 13:15:52 -0300 Subject: [PATCH 4/6] revert: remove buildQueryString utility, use inline pattern Match existing codebase convention for query param handling. --- src/internal/query-string.ts | 7 ------- src/resources/bank-accounts/index.ts | 6 ++++-- src/resources/payins/index.ts | 4 ++-- src/resources/payouts/index.ts | 4 ++-- src/resources/receivers/index.ts | 4 ++-- src/resources/transfers/index.ts | 4 ++-- 6 files changed, 12 insertions(+), 17 deletions(-) delete mode 100644 src/internal/query-string.ts diff --git a/src/internal/query-string.ts b/src/internal/query-string.ts deleted file mode 100644 index 5b65539..0000000 --- a/src/internal/query-string.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function buildQueryString(params: Record): string { - const searchParams = new URLSearchParams(); - for (const [key, value] of Object.entries(params)) { - if (value != null) searchParams.set(key, value); - } - return searchParams.size ? `?${searchParams}` : ""; -} diff --git a/src/resources/bank-accounts/index.ts b/src/resources/bank-accounts/index.ts index cba59f5..503b266 100644 --- a/src/resources/bank-accounts/index.ts +++ b/src/resources/bank-accounts/index.ts @@ -8,7 +8,6 @@ import type { Rail, } from "../../../types"; import type { InternalApiClient } from "../../internal/api-client"; -import { buildQueryString } from "../../internal/query-string"; import type { BusinessIndustry } from "../receivers"; import type { SpeiProtocol } from "../payouts"; @@ -423,8 +422,11 @@ export function createBankAccountsResource(instanceId: string, client: InternalA 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${buildQueryString(params)}` + `/instances/${instanceId}/receivers/${receiver_id}/bank-accounts${queryParams}` ); }, get({ diff --git a/src/resources/payins/index.ts b/src/resources/payins/index.ts index 7b5679b..c4d2cde 100644 --- a/src/resources/payins/index.ts +++ b/src/resources/payins/index.ts @@ -16,7 +16,6 @@ import type { TransactionStatus, } from "../../../types"; import type { InternalApiClient } from "../../internal/api-client"; -import { buildQueryString } from "../../internal/query-string"; export type BlindpayBankDetails = { routing_number: string; @@ -133,7 +132,8 @@ export type CreateEvmPayinResponse = Pick< export function createPayinsResource(instanceId: string, client: InternalApiClient) { return { list(params?: ListPayinsInput): Promise> { - return client.get(`/instances/${instanceId}/payins${buildQueryString(params ?? {})}`); + const queryParams = params ? `?${new URLSearchParams(params)}` : ""; + return client.get(`/instances/${instanceId}/payins${queryParams}`); }, get(payinId: GetPayinInput): Promise> { return client.get(`/instances/${instanceId}/payins/${payinId}`); diff --git a/src/resources/payouts/index.ts b/src/resources/payouts/index.ts index e0ce008..99205ba 100644 --- a/src/resources/payouts/index.ts +++ b/src/resources/payouts/index.ts @@ -18,7 +18,6 @@ import type { TransactionStatus, } from "../../../types"; import type { InternalApiClient } from "../../internal/api-client"; -import { buildQueryString } from "../../internal/query-string"; export type SpeiProtocol = "clabe" | "debitcard" | "phonenum"; @@ -193,7 +192,8 @@ export type CreateSolanaPayoutResponse = { export function createPayoutsResource(instanceId: string, client: InternalApiClient) { return { list(params?: ListPayoutsInput): Promise> { - return client.get(`/instances/${instanceId}/payouts${buildQueryString(params ?? {})}`); + const queryParams = params ? `?${new URLSearchParams(params)}` : ""; + return client.get(`/instances/${instanceId}/payouts${queryParams}`); }, get(payoutId: GetPayoutInput): Promise> { return client.get(`/instances/${instanceId}/payouts/${payoutId}`); diff --git a/src/resources/receivers/index.ts b/src/resources/receivers/index.ts index 58b507e..b838307 100644 --- a/src/resources/receivers/index.ts +++ b/src/resources/receivers/index.ts @@ -7,7 +7,6 @@ import type { } from "../../../types"; import type { InternalApiClient } from "../../internal/api-client"; import type { StrictOmit } from "../../internal/helpers/strict-omit"; -import { buildQueryString } from "../../internal/query-string"; export type ProofOfAddressDocType = | "UTILITY_BILL" @@ -527,7 +526,8 @@ export type RequestLimitIncreaseResponse = { export function createReceiversResource(instanceId: string, client: InternalApiClient) { return { list(params?: ListReceiversInput): Promise> { - return client.get(`/instances/${instanceId}/receivers${buildQueryString(params ?? {})}`); + const queryParams = params ? `?${new URLSearchParams(params)}` : ""; + return client.get(`/instances/${instanceId}/receivers${queryParams}`); }, createIndividualWithStandardKYC( data: CreateIndividualWithStandardKYCInput diff --git a/src/resources/transfers/index.ts b/src/resources/transfers/index.ts index 5d3986f..4371433 100644 --- a/src/resources/transfers/index.ts +++ b/src/resources/transfers/index.ts @@ -8,7 +8,6 @@ import type { TransactionStatus, } from "../../../types"; import type { InternalApiClient } from "../../internal/api-client"; -import { buildQueryString } from "../../internal/query-string"; export type TransferTrackingStep = { step: TrackingStatus; @@ -122,7 +121,8 @@ export function createTransfersResource(instanceId: string, client: InternalApiC list( params?: ListTransfersInput ): Promise> { - return client.get(`/instances/${instanceId}/transfers${buildQueryString(params ?? {})}`); + const queryParams = params ? `?${new URLSearchParams(params)}` : ""; + return client.get(`/instances/${instanceId}/transfers${queryParams}`); }, get( transferId: GetTransferInput From c684f8cafa20fc07798dca67cd6048f35c3b0041 Mon Sep 17 00:00:00 2001 From: alvseven Date: Thu, 9 Apr 2026 17:03:37 -0300 Subject: [PATCH 5/6] fix: resolve lint formatting and import sort order issues Add changeset for v4 SDK updates. --- .changeset/sdk-v4-update.md | 5 +++++ src/resources/available/available.test.ts | 7 ++++++- .../bank-accounts/bank-accounts.test.ts | 4 +++- src/resources/bank-accounts/index.ts | 2 +- src/resources/payouts/index.ts | 5 +---- src/resources/transfers/index.ts | 8 ++------ src/resources/wallets/custodial.ts | 17 ++++------------- 7 files changed, 22 insertions(+), 26 deletions(-) create mode 100644 .changeset/sdk-v4-update.md diff --git a/.changeset/sdk-v4-update.md b/.changeset/sdk-v4-update.md new file mode 100644 index 0000000..41ee16c --- /dev/null +++ b/.changeset/sdk-v4-update.md @@ -0,0 +1,5 @@ +--- +"@blindpay/node": minor +--- + +Add transfers, custodial wallets, and fees resources. Update bank account inputs with new required fields. Add pix_safe rail, solana networks, payout document submission, NAICS codes, and 10 new webhook events. Fix receiver update to use PUT. diff --git a/src/resources/available/available.test.ts b/src/resources/available/available.test.ts index e9d7fa9..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, GetNaicsCodesResponse, GetRailsResponse, GetSwiftCodeBankDetailsResponse } from "."; +import type { + GetBankDetailsResponse, + GetNaicsCodesResponse, + GetRailsResponse, + GetSwiftCodeBankDetailsResponse, +} from "."; describe("Available", () => { afterEach(() => fetchMock.resetMocks()); diff --git a/src/resources/bank-accounts/bank-accounts.test.ts b/src/resources/bank-accounts/bank-accounts.test.ts index 76e7721..bf3371c 100644 --- a/src/resources/bank-accounts/bank-accounts.test.ts +++ b/src/resources/bank-accounts/bank-accounts.test.ts @@ -467,7 +467,9 @@ describe("Bank accounts", () => { headers: { "Content-Type": "application/json" }, }); - const { data, error } = await blindpay.receivers.bankAccounts.list({ receiver_id: "re_000000000000" }); + const { data, error } = await blindpay.receivers.bankAccounts.list({ + receiver_id: "re_000000000000", + }); expect(error).toBeNull(); expect(data).toEqual(mockedBankAccounts); diff --git a/src/resources/bank-accounts/index.ts b/src/resources/bank-accounts/index.ts index 503b266..802b68b 100644 --- a/src/resources/bank-accounts/index.ts +++ b/src/resources/bank-accounts/index.ts @@ -8,8 +8,8 @@ import type { Rail, } from "../../../types"; import type { InternalApiClient } from "../../internal/api-client"; -import type { BusinessIndustry } from "../receivers"; import type { SpeiProtocol } from "../payouts"; +import type { BusinessIndustry } from "../receivers"; export type RecipientRelationship = | "first_party" diff --git a/src/resources/payouts/index.ts b/src/resources/payouts/index.ts index 99205ba..2501c97 100644 --- a/src/resources/payouts/index.ts +++ b/src/resources/payouts/index.ts @@ -231,10 +231,7 @@ export function createPayoutsResource(instanceId: string, client: InternalApiCli }: SubmitPayoutDocumentsInput): Promise< BlindpayApiResponse > { - return client.post( - `/instances/${instanceId}/payouts/${payout_id}/documents`, - data - ); + return client.post(`/instances/${instanceId}/payouts/${payout_id}/documents`, data); }, }; } diff --git a/src/resources/transfers/index.ts b/src/resources/transfers/index.ts index 4371433..52a309c 100644 --- a/src/resources/transfers/index.ts +++ b/src/resources/transfers/index.ts @@ -118,15 +118,11 @@ export function createTransfersResource(instanceId: string, client: InternalApiC }: CreateTransferInput): Promise> { return client.post(`/instances/${instanceId}/transfers`, data); }, - list( - params?: ListTransfersInput - ): Promise> { + list(params?: ListTransfersInput): Promise> { const queryParams = params ? `?${new URLSearchParams(params)}` : ""; return client.get(`/instances/${instanceId}/transfers${queryParams}`); }, - get( - transferId: GetTransferInput - ): Promise> { + get(transferId: GetTransferInput): Promise> { return client.get(`/instances/${instanceId}/transfers/${transferId}`); }, getTrack( diff --git a/src/resources/wallets/custodial.ts b/src/resources/wallets/custodial.ts index a4a92de..b218335 100644 --- a/src/resources/wallets/custodial.ts +++ b/src/resources/wallets/custodial.ts @@ -58,17 +58,13 @@ export function createCustodialWalletsResource(instanceId: string, client: Inter list( receiver_id: ListCustodialWalletsInput ): Promise> { - return client.get( - `/instances/${instanceId}/receivers/${receiver_id}/wallets` - ); + 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}` - ); + return client.get(`/instances/${instanceId}/receivers/${receiver_id}/wallets/${id}`); }, create({ receiver_id, @@ -76,10 +72,7 @@ export function createCustodialWalletsResource(instanceId: string, client: Inter }: CreateCustodialWalletInput): Promise< BlindpayApiResponse > { - return client.post( - `/instances/${instanceId}/receivers/${receiver_id}/wallets`, - data - ); + return client.post(`/instances/${instanceId}/receivers/${receiver_id}/wallets`, data); }, getBalance({ receiver_id, @@ -95,9 +88,7 @@ export function createCustodialWalletsResource(instanceId: string, client: Inter receiver_id, id, }: DeleteCustodialWalletInput): Promise> { - return client.delete( - `/instances/${instanceId}/receivers/${receiver_id}/wallets/${id}` - ); + return client.delete(`/instances/${instanceId}/receivers/${receiver_id}/wallets/${id}`); }, }; } From 0540e357fe94af4fd5758ef1ecc55fd5fab5f97d Mon Sep 17 00:00:00 2001 From: alvseven Date: Thu, 9 Apr 2026 18:09:49 -0300 Subject: [PATCH 6/6] chore: remove changeset file --- .changeset/sdk-v4-update.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/sdk-v4-update.md diff --git a/.changeset/sdk-v4-update.md b/.changeset/sdk-v4-update.md deleted file mode 100644 index 41ee16c..0000000 --- a/.changeset/sdk-v4-update.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@blindpay/node": minor ---- - -Add transfers, custodial wallets, and fees resources. Update bank account inputs with new required fields. Add pix_safe rail, solana networks, payout document submission, NAICS codes, and 10 new webhook events. Fix receiver update to use PUT.