Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 2 additions & 9 deletions examples/accounts/0-register-new-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,8 @@ const bloque = new SDK({
await bloque.register('@nestor', {
type: 'individual',
profile: {
firstName: 'Nestor',
lastName: 'Nestor',
// Basic profile is supported: at least one of email or phone.
email: 'nestor@example.com',
phone: '+1234567890',
birthdate: '1990-01-01',
city: 'Mexico City',
state: 'Mexico',
postalCode: '10001',
countryOfBirthCode: 'MX',
countryOfResidenceCode: 'MX',
firstName: 'Nestor',
},
});
74 changes: 8 additions & 66 deletions examples/accounts/external-us-bank-hosted-plaid-link.ts
Original file line number Diff line number Diff line change
@@ -1,78 +1,20 @@
import { SDK } from '../../packages/sdk/src';

/**
* External US bank → hosted Plaid Link page.
*
* Same end goal as `external-us-bank-ach-kusama.ts`, but the user finishes
* linking on a Bloque-hosted page instead of inside the caller's frontend.
*
* Server flow:
* 1. Pass `returnUrl` (and optionally `state`) to `externalUsBank.create()` —
* serialized as `input.return_url` / `input.state` on the mediums API.
* 2. The server mints a short-lived `plaid-link` JWT, builds a hosted URL,
* and returns it as `details.linkUrl`.
* 3. Send the user to `details.linkUrl` (redirect, email, deep link...).
* 4. The hosted page runs Plaid Link, exchanges `public_token` on behalf of
* the user, then redirects to `returnUrl?status=success&state=<state>`
* (or `status=cancelled` / `status=error`).
* 5. Read final state with `accounts.get(urn)` when the user returns.
*
* The `returnUrl` origin must be in the server's `PLAID_LINK_RETURN_URL_ALLOWLIST`.
*/

const bloque = new SDK({
origin: process.env.ORIGIN!,
origin: 'ktg',
auth: {
type: 'originKey',
originKey: process.env.ORIGIN_KEY!,
originKey: 'sk_live_your_origin_key_here',
},
mode: 'sandbox',
baseUrl: 'https://api.bloque.sh',
platform: 'node',
});

const user = await bloque.connect('@nestor4');

console.log({
profile: await user.identity.get(user.urn!),
});
const user = await bloque.connect('cus_kr7rr5bfzr6igm7');

const bank = await user.accounts.externalUsBank.create({
holderUrn: user.urn,
ledgerId:
'0x8a3035ae6d8e9fd867694494d269e94cfa389d17491c63730ed3ee5fb150d251',
label: 'ACH on-ramp',
returnUrl: 'https://app.example.com/wallet/plaid-return',
state: 'user-session-xyz',
const bank = await user.accounts.list({
medium: 'external-us-bank',
urn: 'did:bloque:account:external-us-bank:5499a525-b415-4861-b978-0f5afd605ec8',
});

if (!bank.details.linkUrl) {
throw new Error(
'Expected details.linkUrl. Check that returnUrl origin is allowlisted.',
);
}

console.log('Account URN: ', bank.urn);
console.log('Hosted Plaid Link URL: ', bank.details.linkUrl);
console.log('linkToken expires at: ', bank.details.linkTokenExpiration);

// After the user returns to returnUrl?status=success&state=user-session-xyz:
// const linked = await user.accounts.get(bank.urn);
//
// // Narrow the MappedAccount union to the external-us-bank shape:
// if (!('linkStatus' in linked.details)) {
// throw new Error('Not an external-us-bank account');
// }
//
// console.log(linked.details.linkStatus); // 'active' | 'pending_link' | ...
// console.log(linked.details.bankName); // Plaid-reported institution
// console.log(linked.details.bankAccountLast4); // last 4 digits
//
// // ── Brale address enrichment (best-effort, populated post-link) ──
// console.log(linked.details.owner); // beneficiary name on file
// console.log(linked.details.routingNumber); // ABA routing number
// console.log(linked.details.accountType); // 'checking' | 'savings'
// console.log(linked.details.transferTypes); // ['ach_debit', 'rtp_credit', ...]
// console.log(linked.details.needsUpdate); // true → user must redo Plaid Link
// console.log(linked.details.lastUpdated); // ISO 8601 from Brale
// console.log(linked.details.bankAddress); // { streetLine1, city, state, zip, ... }
// console.log(linked.details.beneficiaryAddress); // { streetLine1, city, state, zip, ... }
console.log(JSON.stringify(bank, null, 2));
2 changes: 1 addition & 1 deletion packages/accounts/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@bloque/sdk-accounts",
"version": "0.1.13",
"version": "0.1.14",
"type": "module",
"keywords": [
"bloque",
Expand Down
14 changes: 6 additions & 8 deletions packages/accounts/src/accounts-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
mapExternalUsBankAccountFromWire,
} from './external-us-bank/external-us-bank-client';
import type { ExternalUsBankAccount } from './external-us-bank/types';
import { buildAccountListQuery } from './internal/build-account-list-query';
import type {
AccountWithBalance,
BancolombiaDetails,
Expand Down Expand Up @@ -93,6 +94,7 @@ export type MappedAccount =
* - bancolombia: Bancolombia accounts
* - breb: BRE-B key accounts
* - us: US bank accounts
* - externalUsBank: External US bank accounts (Plaid / Brale linkage)
* - polygon: Polygon wallets
*/
export class AccountsClient extends BaseClient {
Expand Down Expand Up @@ -221,14 +223,10 @@ export class AccountsClient extends BaseClient {
* ```
*/
async list(params?: ListAccountsParams): Promise<ListAccountsResult> {
const holderUrn = params?.holderUrn || this.httpClient.urn;

const queryParams = new URLSearchParams();
if (holderUrn) {
queryParams.append('holder_urn', holderUrn);
}

const queryString = queryParams.toString();
const queryString = buildAccountListQuery({
...params,
holderUrn: params?.holderUrn || this.httpClient.urn,
});
const path = queryString ? `/api/accounts?${queryString}` : '/api/accounts';

const response = await this.httpClient.request<ListAccountsResponse>({
Expand Down
21 changes: 6 additions & 15 deletions packages/accounts/src/bancolombia/bancolombia-client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { BaseClient } from '@bloque/sdk-core';
import { buildAccountListQuery } from '../internal/build-account-list-query';
import type {
AccountStatus,
AccountWithBalance,
Expand Down Expand Up @@ -74,26 +75,16 @@ export class BancolombiaClient extends BaseClient {
async list(
params?: ListBancolombiaAccountsParams,
): Promise<ListBancolombiaAccountsResult> {
const holderUrn = params?.holderUrn || this.httpClient.urn;

const queryParams = new URLSearchParams();
queryParams.append('medium', 'bancolombia');

if (holderUrn) {
queryParams.append('holder_urn', holderUrn);
}

if (params?.urn) {
queryParams.append('urn', params.urn);
}

const path = `/api/accounts?${queryParams.toString()}`;
const queryString = buildAccountListQuery(
{ ...params, holderUrn: params?.holderUrn || this.httpClient.urn },
'bancolombia',
);

const response = await this.httpClient.request<
ListAccountsResponse<BancolombiaDetails>
>({
method: 'GET',
path,
path: `/api/accounts?${queryString}`,
});

return {
Expand Down
17 changes: 4 additions & 13 deletions packages/accounts/src/bancolombia/types.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,10 @@
import type { ListAccountsFilterParams } from '../types';

/**
* Parameters for listing Bancolombia accounts
*/
export interface ListBancolombiaAccountsParams {
/**
* URN of the account holder (user or organization) to filter by
* @example "did:bloque:bloque-root:nestor"
*/
holderUrn?: string;

/**
* URN of a specific Bancolombia account to retrieve
* @example "did:bloque:account:bancolombia:abc-123"
*/
urn?: string;
}
export interface ListBancolombiaAccountsParams
extends Omit<ListAccountsFilterParams, 'medium'> {}

/**
* Result of listing Bancolombia accounts
Expand Down
21 changes: 6 additions & 15 deletions packages/accounts/src/card/card-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
isSupportedAsset,
SUPPORTED_ASSETS,
} from '@bloque/sdk-core';
import { buildAccountListQuery } from '../internal/build-account-list-query';
import type {
AccountStatus,
AccountWithBalance,
Expand Down Expand Up @@ -189,26 +190,16 @@ export class CardClient extends BaseClient {
* ```
*/
async list(params?: ListCardAccountsParams): Promise<ListCardAccountsResult> {
const holderUrn = params?.holderUrn || this.httpClient.urn;

const queryParams = new URLSearchParams();
queryParams.append('medium', 'card');

if (holderUrn) {
queryParams.append('holder_urn', holderUrn);
}

if (params?.urn) {
queryParams.append('urn', params.urn);
}

const path = `/api/accounts?${queryParams.toString()}`;
const queryString = buildAccountListQuery(
{ ...params, holderUrn: params?.holderUrn || this.httpClient.urn },
'card',
);

const response = await this.httpClient.request<
ListAccountsResponse<CardDetails>
>({
method: 'GET',
path,
path: `/api/accounts?${queryString}`,
});

return {
Expand Down
17 changes: 3 additions & 14 deletions packages/accounts/src/card/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,13 @@ import type {
TokenBalance,
Transaction,
} from '../internal/wire-types';
import type { SupportedAsset } from '../types';
import type { ListAccountsFilterParams, SupportedAsset } from '../types';

/**
* Parameters for listing card accounts
*/
export interface ListCardAccountsParams {
/**
* URN of the account holder (user or organization) to filter by
* @example "did:bloque:bloque-root:nestor"
*/
holderUrn?: string;

/**
* URN of a specific card account to retrieve
* @example "did:bloque:account:card:usr-123:crd-456"
*/
urn?: string;
}
export interface ListCardAccountsParams
extends Omit<ListAccountsFilterParams, 'medium'> {}

/**
* Result of listing card accounts
Expand Down
75 changes: 51 additions & 24 deletions packages/accounts/src/external-us-bank/external-us-bank-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
CreateExternalUsBankAccountParams,
ExchangeExternalUsBankPublicTokenParams,
ExternalUsBankAccount,
ExternalUsBankAccountDetails,
ExternalUsBankBankAddress,
PullExternalUsBankParams,
PullExternalUsBankResult,
Expand Down Expand Up @@ -59,6 +60,55 @@ type PullExternalUsBankResponse = {
req_id?: string;
};

function mapExternalUsBankDetailsFromWire(
details: ExternalUsBankDetails,
): ExternalUsBankAccountDetails {
const common = {
id: details.id,
braleAccountId: details.brale_account_id,
braleAddressId: details.brale_address_id,
bankAccountLast4: details.bank_account_last4,
bankName: details.bank_name,
};

switch (details.link_status) {
case 'pending_link':
return {
...common,
linkStatus: 'pending_link',
linkToken: details.link_token,
linkTokenExpiration: details.link_token_expiration,
linkUrl: details.link_url,
jwt: details.jwt,
};
case 'active':
return {
...common,
linkStatus: 'active',
owner: details.owner,
routingNumber: details.routing_number,
accountNumber: details.account_number,
accountType: details.account_type,
bankAddress: mapBankAddressFromWire(details.bank_address),
beneficiaryAddress: mapBankAddressFromWire(details.beneficiary_address),
transferTypes: details.transfer_types,
needsUpdate: details.needs_update,
lastUpdated: details.last_updated,
};
case 'link_failed':
return {
...common,
linkStatus: 'link_failed',
failureReason: details.failure_reason,
};
case 'closed':
return {
...common,
linkStatus: 'closed',
};
}
}

export function mapExternalUsBankAccountFromWire(
account: AccountWithBalance<ExternalUsBankDetails>,
): ExternalUsBankAccount {
Expand All @@ -73,30 +123,7 @@ export function mapExternalUsBankAccountFromWire(
createdAt: account.created_at,
updatedAt: account.updated_at,
balance: account.balance,
details: {
id: account.details.id,
linkStatus: account.details.link_status,
braleAccountId: account.details.brale_account_id,
braleAddressId: account.details.brale_address_id,
linkToken: account.details.link_token,
linkTokenExpiration: account.details.link_token_expiration,
linkUrl: account.details.link_url,
jwt: account.details.jwt,
bankAccountLast4: account.details.bank_account_last4,
bankName: account.details.bank_name,
failureReason: account.details.failure_reason,
owner: account.details.owner,
routingNumber: account.details.routing_number,
accountNumber: account.details.account_number,
accountType: account.details.account_type,
bankAddress: mapBankAddressFromWire(account.details.bank_address),
beneficiaryAddress: mapBankAddressFromWire(
account.details.beneficiary_address,
),
transferTypes: account.details.transfer_types,
needsUpdate: account.details.needs_update,
lastUpdated: account.details.last_updated,
},
details: mapExternalUsBankDetailsFromWire(account.details),
};
}

Expand Down
Loading