feat: show pending Magma orders on Channels page

Adds a getPendingMagmaOrders GraphQL query that fetches action-needed
purchases and sales from the Amboss Magma API. The Channels page now
renders a MagmaOrders card above the channel tabs whenever pending orders
exist, showing peer, amount, fee, status badge, and a link to act on
Amboss Space.
This commit is contained in:
JC Brand 2026-04-14 12:30:50 +02:00
parent 6bfb2a708e
commit 4ff5757b80
No known key found for this signature in database
GPG key ID: D48D88C41B3A34E6
10 changed files with 923 additions and 0 deletions

View file

@ -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!

View file

@ -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
>;

View file

@ -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
}
}
}
`;

View file

@ -525,6 +525,48 @@ export type LndInput = {
socket: Scalars['String']['input'];
};
export type MagmaOrder = {
__typename?: 'MagmaOrder';
amount: MagmaOrderAmount;
channelId?: Maybe<Scalars['String']['output']>;
createdAt: Scalars['String']['output'];
destination: MagmaOrderParty;
fees: MagmaOrderFees;
id: Scalars['String']['output'];
paymentStatus?: Maybe<Scalars['String']['output']>;
source: MagmaOrderParty;
status: Scalars['String']['output'];
timeout?: Maybe<Scalars['Int']['output']>;
};
export type MagmaOrderAmount = {
__typename?: 'MagmaOrderAmount';
sats?: Maybe<Scalars['String']['output']>;
};
export type MagmaOrderFeeAmount = {
__typename?: 'MagmaOrderFeeAmount';
sats?: Maybe<Scalars['Int']['output']>;
};
export type MagmaOrderFees = {
__typename?: 'MagmaOrderFees';
buyer?: Maybe<MagmaOrderFeeAmount>;
seller?: Maybe<MagmaOrderFeeAmount>;
};
export type MagmaOrderParty = {
__typename?: 'MagmaOrderParty';
alias?: Maybe<Scalars['String']['output']>;
pubkey?: Maybe<Scalars['String']['output']>;
};
export type MagmaPendingOrders = {
__typename?: 'MagmaPendingOrders';
purchases: Array<MagmaOrder>;
sales: Array<MagmaOrder>;
};
export type MessageType = {
__typename?: 'MessageType';
message?: Maybe<Scalars['String']['output']>;
@ -1068,6 +1110,7 @@ export type Query = {
getPayments: GetPaymentsType;
getPeers: Array<Peer>;
getPendingChannels: Array<PendingChannel>;
getPendingMagmaOrders?: Maybe<MagmaPendingOrders>;
getTapAssetChannelBalances: Array<TapAssetChannelBalance>;
getTapAssets: TapAssetList;
getTapBalances: TapBalances;

View file

@ -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 = () => {
</CardContent>
</Card>
<MagmaOrders />
<Dialog
open={openDialog === 'open'}
onOpenChange={open => !open && setOpenDialog(null)}

View file

@ -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<string, string> = {
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 (
<div className="space-y-2">
<p className="text-sm font-medium text-muted-foreground">
{role === 'buyer'
? 'Purchases (you are buying)'
: 'Sales (you are selling)'}
</p>
<Table>
<TableHeader>
<TableRow>
<TableHead>Peer</TableHead>
<TableHead>Amount</TableHead>
<TableHead>Fee</TableHead>
<TableHead>Status</TableHead>
<TableHead>Created</TableHead>
<TableHead className="text-right">Action</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{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 (
<TableRow key={order.id}>
<TableCell className="font-medium">{peer}</TableCell>
<TableCell>
<Price amount={order.amount?.sats} />
</TableCell>
<TableCell>
{feeSats != null ? <Price amount={feeSats} /> : '-'}
</TableCell>
<TableCell>
<Badge variant={statusVariant(order.status)}>
{statusLabel(order.status)}
</Badge>
</TableCell>
<TableCell className="text-muted-foreground">
{created}
</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm" asChild>
<a
href="https://amboss.space/magma"
target="_blank"
rel="noopener noreferrer"
>
<ExternalLink className="mr-1 size-3.5" />
{order.status === 'WAITING_FOR_BUYER_PAYMENT'
? 'Pay on Amboss'
: 'View on Amboss'}
</a>
</Button>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
);
}
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 (
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base">Pending Magma Orders</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<OrderTable orders={purchases} role="buyer" />
<OrderTable orders={sales} role="seller" />
</CardContent>
</Card>
);
};

View file

@ -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 {

View file

@ -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);
});
});
});

View file

@ -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<MagmaPendingOrders | null> {
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)

View file

@ -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 {