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 {