diff --git a/localenv/mock-account-servicing-entity/generated/graphql.ts b/localenv/mock-account-servicing-entity/generated/graphql.ts index d5b26bbf7e..2dc0e4bfb2 100644 --- a/localenv/mock-account-servicing-entity/generated/graphql.ts +++ b/localenv/mock-account-servicing-entity/generated/graphql.ts @@ -998,6 +998,8 @@ export type Peer = Model & { http: Http; /** Peer id */ id: Scalars['ID']['output']; + /** Incoming tokens */ + incomingTokens: Array; /** Available liquidity */ liquidity?: Maybe; /** Account Servicing Entity will be notified via a webhook event if peer liquidity falls below this value */ @@ -2130,6 +2132,7 @@ export type PeerResolvers; http?: Resolver; id?: Resolver; + incomingTokens?: Resolver, ParentType, ContextType>; liquidity?: Resolver, ParentType, ContextType>; liquidityThreshold?: Resolver, ParentType, ContextType>; maxPacketAmount?: Resolver, ParentType, ContextType>; diff --git a/packages/backend/src/graphql/generated/graphql.schema.json b/packages/backend/src/graphql/generated/graphql.schema.json index a43aafe99d..ca49efbe3f 100644 --- a/packages/backend/src/graphql/generated/graphql.schema.json +++ b/packages/backend/src/graphql/generated/graphql.schema.json @@ -5890,6 +5890,30 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "incomingTokens", + "description": "Incoming tokens", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "liquidity", "description": "Available liquidity", diff --git a/packages/backend/src/graphql/generated/graphql.ts b/packages/backend/src/graphql/generated/graphql.ts index d5b26bbf7e..2dc0e4bfb2 100644 --- a/packages/backend/src/graphql/generated/graphql.ts +++ b/packages/backend/src/graphql/generated/graphql.ts @@ -998,6 +998,8 @@ export type Peer = Model & { http: Http; /** Peer id */ id: Scalars['ID']['output']; + /** Incoming tokens */ + incomingTokens: Array; /** Available liquidity */ liquidity?: Maybe; /** Account Servicing Entity will be notified via a webhook event if peer liquidity falls below this value */ @@ -2130,6 +2132,7 @@ export type PeerResolvers; http?: Resolver; id?: Resolver; + incomingTokens?: Resolver, ParentType, ContextType>; liquidity?: Resolver, ParentType, ContextType>; liquidityThreshold?: Resolver, ParentType, ContextType>; maxPacketAmount?: Resolver, ParentType, ContextType>; diff --git a/packages/backend/src/graphql/resolvers/peer.ts b/packages/backend/src/graphql/resolvers/peer.ts index b2296b7a89..d492026649 100644 --- a/packages/backend/src/graphql/resolvers/peer.ts +++ b/packages/backend/src/graphql/resolvers/peer.ts @@ -122,6 +122,9 @@ export const peerToGraphql = (peer: Peer): SchemaPeer => ({ id: peer.id, maxPacketAmount: peer.maxPacketAmount, http: peer.http, + incomingTokens: peer.incomingTokens?.map( + (incomingToken) => incomingToken.token + ), asset: assetToGraphql(peer.asset), staticIlpAddress: peer.staticIlpAddress, name: peer.name, diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index cc7309c3d5..5de35ef09c 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -619,6 +619,8 @@ type Peer implements Model { maxPacketAmount: UInt64 "Peering connection details" http: Http! + "Incoming tokens" + incomingTokens: [String!]! "Asset of peering relationship" asset: Asset! "Peer's ILP address" diff --git a/packages/backend/src/payment-method/ilp/peer/service.ts b/packages/backend/src/payment-method/ilp/peer/service.ts index be64dcc520..a2a420644e 100644 --- a/packages/backend/src/payment-method/ilp/peer/service.ts +++ b/packages/backend/src/payment-method/ilp/peer/service.ts @@ -114,7 +114,10 @@ async function getPeer( deps: ServiceDependencies, id: string ): Promise { - return Peer.query(deps.knex).findById(id).withGraphFetched('asset') + return Peer.query(deps.knex) + .findById(id) + .withGraphFetched('asset') + .withGraphFetched('incomingTokens') } async function createPeer( @@ -222,14 +225,16 @@ async function updatePeer( return await Peer.transaction(deps.knex, async (trx) => { if (options.http?.incoming) { await deps.httpTokenService.deleteByPeer(options.id, trx) - const err = await addIncomingHttpTokens({ - deps, - peerId: options.id, - tokens: options.http?.incoming?.authTokens, - trx - }) - if (err) { - throw err + if (options.http?.incoming?.authTokens.length > 0) { + const err = await addIncomingHttpTokens({ + deps, + peerId: options.id, + tokens: options.http?.incoming?.authTokens, + trx + }) + if (err) { + throw err + } } } return await Peer.query(trx) diff --git a/packages/frontend/app/components/ui/EditableTable.tsx b/packages/frontend/app/components/ui/EditableTable.tsx new file mode 100644 index 0000000000..f903674bbe --- /dev/null +++ b/packages/frontend/app/components/ui/EditableTable.tsx @@ -0,0 +1,149 @@ +import type { ReactNode } from 'react' +import { useEffect, useState } from 'react' +import { Input } from './Input' +import { Table } from './Table' +import { FieldError } from './FieldError' +import { Button } from './Button' + +type EditableTableProps = { + name: string + label: string + options: EditableTableOption[] + error?: string | string[] + description?: ReactNode + required?: boolean +} + +type EditableTableOption = { + label: string + value: string + canDelete?: boolean + canEdit?: boolean + showInput?: boolean +} + +export const EditableTable = ({ + name, + label, + options, + error, + description = undefined, + required = false +}: EditableTableProps) => { + const [optionsList, setOptionsList] = useState(options) + const [values, setValues] = useState() + + const toggleEditInput = (index: number) => { + setOptionsList( + optionsList.map((option, i) => { + if (i === index) { + return { + ...option, + showInput: true + } + } + return option + }) + ) + } + + const editOption = (index: number, value: string) => { + if (!value) { + deleteOption(index) + return + } + setOptionsList( + optionsList.map((option, i) => { + if (i === index) { + return { + ...option, + showInput: false, + value + } + } + return option + }) + ) + } + + const deleteOption = (index: number) => { + setOptionsList(optionsList.filter((_, i) => i !== index)) + } + + const addOption = () => { + setOptionsList([ + ...optionsList, + { label: '', value: '', canDelete: true, canEdit: true, showInput: true } + ]) + } + + useEffect( + () => setValues(optionsList.map((option) => option.value)), + [optionsList] + ) + + return ( + <> + + + + + {(optionsList || []).map((option, index) => ( + + + {option.showInput ? ( + + e.key === 'Enter' && + (e.preventDefault(), + editOption(index, e.currentTarget.value)) + } + defaultValue={option.value} + required={required} + /> + ) : ( + {option.value} + )} + + + {option.canEdit && !option.showInput ? ( + + ) : null} + {option.canDelete ? ( + + ) : null} + + + ))} + +
+
+ +
+ {description ? ( +
{description}
+ ) : null} + + + ) +} diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index bc7e5fabb1..9efa296887 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -998,6 +998,8 @@ export type Peer = Model & { http: Http; /** Peer id */ id: Scalars['ID']['output']; + /** Incoming tokens */ + incomingTokens: Array; /** Available liquidity */ liquidity?: Maybe; /** Account Servicing Entity will be notified via a webhook event if peer liquidity falls below this value */ @@ -2130,6 +2132,7 @@ export type PeerResolvers; http?: Resolver; id?: Resolver; + incomingTokens?: Resolver, ParentType, ContextType>; liquidity?: Resolver, ParentType, ContextType>; liquidityThreshold?: Resolver, ParentType, ContextType>; maxPacketAmount?: Resolver, ParentType, ContextType>; @@ -2515,7 +2518,7 @@ export type GetPeerQueryVariables = Exact<{ }>; -export type GetPeerQuery = { __typename?: 'Query', peer?: { __typename?: 'Peer', id: string, name?: string | null, staticIlpAddress: string, maxPacketAmount?: bigint | null, liquidity?: bigint | null, createdAt: string, asset: { __typename?: 'Asset', id: string, code: string, scale: number, withdrawalThreshold?: bigint | null }, http: { __typename?: 'Http', outgoing: { __typename?: 'HttpOutgoing', endpoint: string, authToken: string } } } | null }; +export type GetPeerQuery = { __typename?: 'Query', peer?: { __typename?: 'Peer', id: string, name?: string | null, staticIlpAddress: string, maxPacketAmount?: bigint | null, liquidity?: bigint | null, createdAt: string, incomingTokens: Array, asset: { __typename?: 'Asset', id: string, code: string, scale: number, withdrawalThreshold?: bigint | null }, http: { __typename?: 'Http', outgoing: { __typename?: 'HttpOutgoing', endpoint: string, authToken: string } } } | null }; export type ListPeersQueryVariables = Exact<{ after?: InputMaybe; diff --git a/packages/frontend/app/lib/api/peer.server.ts b/packages/frontend/app/lib/api/peer.server.ts index fd064ed0c5..b6a3d8d549 100644 --- a/packages/frontend/app/lib/api/peer.server.ts +++ b/packages/frontend/app/lib/api/peer.server.ts @@ -50,6 +50,7 @@ export const getPeer = async (args: QueryPeerArgs) => { authToken } } + incomingTokens } } `, diff --git a/packages/frontend/app/lib/validate.server.ts b/packages/frontend/app/lib/validate.server.ts index ca74197fe3..c48401e800 100644 --- a/packages/frontend/app/lib/validate.server.ts +++ b/packages/frontend/app/lib/validate.server.ts @@ -47,7 +47,7 @@ export const peerGeneralInfoSchema = z export const peerHttpInfoSchema = z .object({ - incomingAuthTokens: z.string().optional(), + incomingAuthTokens: z.array(z.string()), outgoingAuthToken: z.string(), outgoingEndpoint: z .string() diff --git a/packages/frontend/app/routes/peers.$peerId.tsx b/packages/frontend/app/routes/peers.$peerId.tsx index 20885c65e9..b49d4d5882 100644 --- a/packages/frontend/app/routes/peers.$peerId.tsx +++ b/packages/frontend/app/routes/peers.$peerId.tsx @@ -30,6 +30,7 @@ import { import type { ZodFieldErrors } from '~/shared/types' import { formatAmount } from '~/shared/utils' import { checkAuthAndRedirect } from '../lib/kratos_checks.server' +import { EditableTable } from '~/components/ui/EditableTable' export async function loader({ request, params }: LoaderFunctionArgs) { const cookies = request.headers.get('cookie') @@ -204,10 +205,15 @@ export default function ViewPeerPage() {
- ({ + label: token, + value: token, + canDelete: true, + canEdit: true + }))} error={response?.errors.http.fieldErrors.incomingAuthTokens} description={ <> @@ -428,14 +434,18 @@ export async function action({ request }: ActionFunctionArgs) { break } case 'http': { - const result = peerHttpInfoSchema.safeParse(Object.fromEntries(formData)) - + const formDataEntries = Object.fromEntries(formData) + const result = peerHttpInfoSchema.safeParse({ + ...formDataEntries, + incomingAuthTokens: formDataEntries.incomingAuthTokens + ? formDataEntries.incomingAuthTokens.toString().split(',') + : [] + }) if (!result.success) { actionResponse.errors.http.fieldErrors = result.error.flatten().fieldErrors return json({ ...actionResponse }, { status: 400 }) } - const response = await updatePeer({ id: result.data.id, http: { @@ -443,8 +453,6 @@ export async function action({ request }: ActionFunctionArgs) { ? { incoming: { authTokens: result.data.incomingAuthTokens - ?.replace(/ /g, '') - .split(',') } } : {}), diff --git a/packages/mock-account-service-lib/src/generated/graphql.ts b/packages/mock-account-service-lib/src/generated/graphql.ts index d5b26bbf7e..2dc0e4bfb2 100644 --- a/packages/mock-account-service-lib/src/generated/graphql.ts +++ b/packages/mock-account-service-lib/src/generated/graphql.ts @@ -998,6 +998,8 @@ export type Peer = Model & { http: Http; /** Peer id */ id: Scalars['ID']['output']; + /** Incoming tokens */ + incomingTokens: Array; /** Available liquidity */ liquidity?: Maybe; /** Account Servicing Entity will be notified via a webhook event if peer liquidity falls below this value */ @@ -2130,6 +2132,7 @@ export type PeerResolvers; http?: Resolver; id?: Resolver; + incomingTokens?: Resolver, ParentType, ContextType>; liquidity?: Resolver, ParentType, ContextType>; liquidityThreshold?: Resolver, ParentType, ContextType>; maxPacketAmount?: Resolver, ParentType, ContextType>; diff --git a/test/integration/lib/generated/graphql.ts b/test/integration/lib/generated/graphql.ts index d5b26bbf7e..2dc0e4bfb2 100644 --- a/test/integration/lib/generated/graphql.ts +++ b/test/integration/lib/generated/graphql.ts @@ -998,6 +998,8 @@ export type Peer = Model & { http: Http; /** Peer id */ id: Scalars['ID']['output']; + /** Incoming tokens */ + incomingTokens: Array; /** Available liquidity */ liquidity?: Maybe; /** Account Servicing Entity will be notified via a webhook event if peer liquidity falls below this value */ @@ -2130,6 +2132,7 @@ export type PeerResolvers; http?: Resolver; id?: Resolver; + incomingTokens?: Resolver, ParentType, ContextType>; liquidity?: Resolver, ParentType, ContextType>; liquidityThreshold?: Resolver, ParentType, ContextType>; maxPacketAmount?: Resolver, ParentType, ContextType>;