diff --git a/schema.gql b/schema.gql index f6bb93fe..e41a3d25 100644 --- a/schema.gql +++ b/schema.gql @@ -456,6 +456,42 @@ input LndInput { socket: String! } +type MagmaOrder { + amount: MagmaOrderAmount! + channelId: String + createdAt: String! + destination: MagmaOrderParty! + fees: MagmaOrderFees! + id: String! + paymentStatus: String + source: MagmaOrderParty! + status: String! + timeout: Int +} + +type MagmaOrderAmount { + sats: String +} + +type MagmaOrderFeeAmount { + sats: Int +} + +type MagmaOrderFees { + buyer: MagmaOrderFeeAmount + seller: MagmaOrderFeeAmount +} + +type MagmaOrderParty { + alias: String + pubkey: String +} + +type MagmaPendingOrders { + purchases: [MagmaOrder!]! + sales: [MagmaOrder!]! +} + type MessageType { message: String } @@ -773,6 +809,7 @@ type Query { getPayments(token: String): GetPaymentsType! getPeers: [Peer!]! getPendingChannels: [PendingChannel!]! + getPendingMagmaOrders: MagmaPendingOrders getTapAssetChannelBalances(peerPubkey: String): [TapAssetChannelBalance!]! getTapAssets: TapAssetList! getTapBalances(groupBy: TapBalanceGroupBy = GROUP_KEY): TapBalances! diff --git a/src/client/src/graphql/queries/__generated__/getPendingMagmaOrders.generated.tsx b/src/client/src/graphql/queries/__generated__/getPendingMagmaOrders.generated.tsx new file mode 100644 index 00000000..aadf65b4 --- /dev/null +++ b/src/client/src/graphql/queries/__generated__/getPendingMagmaOrders.generated.tsx @@ -0,0 +1,229 @@ +import * as Types from '../../types'; + +import { gql } from '@apollo/client'; +import * as Apollo from '@apollo/client'; +const defaultOptions = {} as const; +export type GetPendingMagmaOrdersQueryVariables = Types.Exact<{ + [key: string]: never; +}>; + +export type GetPendingMagmaOrdersQuery = { + __typename?: 'Query'; + getPendingMagmaOrders?: { + __typename?: 'MagmaPendingOrders'; + purchases: Array<{ + __typename?: 'MagmaOrder'; + id: string; + createdAt: string; + status: string; + paymentStatus?: string | null; + timeout?: number | null; + channelId?: string | null; + source: { + __typename?: 'MagmaOrderParty'; + pubkey?: string | null; + alias?: string | null; + }; + destination: { + __typename?: 'MagmaOrderParty'; + pubkey?: string | null; + alias?: string | null; + }; + amount: { __typename?: 'MagmaOrderAmount'; sats?: string | null }; + fees: { + __typename?: 'MagmaOrderFees'; + seller?: { + __typename?: 'MagmaOrderFeeAmount'; + sats?: number | null; + } | null; + buyer?: { + __typename?: 'MagmaOrderFeeAmount'; + sats?: number | null; + } | null; + }; + }>; + sales: Array<{ + __typename?: 'MagmaOrder'; + id: string; + createdAt: string; + status: string; + paymentStatus?: string | null; + timeout?: number | null; + channelId?: string | null; + source: { + __typename?: 'MagmaOrderParty'; + pubkey?: string | null; + alias?: string | null; + }; + destination: { + __typename?: 'MagmaOrderParty'; + pubkey?: string | null; + alias?: string | null; + }; + amount: { __typename?: 'MagmaOrderAmount'; sats?: string | null }; + fees: { + __typename?: 'MagmaOrderFees'; + seller?: { + __typename?: 'MagmaOrderFeeAmount'; + sats?: number | null; + } | null; + buyer?: { + __typename?: 'MagmaOrderFeeAmount'; + sats?: number | null; + } | null; + }; + }>; + } | null; +}; + +export const GetPendingMagmaOrdersDocument = gql` + query GetPendingMagmaOrders { + getPendingMagmaOrders { + purchases { + id + createdAt + status + paymentStatus + source { + pubkey + alias + } + destination { + pubkey + alias + } + amount { + sats + } + fees { + seller { + sats + } + buyer { + sats + } + } + timeout + channelId + } + sales { + id + createdAt + status + paymentStatus + source { + pubkey + alias + } + destination { + pubkey + alias + } + amount { + sats + } + fees { + seller { + sats + } + buyer { + sats + } + } + timeout + channelId + } + } + } +`; + +/** + * __useGetPendingMagmaOrdersQuery__ + * + * To run a query within a React component, call `useGetPendingMagmaOrdersQuery` and pass it any options that fit your needs. + * When your component renders, `useGetPendingMagmaOrdersQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetPendingMagmaOrdersQuery({ + * variables: { + * }, + * }); + */ +export function useGetPendingMagmaOrdersQuery( + baseOptions?: Apollo.QueryHookOptions< + GetPendingMagmaOrdersQuery, + GetPendingMagmaOrdersQueryVariables + > +) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useQuery< + GetPendingMagmaOrdersQuery, + GetPendingMagmaOrdersQueryVariables + >(GetPendingMagmaOrdersDocument, options); +} +export function useGetPendingMagmaOrdersLazyQuery( + baseOptions?: Apollo.LazyQueryHookOptions< + GetPendingMagmaOrdersQuery, + GetPendingMagmaOrdersQueryVariables + > +) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useLazyQuery< + GetPendingMagmaOrdersQuery, + GetPendingMagmaOrdersQueryVariables + >(GetPendingMagmaOrdersDocument, options); +} +// @ts-ignore +export function useGetPendingMagmaOrdersSuspenseQuery( + baseOptions?: Apollo.SuspenseQueryHookOptions< + GetPendingMagmaOrdersQuery, + GetPendingMagmaOrdersQueryVariables + > +): Apollo.UseSuspenseQueryResult< + GetPendingMagmaOrdersQuery, + GetPendingMagmaOrdersQueryVariables +>; +export function useGetPendingMagmaOrdersSuspenseQuery( + baseOptions?: + | Apollo.SkipToken + | Apollo.SuspenseQueryHookOptions< + GetPendingMagmaOrdersQuery, + GetPendingMagmaOrdersQueryVariables + > +): Apollo.UseSuspenseQueryResult< + GetPendingMagmaOrdersQuery | undefined, + GetPendingMagmaOrdersQueryVariables +>; +export function useGetPendingMagmaOrdersSuspenseQuery( + baseOptions?: + | Apollo.SkipToken + | Apollo.SuspenseQueryHookOptions< + GetPendingMagmaOrdersQuery, + GetPendingMagmaOrdersQueryVariables + > +) { + const options = + baseOptions === Apollo.skipToken + ? baseOptions + : { ...defaultOptions, ...baseOptions }; + return Apollo.useSuspenseQuery< + GetPendingMagmaOrdersQuery, + GetPendingMagmaOrdersQueryVariables + >(GetPendingMagmaOrdersDocument, options); +} +export type GetPendingMagmaOrdersQueryHookResult = ReturnType< + typeof useGetPendingMagmaOrdersQuery +>; +export type GetPendingMagmaOrdersLazyQueryHookResult = ReturnType< + typeof useGetPendingMagmaOrdersLazyQuery +>; +export type GetPendingMagmaOrdersSuspenseQueryHookResult = ReturnType< + typeof useGetPendingMagmaOrdersSuspenseQuery +>; +export type GetPendingMagmaOrdersQueryResult = Apollo.QueryResult< + GetPendingMagmaOrdersQuery, + GetPendingMagmaOrdersQueryVariables +>; diff --git a/src/client/src/graphql/queries/getPendingMagmaOrders.ts b/src/client/src/graphql/queries/getPendingMagmaOrders.ts new file mode 100644 index 00000000..91c79b68 --- /dev/null +++ b/src/client/src/graphql/queries/getPendingMagmaOrders.ts @@ -0,0 +1,62 @@ +import { gql } from '@apollo/client'; + +export const GET_PENDING_MAGMA_ORDERS = gql` + query GetPendingMagmaOrders { + getPendingMagmaOrders { + purchases { + id + createdAt + status + paymentStatus + source { + pubkey + alias + } + destination { + pubkey + alias + } + amount { + sats + } + fees { + seller { + sats + } + buyer { + sats + } + } + timeout + channelId + } + sales { + id + createdAt + status + paymentStatus + source { + pubkey + alias + } + destination { + pubkey + alias + } + amount { + sats + } + fees { + seller { + sats + } + buyer { + sats + } + } + timeout + channelId + } + } + } +`; diff --git a/src/client/src/graphql/types.ts b/src/client/src/graphql/types.ts index f9362840..e8ade017 100644 --- a/src/client/src/graphql/types.ts +++ b/src/client/src/graphql/types.ts @@ -525,6 +525,48 @@ export type LndInput = { socket: Scalars['String']['input']; }; +export type MagmaOrder = { + __typename?: 'MagmaOrder'; + amount: MagmaOrderAmount; + channelId?: Maybe; + createdAt: Scalars['String']['output']; + destination: MagmaOrderParty; + fees: MagmaOrderFees; + id: Scalars['String']['output']; + paymentStatus?: Maybe; + source: MagmaOrderParty; + status: Scalars['String']['output']; + timeout?: Maybe; +}; + +export type MagmaOrderAmount = { + __typename?: 'MagmaOrderAmount'; + sats?: Maybe; +}; + +export type MagmaOrderFeeAmount = { + __typename?: 'MagmaOrderFeeAmount'; + sats?: Maybe; +}; + +export type MagmaOrderFees = { + __typename?: 'MagmaOrderFees'; + buyer?: Maybe; + seller?: Maybe; +}; + +export type MagmaOrderParty = { + __typename?: 'MagmaOrderParty'; + alias?: Maybe; + pubkey?: Maybe; +}; + +export type MagmaPendingOrders = { + __typename?: 'MagmaPendingOrders'; + purchases: Array; + sales: Array; +}; + export type MessageType = { __typename?: 'MessageType'; message?: Maybe; @@ -1068,6 +1110,7 @@ export type Query = { getPayments: GetPaymentsType; getPeers: Array; getPendingChannels: Array; + getPendingMagmaOrders?: Maybe; getTapAssetChannelBalances: Array; getTapAssets: TapAssetList; getTapBalances: TapBalances; diff --git a/src/client/src/pages/ChannelsPage.tsx b/src/client/src/pages/ChannelsPage.tsx index be3d1483..6f2c67d3 100644 --- a/src/client/src/pages/ChannelsPage.tsx +++ b/src/client/src/pages/ChannelsPage.tsx @@ -19,6 +19,7 @@ import { import { OpenChannel } from '../views/home/liquidity/OpenChannel'; import { DetailsChange } from '../components/details/detailsChange'; import { useNodeSlug, useNodePath } from '../hooks/useNodeSlug'; +import { MagmaOrders } from '../views/channels/magmaOrders/MagmaOrders'; type ChannelTab = 'open' | 'pending' | 'closed'; @@ -131,6 +132,8 @@ const ChannelView = () => { + + !open && setOpenDialog(null)} diff --git a/src/client/src/views/channels/magmaOrders/MagmaOrders.tsx b/src/client/src/views/channels/magmaOrders/MagmaOrders.tsx new file mode 100644 index 00000000..e0b22c03 --- /dev/null +++ b/src/client/src/views/channels/magmaOrders/MagmaOrders.tsx @@ -0,0 +1,179 @@ +import { ExternalLink } from 'lucide-react'; +import { + useGetPendingMagmaOrdersQuery, + GetPendingMagmaOrdersQuery, +} from '../../../graphql/queries/__generated__/getPendingMagmaOrders.generated'; +import { Price } from '../../../components/price/Price'; +import { Badge } from '../../../components/ui/badge'; +import { Button } from '../../../components/ui/button'; +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from '../../../components/ui/card'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '../../../components/ui/table'; + +type PendingOrder = NonNullable< + NonNullable< + GetPendingMagmaOrdersQuery['getPendingMagmaOrders'] + >['purchases'][number] +>; + +const STATUS_LABELS: Record = { + WAITING_FOR_SELLER_APPROVAL: 'Awaiting seller approval', + WAITING_FOR_BUYER_PAYMENT: 'Payment required', + WAITING_FOR_CHANNEL_OPEN: 'Awaiting channel open', + SELLER_SENT_TRANSACTION: 'Transaction broadcast', + WAITING_FOR_ON_CHAIN_CONFIRMATION: 'Awaiting confirmation', + VALID_CHANNEL_OPENING: 'Channel opened', + SELLER_OPENED_CHANNEL: 'Channel confirmed', + CHANNEL_MONITORING_FINISHED: 'Complete', + SELLER_REJECTED: 'Seller rejected', + BUYER_REJECTED: 'Buyer cancelled', + SELLER_FAILED_TO_REACT: 'Seller timed out', + BUYER_FAILED_TO_PAY: 'Payment timed out', + SELLER_FAILED_TO_OPEN_CHANNEL: 'Channel open failed', + INVALID_CHANNEL_OPENING: 'Invalid channel', + ADMIN_CLOSED: 'Closed by admin', +}; + +const STATUS_VARIANTS: Record< + string, + 'default' | 'secondary' | 'destructive' | 'outline' +> = { + WAITING_FOR_BUYER_PAYMENT: 'default', + WAITING_FOR_SELLER_APPROVAL: 'secondary', + WAITING_FOR_CHANNEL_OPEN: 'secondary', +}; + +function statusLabel(status: string): string { + return STATUS_LABELS[status] ?? status; +} + +function statusVariant( + status: string +): 'default' | 'secondary' | 'destructive' | 'outline' { + return STATUS_VARIANTS[status] ?? 'outline'; +} + +function peerName( + party: { pubkey?: string | null; alias?: string | null } | null | undefined +): string { + if (!party) return 'Unknown'; + return ( + party.alias || (party.pubkey ? party.pubkey.slice(0, 16) + '…' : 'Unknown') + ); +} + +function OrderTable({ + orders, + role, +}: { + orders: PendingOrder[]; + role: 'buyer' | 'seller'; +}) { + if (!orders.length) return null; + + return ( +
+

+ {role === 'buyer' + ? 'Purchases (you are buying)' + : 'Sales (you are selling)'} +

+ + + + Peer + Amount + Fee + Status + Created + Action + + + + {orders.map(order => { + const peer = + role === 'buyer' + ? peerName(order.source) + : peerName(order.destination); + const feeSats = + role === 'buyer' + ? order.fees?.buyer?.sats + : order.fees?.seller?.sats; + const created = order.createdAt + ? new Date(order.createdAt).toLocaleDateString() + : '-'; + + return ( + + {peer} + + + + + {feeSats != null ? : '-'} + + + + {statusLabel(order.status)} + + + + {created} + + + + + + ); + })} + +
+
+ ); +} + +export const MagmaOrders = () => { + const { data } = useGetPendingMagmaOrdersQuery(); + + const orders = data?.getPendingMagmaOrders; + if (!orders) return null; + + const purchases = orders.purchases ?? []; + const sales = orders.sales ?? []; + + if (!purchases.length && !sales.length) return null; + + return ( + + + Pending Magma Orders + + + + + + + ); +}; diff --git a/src/server/modules/api/magma/magma.gql.ts b/src/server/modules/api/magma/magma.gql.ts index be0e5385..1e28f437 100644 --- a/src/server/modules/api/magma/magma.gql.ts +++ b/src/server/modules/api/magma/magma.gql.ts @@ -52,6 +52,77 @@ export const createMagmaOrderMutation = gql` } `; +export const getPendingOrdersQuery = gql` + query GetPendingOrders($page: PageInput) { + getUser { + market { + orders { + purchases(input: { action_needed: true }, page: $page) { + total + list { + id + created_at + status + payment_status + source { + pubkey + alias + } + destination { + pubkey + alias + } + amount { + sats + } + fees { + seller { + sats + } + buyer { + sats + } + } + timeout + channel_id + } + } + sales(input: { action_needed: true }, page: $page) { + total + list { + id + created_at + status + payment_status + source { + pubkey + alias + } + destination { + pubkey + alias + } + amount { + sats + } + fees { + seller { + sats + } + buyer { + sats + } + } + timeout + channel_id + } + } + } + } + } + } +`; + export const getSupportedAssetsQuery = gql` query GetSupportedAssets($input: SupportedAssetsInput) { public { diff --git a/src/server/modules/api/magma/magma.resolver.spec.ts b/src/server/modules/api/magma/magma.resolver.spec.ts index 924609bf..db847d4f 100644 --- a/src/server/modules/api/magma/magma.resolver.spec.ts +++ b/src/server/modules/api/magma/magma.resolver.spec.ts @@ -566,4 +566,133 @@ describe('MagmaResolver', () => { ).rejects.toThrow('Either tapdAssetId or tapdGroupKey must be provided'); }); }); + + // ── getPendingMagmaOrders ────────────────────────────────────── + + describe('getPendingMagmaOrders', () => { + const magmaUrl = 'https://magma.test/graphql'; + + const rawOrder = { + id: 'order-1', + created_at: '2026-01-01T00:00:00Z', + status: 'WAITING_FOR_BUYER_PAYMENT', + payment_status: 'UNPAID', + source: { pubkey: 'srcpub', alias: 'Alice' }, + destination: { pubkey: 'dstpub', alias: 'Bob' }, + amount: { sats: '100000' }, + fees: { seller: { sats: 200 }, buyer: { sats: 300 } }, + timeout: 3600, + channel_id: 'chan-1', + }; + + beforeEach(() => { + mockAmbossService.resolveMagmaUrl.mockResolvedValue(magmaUrl); + }); + + it('maps purchases and sales from the API response', async () => { + mockFetchService.graphqlFetchWithProxy.mockResolvedValue({ + data: { + getUser: { + market: { + orders: { + purchases: { total: 1, list: [rawOrder] }, + sales: { total: 1, list: [{ ...rawOrder, id: 'order-2' }] }, + }, + }, + }, + }, + error: undefined, + }); + + const result = await resolver.getPendingMagmaOrders(userId); + + expect(result).not.toBeNull(); + expect(result!.purchases).toHaveLength(1); + expect(result!.purchases[0]).toEqual({ + id: 'order-1', + createdAt: '2026-01-01T00:00:00Z', + status: 'WAITING_FOR_BUYER_PAYMENT', + paymentStatus: 'UNPAID', + source: { pubkey: 'srcpub', alias: 'Alice' }, + destination: { pubkey: 'dstpub', alias: 'Bob' }, + amount: { sats: '100000' }, + fees: { seller: { sats: 200 }, buyer: { sats: 300 } }, + timeout: 3600, + channelId: 'chan-1', + }); + expect(result!.sales[0].id).toBe('order-2'); + }); + + it('returns null when no auth token is available', async () => { + mockAmbossTokenService.get.mockResolvedValue(null); + + const result = await resolver.getPendingMagmaOrders(userId); + + expect(result).toBeNull(); + expect(mockFetchService.graphqlFetchWithProxy).not.toHaveBeenCalled(); + }); + + it('returns null and logs error when the fetch fails', async () => { + mockFetchService.graphqlFetchWithProxy.mockResolvedValue({ + data: undefined, + error: new Error('network error'), + }); + + const result = await resolver.getPendingMagmaOrders(userId); + + expect(result).toBeNull(); + expect(mockLogger.error).toHaveBeenCalledWith( + 'Error fetching pending Magma orders', + expect.any(Object) + ); + }); + + it('returns null when the API response has no orders data', async () => { + mockFetchService.graphqlFetchWithProxy.mockResolvedValue({ + data: { getUser: null }, + error: undefined, + }); + + const result = await resolver.getPendingMagmaOrders(userId); + + expect(result).toBeNull(); + }); + + it('handles orders with missing optional fields', async () => { + const sparse = { + id: 'order-3', + created_at: '2026-02-01T00:00:00Z', + status: 'WAITING_FOR_SELLER_APPROVAL', + source: {}, + destination: {}, + amount: {}, + fees: {}, + }; + mockFetchService.graphqlFetchWithProxy.mockResolvedValue({ + data: { + getUser: { + market: { + orders: { + purchases: { total: 1, list: [sparse] }, + sales: { total: 0, list: [] }, + }, + }, + }, + }, + error: undefined, + }); + + const result = await resolver.getPendingMagmaOrders(userId); + + expect(result).not.toBeNull(); + expect(result!.purchases[0]).toMatchObject({ + id: 'order-3', + paymentStatus: undefined, + channelId: undefined, + timeout: undefined, + fees: { seller: undefined, buyer: undefined }, + }); + expect(result!.sales).toHaveLength(0); + }); + }); }); diff --git a/src/server/modules/api/magma/magma.resolver.ts b/src/server/modules/api/magma/magma.resolver.ts index 6177053d..4aaa857b 100644 --- a/src/server/modules/api/magma/magma.resolver.ts +++ b/src/server/modules/api/magma/magma.resolver.ts @@ -22,11 +22,16 @@ import { SetupTradePartnerInput, SetupTradePartnerResult, SetupTradePartnerAuto, + MagmaPendingOrders, + MagmaOrder, + AmbossOrderRaw, + AmbossOrderList, } from './magma.types'; import { getOffersQuery, getSupportedAssetsQuery, createMagmaOrderMutation, + getPendingOrdersQuery, } from './magma.gql'; /** @@ -221,6 +226,65 @@ export class MagmaResolver { }; } + // ── Pending Orders ── + + @Query(() => MagmaPendingOrders, { nullable: true }) + async getPendingMagmaOrders( + @CurrentUser() user: UserId + ): Promise { + const ambossAuth = await this.ambossTokenService.get(user); + if (!ambossAuth) return null; + + const magmaUrl = await this.ambossService.resolveMagmaUrl(user); + + const { data, error } = await this.fetchService.graphqlFetchWithProxy<{ + getUser: { + market: { + orders: { + purchases: AmbossOrderList; + sales: AmbossOrderList; + }; + }; + }; + }>( + magmaUrl, + getPendingOrdersQuery, + { page: { offset: 0, limit: 50 } }, + { authorization: `Bearer ${ambossAuth}` } + ); + + if (error || !data?.getUser?.market?.orders) { + if (error) + this.logger.error('Error fetching pending Magma orders', { error }); + return null; + } + + const mapOrder = (o: AmbossOrderRaw): MagmaOrder => ({ + id: o.id, + createdAt: o.created_at, + status: o.status, + paymentStatus: o.payment_status, + source: { pubkey: o.source?.pubkey, alias: o.source?.alias }, + destination: { + pubkey: o.destination?.pubkey, + alias: o.destination?.alias, + }, + amount: { sats: o.amount?.sats }, + fees: { + seller: o.fees?.seller ? { sats: o.fees.seller.sats } : undefined, + buyer: o.fees?.buyer ? { sats: o.fees.buyer.sats } : undefined, + }, + timeout: o.timeout, + channelId: o.channel_id, + }); + + const { purchases, sales } = data.getUser.market.orders; + return { + purchases: purchases.list.map(mapOrder), + sales: sales.list.map(mapOrder), + }; + } + // ── Trade Partner Setup ── @Mutation(() => SetupTradePartnerResult) diff --git a/src/server/modules/api/magma/magma.types.ts b/src/server/modules/api/magma/magma.types.ts index b0055bd0..5f617027 100644 --- a/src/server/modules/api/magma/magma.types.ts +++ b/src/server/modules/api/magma/magma.types.ts @@ -7,6 +7,112 @@ import { registerEnumType, } from '@nestjs/graphql'; +// ─── Pending Orders ───────────────────────────────────────────── + +// Raw shapes returned by the Amboss Magma API (before mapping to GQL types) + +export type AmbossOrderParty = { + pubkey?: string; + alias?: string; +}; + +export type AmbossOrderFeeAmount = { + sats?: number; +}; + +export type AmbossOrderRaw = { + id: string; + created_at: string; + status: string; + payment_status?: string; + source: AmbossOrderParty; + destination: AmbossOrderParty; + amount?: { sats?: string }; + fees?: { + seller?: AmbossOrderFeeAmount; + buyer?: AmbossOrderFeeAmount; + }; + timeout?: number; + channel_id?: string; +}; + +export type AmbossOrderList = { + total: number; + list: AmbossOrderRaw[]; +}; + +@ObjectType() +export class MagmaOrderParty { + @Field({ nullable: true }) + pubkey?: string; + + @Field({ nullable: true }) + alias?: string; +} + +@ObjectType() +export class MagmaOrderAmount { + @Field({ nullable: true }) + sats?: string; +} + +@ObjectType() +export class MagmaOrderFeeAmount { + @Field(() => Int, { nullable: true }) + sats?: number; +} + +@ObjectType() +export class MagmaOrderFees { + @Field(() => MagmaOrderFeeAmount, { nullable: true }) + seller?: MagmaOrderFeeAmount; + + @Field(() => MagmaOrderFeeAmount, { nullable: true }) + buyer?: MagmaOrderFeeAmount; +} + +@ObjectType() +export class MagmaOrder { + @Field() + id: string; + + @Field() + createdAt: string; + + @Field() + status: string; + + @Field({ nullable: true }) + paymentStatus?: string; + + @Field(() => MagmaOrderParty) + source: MagmaOrderParty; + + @Field(() => MagmaOrderParty) + destination: MagmaOrderParty; + + @Field(() => MagmaOrderAmount) + amount: MagmaOrderAmount; + + @Field(() => MagmaOrderFees) + fees: MagmaOrderFees; + + @Field(() => Int, { nullable: true }) + timeout?: number; + + @Field({ nullable: true }) + channelId?: string; +} + +@ObjectType() +export class MagmaPendingOrders { + @Field(() => [MagmaOrder]) + purchases: MagmaOrder[]; + + @Field(() => [MagmaOrder]) + sales: MagmaOrder[]; +} + // ─── Enums ────────────────────────────────────────────────────── export enum TapTransactionType {