mirror of
https://github.com/apotdevin/thunderhub.git
synced 2026-08-13 12:33:08 +02:00
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:
parent
6bfb2a708e
commit
4ff5757b80
10 changed files with 923 additions and 0 deletions
37
schema.gql
37
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!
|
||||
|
|
|
|||
229
src/client/src/graphql/queries/__generated__/getPendingMagmaOrders.generated.tsx
generated
Normal file
229
src/client/src/graphql/queries/__generated__/getPendingMagmaOrders.generated.tsx
generated
Normal 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
|
||||
>;
|
||||
62
src/client/src/graphql/queries/getPendingMagmaOrders.ts
Normal file
62
src/client/src/graphql/queries/getPendingMagmaOrders.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
179
src/client/src/views/channels/magmaOrders/MagmaOrders.tsx
Normal file
179
src/client/src/views/channels/magmaOrders/MagmaOrders.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue