From 2d0ece682e45811d34ceb520b52527cd27835076 Mon Sep 17 00:00:00 2001 From: "Darlan Jr." Date: Wed, 30 Sep 2026 13:57:11 -0400 Subject: [PATCH 1/2] feat(wallets): add self-custody support to blockchain wallets Add is_self_custody to blockchain wallet responses and as an optional input on createWithAddress/createWithHash, a setSelfCustody method (PATCH on the customers route), and the blockchainWallet.update webhook event. --- .api-sync/contract-check-allowlist.json | 36 ++++++++++++++++ .changeset/blockchain-wallet-self-custody.md | 5 +++ src/resources/wallets/blockchain.test.ts | 45 ++++++++++++++++++++ src/resources/wallets/blockchain.ts | 25 +++++++++++ src/resources/webhooks/index.ts | 1 + 5 files changed, 112 insertions(+) create mode 100644 .changeset/blockchain-wallet-self-custody.md diff --git a/.api-sync/contract-check-allowlist.json b/.api-sync/contract-check-allowlist.json index e7ac78d..d2ff7b9 100644 --- a/.api-sync/contract-check-allowlist.json +++ b/.api-sync/contract-check-allowlist.json @@ -166,5 +166,41 @@ "field": "payout_id", "reason": "Path parameter (the spec names it id) modeled on the request input type for URL building; never a wire body property. Pre-existing modeling choice, unrelated to the customers rename.", "owner": "eric@blindpay.com" + }, + { + "schema": "ListBlockchainWalletsResponse", + "field": "is_self_custody", + "reason": "Added by blindpay-v2 PR #2632 (BCB Resolution 588 self-custody flag); not in the spec snapshot until that ships and api-sync refreshes it. Remove once the snapshot contains is_self_custody.", + "owner": "darlan@blindpay.com" + }, + { + "schema": "CreateBlockchainWalletWithAddressInput", + "field": "is_self_custody", + "reason": "Added by blindpay-v2 PR #2632 (BCB Resolution 588 self-custody flag); not in the spec snapshot until that ships and api-sync refreshes it. Remove once the snapshot contains is_self_custody.", + "owner": "darlan@blindpay.com" + }, + { + "schema": "CreateBlockchainWalletWithHashInput", + "field": "is_self_custody", + "reason": "Added by blindpay-v2 PR #2632 (BCB Resolution 588 self-custody flag); not in the spec snapshot until that ships and api-sync refreshes it. Remove once the snapshot contains is_self_custody.", + "owner": "darlan@blindpay.com" + }, + { + "schema": "GetBlockchainWalletResponse", + "field": "is_self_custody", + "reason": "Added by blindpay-v2 PR #2632 (BCB Resolution 588 self-custody flag); not in the spec snapshot until that ships and api-sync refreshes it. Remove once the snapshot contains is_self_custody.", + "owner": "darlan@blindpay.com" + }, + { + "schema": "CreateBlockchainWalletResponse", + "field": "is_self_custody", + "reason": "Added by blindpay-v2 PR #2632 (BCB Resolution 588 self-custody flag); not in the spec snapshot until that ships and api-sync refreshes it. Remove once the snapshot contains is_self_custody.", + "owner": "darlan@blindpay.com" + }, + { + "schema": "SetBlockchainWalletSelfCustodyInput", + "field": "is_self_custody", + "reason": "Added by blindpay-v2 PR #2632 (BCB Resolution 588 self-custody flag); not in the spec snapshot until that ships and api-sync refreshes it. Remove once the snapshot contains is_self_custody.", + "owner": "darlan@blindpay.com" } ] diff --git a/.changeset/blockchain-wallet-self-custody.md b/.changeset/blockchain-wallet-self-custody.md new file mode 100644 index 0000000..a821cb6 --- /dev/null +++ b/.changeset/blockchain-wallet-self-custody.md @@ -0,0 +1,5 @@ +--- +"@blindpay/node": minor +--- + +Add self-custody support to external blockchain wallets. `is_self_custody` (`boolean | null`, null = never answered) is now returned on blockchain wallets and accepted as an optional input on `createWithAddress` and `createWithHash` (the API requires it for customers in Brazil). New `wallets.blockchain.setSelfCustody({ customer_id, id, is_self_custody })` records the answer once on an existing wallet. Adds the `blockchainWallet.update` webhook event. diff --git a/src/resources/wallets/blockchain.test.ts b/src/resources/wallets/blockchain.test.ts index f9fa396..458fe64 100644 --- a/src/resources/wallets/blockchain.test.ts +++ b/src/resources/wallets/blockchain.test.ts @@ -7,6 +7,7 @@ import type { ListBlockchainWalletsResponse, MintUsdbSolanaResponse, PrepareSolanaDelegationTransactionResponse, + SetBlockchainWalletSelfCustodyResponse, } from "./blockchain"; describe("Blockchain wallets", () => { @@ -40,6 +41,7 @@ describe("Blockchain wallets", () => { address: "0xDD6a3aD0949396e57C7738ba8FC1A46A5a1C372C", signature_tx_hash: "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", is_account_abstraction: false, + is_self_custody: null, customer_id: "re_000000000000", }, ]; @@ -64,6 +66,7 @@ describe("Blockchain wallets", () => { address: "0xDD6a3aD0949396e57C7738ba8FC1A46A5a1C372C", signature_tx_hash: undefined, is_account_abstraction: true, + is_self_custody: true, customer_id: "re_000000000000", }; @@ -76,10 +79,15 @@ describe("Blockchain wallets", () => { name: "Wallet Display Name", network: "polygon", address: "0xDD6a3aD0949396e57C7738ba8FC1A46A5a1C372C", + is_self_custody: true, }); expect(error).toBeNull(); expect(data).toEqual(mockedWallet); + expect(JSON.parse(fetchMock.mock.calls[0][1]?.body as string)).toMatchObject({ + is_self_custody: true, + is_account_abstraction: true, + }); }); }); @@ -92,6 +100,7 @@ describe("Blockchain wallets", () => { address: "0xDD6a3aD0949396e57C7738ba8FC1A46A5a1C372C", signature_tx_hash: "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", is_account_abstraction: false, + is_self_custody: null, customer_id: "re_000000000000", }; @@ -120,6 +129,7 @@ describe("Blockchain wallets", () => { address: "0xDD6a3aD0949396e57C7738ba8FC1A46A5a1C372C", signature_tx_hash: "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", is_account_abstraction: false, + is_self_custody: null, customer_id: "re_000000000000", }; @@ -137,6 +147,41 @@ describe("Blockchain wallets", () => { }); }); + describe("Set blockchain wallet self-custody", () => { + it("should PATCH the self-custody answer on the customer route", async () => { + const mockedWallet: SetBlockchainWalletSelfCustodyResponse = { + id: "bw_000000000000", + name: "Wallet Display Name", + network: "polygon", + address: "0xDD6a3aD0949396e57C7738ba8FC1A46A5a1C372C", + signature_tx_hash: "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + is_account_abstraction: false, + is_self_custody: false, + customer_id: "re_000000000000", + }; + + fetchMock.mockResponseOnce(JSON.stringify(mockedWallet), { + headers: { "Content-Type": "application/json" }, + }); + + const { data, error } = await blindpay.wallets.blockchain.setSelfCustody({ + customer_id: "re_000000000000", + id: "bw_000000000000", + is_self_custody: false, + }); + + expect(error).toBeNull(); + expect(data).toEqual(mockedWallet); + + const [url, init] = fetchMock.mock.calls[0]; + expect(url).toContain( + "/instances/in_000000000000/customers/re_000000000000/blockchain-wallets/bw_000000000000" + ); + expect(init?.method).toBe("PATCH"); + expect(JSON.parse(init?.body as string)).toEqual({ is_self_custody: false }); + }); + }); + describe("Delete blockchain wallet", () => { it("should delete a blockchain wallet", async () => { fetchMock.mockResponseOnce(JSON.stringify({ data: null }), { diff --git a/src/resources/wallets/blockchain.ts b/src/resources/wallets/blockchain.ts index 5b239b3..874b896 100644 --- a/src/resources/wallets/blockchain.ts +++ b/src/resources/wallets/blockchain.ts @@ -16,6 +16,7 @@ export type ListBlockchainWalletsResponse = Array<{ address?: string; signature_tx_hash?: string; is_account_abstraction: boolean; + is_self_custody: boolean | null; customer_id: string; }>; @@ -24,6 +25,7 @@ export type CreateBlockchainWalletWithAddressInput = { name: string; network: Network; address: string; + is_self_custody?: boolean; }; export type CreateBlockchainWalletWithHashInput = { @@ -31,6 +33,7 @@ export type CreateBlockchainWalletWithHashInput = { name: string; network: Network; signature_tx_hash: string; + is_self_custody?: boolean; }; export type GetBlockchainWalletInput = { @@ -50,6 +53,7 @@ export type GetBlockchainWalletResponse = { address?: string; signature_tx_hash?: string; is_account_abstraction: boolean; + is_self_custody: boolean | null; customer_id: string; }; @@ -60,9 +64,18 @@ export type CreateBlockchainWalletResponse = { address?: string; signature_tx_hash?: string; is_account_abstraction: boolean; + is_self_custody: boolean | null; customer_id: string; }; +export type SetBlockchainWalletSelfCustodyInput = { + customer_id: string; + id: string; + is_self_custody: boolean; +}; + +export type SetBlockchainWalletSelfCustodyResponse = GetBlockchainWalletResponse; + export type CreateAssetTrustlineInput = string; export type CreateAssetTrustlineResponse = { @@ -157,6 +170,18 @@ export function createBlockchainWalletsResource(instanceId: string, client: Inte `/instances/${instanceId}/customers/${customer_id}/blockchain-wallets/${id}` ); }, + setSelfCustody({ + customer_id, + id, + ...data + }: SetBlockchainWalletSelfCustodyInput): Promise< + BlindpayApiResponse + > { + return client.patch( + `/instances/${instanceId}/customers/${customer_id}/blockchain-wallets/${id}`, + data + ); + }, createAssetTrustline( address: CreateAssetTrustlineInput ): Promise> { diff --git a/src/resources/webhooks/index.ts b/src/resources/webhooks/index.ts index 7cfd610..1edc51c 100644 --- a/src/resources/webhooks/index.ts +++ b/src/resources/webhooks/index.ts @@ -8,6 +8,7 @@ export type WebhookEvents = | "payout.complete" | "payout.partnerFee" | "blockchainWallet.new" + | "blockchainWallet.update" | "payin.new" | "payin.update" | "payin.complete" From 10e841821f46640d47af79f2dd4ec4328da78af9 Mon Sep 17 00:00:00 2001 From: "Darlan Jr." Date: Wed, 30 Sep 2026 14:03:41 -0400 Subject: [PATCH 2/2] chore(api-sync): record payin tracking_payment.review_contexts as unmodeled The spec snapshot already carries this nested shape and map.test.ts has been failing on main because of it. --- .api-sync/unmodeled.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.api-sync/unmodeled.json b/.api-sync/unmodeled.json index 8e3b110..d69b2b9 100644 --- a/.api-sync/unmodeled.json +++ b/.api-sync/unmodeled.json @@ -1316,6 +1316,18 @@ "path": "us.wire", "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", "owner": "eric@blindpay.com" + }, + { + "schema": "PayinOut", + "path": "tracking_payment.review_contexts", + "reason": "Inline nested shape added to the payin tracking_payment step alongside review_source/review_context; the SDK does not model it yet (same pending tracking_payment PR). Recorded so main's test suite goes green again.", + "owner": "darlan@blindpay.com" + }, + { + "schema": "CreatePayinOut", + "path": "tracking_payment.review_contexts", + "reason": "Inline nested shape added to the payin tracking_payment step alongside review_source/review_context; the SDK does not model it yet (same pending tracking_payment PR). Recorded so main's test suite goes green again.", + "owner": "darlan@blindpay.com" } ] }