From 1772207618bd76985f1a40cd2d00c862661c01ee Mon Sep 17 00:00:00 2001 From: Bufo Date: Tue, 25 Aug 2026 11:02:57 +0200 Subject: [PATCH] feat: add Transactions.findOne/findMany [AMB-2992] The schema already exposes find_one/find_many on the transaction namespace, but transactions.graphql never declared operations for them, so the typed client never got the methods. Add GetTransaction and ListTransactions operations, regenerate the SDK, and wrap them on Transactions, mirroring the existing Wallets.get/list pattern. --- AGENTS.md | 2 +- docs/INTEGRATION.md | 16 ++++ packages/payments/README.md | 13 +++ packages/payments/src/generated/sdk.ts | 82 +++++++++++++++++++ .../src/operations/transactions.graphql | 54 ++++++++++++ .../src/resources/transactions.find.test.ts | 65 +++++++++++++++ .../payments/src/resources/transactions.ts | 12 +++ 7 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 packages/payments/src/resources/transactions.find.test.ts diff --git a/AGENTS.md b/AGENTS.md index e51ffde..5ba4290 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -71,7 +71,7 @@ Resource getters are lazy and call `requireServiceApiKey`: | --------------- | -------------- | ------------------------------------------------------------- | | `.environments` | `Environments` | `list()`, `get(id)`, `create(input)`, `delete(id)` | | `.wallets` | `Wallets` | `list({ environmentId })`, `get(id)`, `create(input)`, `delete(id)` | -| `.transactions` | `Transactions` | `createReceive(input)`, `send(params)`, `prepareSend(params)`, `isSendReady(walletId)`, `forgetSend(walletId)` | +| `.transactions` | `Transactions` | `findOne(id)`, `findMany(params)`, `createReceive(input)`, `send(params)`, `prepareSend(params)`, `isSendReady(walletId)`, `forgetSend(walletId)` | | `.webhooks` | `Webhooks` | `verify(input)` — does NOT require any API key | `Payments.webhooks` is also a static reference to `Webhooks` for stateless use. diff --git a/docs/INTEGRATION.md b/docs/INTEGRATION.md index 9670b58..b79f56c 100644 --- a/docs/INTEGRATION.md +++ b/docs/INTEGRATION.md @@ -189,6 +189,22 @@ Three things to plan around: rotated node credentials — there is no expiry. The Argon2 master key is never cached; only the one wallet's macaroon is. +### Looking up transactions + +```ts +const transaction = await payments.transactions.findOne(transactionId); + +const { list, pagination, total_count } = await payments.transactions.findMany({ + wallet_id: walletId, + filter: { status: 'COMPLETED', direction: 'SEND' }, // optional + page: { limit: 20, offset: 0 }, // optional +}); +``` + +`findOne` returns the full transaction record. `findMany` returns trimmed +records (no `payment_hash` / `payment_request` / `error`) — use `findOne` when +you need those fields for one transaction. + ## Step 5 — Consume webhooks Amboss signs every webhook: HMAC-SHA256 over `${timestamp}.${rawBody}`, sent diff --git a/packages/payments/README.md b/packages/payments/README.md index 5c8d1c0..60223a0 100644 --- a/packages/payments/README.md +++ b/packages/payments/README.md @@ -160,6 +160,19 @@ await payments.wallets.delete(id); ### Transactions +#### Finding transactions + +```ts +await payments.transactions.findOne(id); // full transaction record +await payments.transactions.findMany({ wallet_id: walletId }); // { list, pagination, total_count } +``` + +`findMany` accepts the same input as the backend's transaction list query — +`environment_id`, `wallet_id`, `page: { limit, offset }`, and +`filter: { date_range, direction, status }`. Each item in `list` is a trimmed +record (no `payment_hash` / `payment_request` / `error` / etc.); use `findOne` +for the full transaction. + #### Receiving `transactions.createReceive` generates a Lightning invoice for a wallet. Unlike diff --git a/packages/payments/src/generated/sdk.ts b/packages/payments/src/generated/sdk.ts index 77585e8..05da60f 100644 --- a/packages/payments/src/generated/sdk.ts +++ b/packages/payments/src/generated/sdk.ts @@ -1587,6 +1587,24 @@ export type DeleteEnvironmentMutation = { payment: { environment: { delete: bool export type PaymentsTransactionFieldsFragment = { id: string, wallet_id: string, node_id?: string | null, idempotency_key: string, direction: PaymentsTransactionDirection, status: PaymentsTransactionStatus, amount_sats?: string | null, fee?: string | null, payment_hash?: string | null, payment_request?: string | null, description?: string | null, error?: string | null, expires_at?: string | null, settled_at?: string | null, created_at: string, updated_at: string, amount: { id: string, display_amount: string, full_amount: string }, asset: { id: string, symbol: string, type: BitcoinAssetType, precision: number } }; +export type SimplePaymentsTransactionFieldsFragment = { id: string, wallet_id: string, direction: PaymentsTransactionDirection, status: PaymentsTransactionStatus, fee?: string | null, description?: string | null, settled_at?: string | null, created_at: string, updated_at: string, amount: { id: string, display_amount: string, full_amount: string }, asset: { id: string, symbol: string, type: BitcoinAssetType, precision: number } }; + +export type FindManyTransactionsFieldsFragment = { total_count: number, list: Array<{ id: string, wallet_id: string, direction: PaymentsTransactionDirection, status: PaymentsTransactionStatus, fee?: string | null, description?: string | null, settled_at?: string | null, created_at: string, updated_at: string, amount: { id: string, display_amount: string, full_amount: string }, asset: { id: string, symbol: string, type: BitcoinAssetType, precision: number } }>, pagination: { limit: number, offset: number } }; + +export type GetTransactionQueryVariables = Exact<{ + id: Scalars['String']['input']; +}>; + + +export type GetTransactionQuery = { payment: { transaction: { find_one: { id: string, wallet_id: string, node_id?: string | null, idempotency_key: string, direction: PaymentsTransactionDirection, status: PaymentsTransactionStatus, amount_sats?: string | null, fee?: string | null, payment_hash?: string | null, payment_request?: string | null, description?: string | null, error?: string | null, expires_at?: string | null, settled_at?: string | null, created_at: string, updated_at: string, amount: { id: string, display_amount: string, full_amount: string }, asset: { id: string, symbol: string, type: BitcoinAssetType, precision: number } } } } }; + +export type ListTransactionsQueryVariables = Exact<{ + input: ListTransactionsInput; +}>; + + +export type ListTransactionsQuery = { payment: { transaction: { find_many: { total_count: number, list: Array<{ id: string, wallet_id: string, direction: PaymentsTransactionDirection, status: PaymentsTransactionStatus, fee?: string | null, description?: string | null, settled_at?: string | null, created_at: string, updated_at: string, amount: { id: string, display_amount: string, full_amount: string }, asset: { id: string, symbol: string, type: BitcoinAssetType, precision: number } }>, pagination: { limit: number, offset: number } } } } }; + export type CreateReceiveTransactionMutationVariables = Exact<{ input: CreateReceiveTransactionInput; }>; @@ -1689,6 +1707,42 @@ export const PaymentsTransactionFieldsFragmentDoc = ` updated_at } `; +export const SimplePaymentsTransactionFieldsFragmentDoc = ` + fragment SimplePaymentsTransactionFields on SimplePaymentsTransaction { + id + wallet_id + direction + status + amount { + id + display_amount + full_amount + } + asset { + id + symbol + type + precision + } + fee + description + settled_at + created_at + updated_at +} + `; +export const FindManyTransactionsFieldsFragmentDoc = ` + fragment FindManyTransactionsFields on FindManyTransactions { + list { + ...SimplePaymentsTransactionFields + } + pagination { + limit + offset + } + total_count +} + ${SimplePaymentsTransactionFieldsFragmentDoc}`; export const PaymentsEnvironmentFieldsFragmentDoc = ` fragment PaymentsEnvironmentFields on PaymentsEnvironment { id @@ -1805,6 +1859,28 @@ export const DeleteEnvironmentDocument = ` } } `; +export const GetTransactionDocument = ` + query GetTransaction($id: String!) { + payment { + transaction { + find_one(id: $id) { + ...PaymentsTransactionFields + } + } + } +} + ${PaymentsTransactionFieldsFragmentDoc}`; +export const ListTransactionsDocument = ` + query ListTransactions($input: ListTransactionsInput!) { + payment { + transaction { + find_many(input: $input) { + ...FindManyTransactionsFields + } + } + } +} + ${FindManyTransactionsFieldsFragmentDoc}`; export const CreateReceiveTransactionDocument = ` mutation CreateReceiveTransaction($input: CreateReceiveTransactionInput!) { payment { @@ -1947,6 +2023,12 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = DeleteEnvironment(variables: DeleteEnvironmentMutationVariables, requestHeaders?: GraphQLClientRequestHeaders, signal?: RequestInit['signal']): Promise { return withWrapper((wrappedRequestHeaders) => client.request({ document: DeleteEnvironmentDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), 'DeleteEnvironment', 'mutation', variables); }, + GetTransaction(variables: GetTransactionQueryVariables, requestHeaders?: GraphQLClientRequestHeaders, signal?: RequestInit['signal']): Promise { + return withWrapper((wrappedRequestHeaders) => client.request({ document: GetTransactionDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), 'GetTransaction', 'query', variables); + }, + ListTransactions(variables: ListTransactionsQueryVariables, requestHeaders?: GraphQLClientRequestHeaders, signal?: RequestInit['signal']): Promise { + return withWrapper((wrappedRequestHeaders) => client.request({ document: ListTransactionsDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), 'ListTransactions', 'query', variables); + }, CreateReceiveTransaction(variables: CreateReceiveTransactionMutationVariables, requestHeaders?: GraphQLClientRequestHeaders, signal?: RequestInit['signal']): Promise { return withWrapper((wrappedRequestHeaders) => client.request({ document: CreateReceiveTransactionDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), 'CreateReceiveTransaction', 'mutation', variables); }, diff --git a/packages/payments/src/operations/transactions.graphql b/packages/payments/src/operations/transactions.graphql index 175e288..6438820 100644 --- a/packages/payments/src/operations/transactions.graphql +++ b/packages/payments/src/operations/transactions.graphql @@ -28,6 +28,60 @@ fragment PaymentsTransactionFields on PaymentsTransaction { updated_at } +fragment SimplePaymentsTransactionFields on SimplePaymentsTransaction { + id + wallet_id + direction + status + amount { + id + display_amount + full_amount + } + asset { + id + symbol + type + precision + } + fee + description + settled_at + created_at + updated_at +} + +fragment FindManyTransactionsFields on FindManyTransactions { + list { + ...SimplePaymentsTransactionFields + } + pagination { + limit + offset + } + total_count +} + +query GetTransaction($id: String!) { + payment { + transaction { + find_one(id: $id) { + ...PaymentsTransactionFields + } + } + } +} + +query ListTransactions($input: ListTransactionsInput!) { + payment { + transaction { + find_many(input: $input) { + ...FindManyTransactionsFields + } + } + } +} + mutation CreateReceiveTransaction($input: CreateReceiveTransactionInput!) { payment { transaction { diff --git a/packages/payments/src/resources/transactions.find.test.ts b/packages/payments/src/resources/transactions.find.test.ts new file mode 100644 index 0000000..e62c16f --- /dev/null +++ b/packages/payments/src/resources/transactions.find.test.ts @@ -0,0 +1,65 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import type { GraphQLClient } from 'graphql-request'; + +import { Transactions } from './transactions.js'; + +/** Fake GraphQLClient that answers GetTransaction / ListTransactions. */ +function fakeClient(): { client: GraphQLClient; ops: string[] } { + const ops: string[] = []; + const request = async ({ document }: { document: string }): Promise => { + ops.push(document); + if (document.includes('GetTransaction')) { + return { + payment: { + transaction: { + find_one: { id: 'tx1', wallet_id: 'w1', status: 'COMPLETED' }, + }, + }, + }; + } + if (document.includes('ListTransactions')) { + return { + payment: { + transaction: { + find_many: { + list: [{ id: 'tx1', wallet_id: 'w1', status: 'COMPLETED' }], + pagination: { limit: 20, offset: 0 }, + total_count: 1, + }, + }, + }, + }; + } + throw new Error(`unexpected document: ${document.slice(0, 40)}`); + }; + return { client: { request } as unknown as GraphQLClient, ops }; +} + +describe('Transactions.findOne', () => { + it('calls GetTransaction and returns the transaction', async () => { + const { client, ops } = fakeClient(); + const transactions = new Transactions(client); + + const result = await transactions.findOne('tx1'); + + assert.equal(result.id, 'tx1'); + assert.ok(ops.some((document) => document.includes('GetTransaction'))); + }); +}); + +describe('Transactions.findMany', () => { + it('calls ListTransactions and returns list, pagination, and total_count', async () => { + const { client, ops } = fakeClient(); + const transactions = new Transactions(client); + + const result = await transactions.findMany({ wallet_id: 'w1' }); + + assert.equal(result.total_count, 1); + assert.equal(result.pagination.limit, 20); + assert.equal(result.list.length, 1); + assert.equal(result.list[0]?.id, 'tx1'); + assert.ok(ops.some((document) => document.includes('ListTransactions'))); + }); +}); diff --git a/packages/payments/src/resources/transactions.ts b/packages/payments/src/resources/transactions.ts index 5ca7414..170c75b 100644 --- a/packages/payments/src/resources/transactions.ts +++ b/packages/payments/src/resources/transactions.ts @@ -7,6 +7,8 @@ import { getSdk, type CreateReceiveTransactionInput, type CreateSendTransactionInput, + type FindManyTransactionsFieldsFragment, + type ListTransactionsInput, type PaymentsTransactionFieldsFragment, } from '../generated/sdk.js'; import { sendAssetPayment } from '../node/lit.js'; @@ -145,6 +147,16 @@ export class Transactions { this.#pending.delete(walletId); } + async findOne(id: string): Promise { + const res = await this.#sdk.GetTransaction({ id }); + return res.payment.transaction.find_one; + } + + async findMany(input: ListTransactionsInput): Promise { + const res = await this.#sdk.ListTransactions({ input }); + return res.payment.transaction.find_many; + } + async createReceive( input: CreateReceiveTransactionInput, ): Promise {