diff --git a/schema.gql b/schema.gql index ca07d4e8..bcc323bc 100644 --- a/schema.gql +++ b/schema.gql @@ -445,7 +445,7 @@ type Mutation { lnUrlWithdraw(amount: Float!, callback: String!, description: String, k1: String!): String! loginAmboss: Boolean! logout: Boolean! - openChannel(amount: Float!, isPrivate: Boolean, partnerPublicKey: String!, pushTokens: Float = 0, tokensPerVByte: Float): OpenOrCloseChannel! + openChannel(input: OpenChannelParams!): OpenOrCloseChannel! pay(max_fee: Float!, max_paths: Float!, out: [String!], request: String!): Boolean! pushBackup: Boolean! removePeer(publicKey: String): Boolean! @@ -548,6 +548,17 @@ type OnChainBalance { pending: String! } +input OpenChannelParams { + base_fee_mtokens: String + chain_fee_tokens_per_vbyte: Float + channel_size: Float + fee_rate: Float + give_tokens: Float = 0 + is_max_funding: Boolean + is_private: Boolean + partner_public_key: String! +} + type OpenOrCloseChannel { transactionId: String! transactionOutputIndex: String! diff --git a/src/client/src/graphql/mutations/__generated__/openChannel.generated.tsx b/src/client/src/graphql/mutations/__generated__/openChannel.generated.tsx index 78606d5d..c17ac9e0 100644 --- a/src/client/src/graphql/mutations/__generated__/openChannel.generated.tsx +++ b/src/client/src/graphql/mutations/__generated__/openChannel.generated.tsx @@ -4,11 +4,7 @@ import { gql } from '@apollo/client'; import * as Apollo from '@apollo/client'; const defaultOptions = {} as const; export type OpenChannelMutationVariables = Types.Exact<{ - amount: Types.Scalars['Float']['input']; - partnerPublicKey: Types.Scalars['String']['input']; - tokensPerVByte?: Types.InputMaybe; - isPrivate?: Types.InputMaybe; - pushTokens?: Types.InputMaybe; + input: Types.OpenChannelParams; }>; export type OpenChannelMutation = { @@ -21,20 +17,8 @@ export type OpenChannelMutation = { }; export const OpenChannelDocument = gql` - mutation OpenChannel( - $amount: Float! - $partnerPublicKey: String! - $tokensPerVByte: Float - $isPrivate: Boolean - $pushTokens: Float - ) { - openChannel( - amount: $amount - partnerPublicKey: $partnerPublicKey - tokensPerVByte: $tokensPerVByte - isPrivate: $isPrivate - pushTokens: $pushTokens - ) { + mutation OpenChannel($input: OpenChannelParams!) { + openChannel(input: $input) { transactionId transactionOutputIndex } @@ -58,11 +42,7 @@ export type OpenChannelMutationFn = Apollo.MutationFunction< * @example * const [openChannelMutation, { data, loading, error }] = useOpenChannelMutation({ * variables: { - * amount: // value for 'amount' - * partnerPublicKey: // value for 'partnerPublicKey' - * tokensPerVByte: // value for 'tokensPerVByte' - * isPrivate: // value for 'isPrivate' - * pushTokens: // value for 'pushTokens' + * input: // value for 'input' * }, * }); */ diff --git a/src/client/src/graphql/mutations/openChannel.ts b/src/client/src/graphql/mutations/openChannel.ts index 4a1d2762..c97247e0 100644 --- a/src/client/src/graphql/mutations/openChannel.ts +++ b/src/client/src/graphql/mutations/openChannel.ts @@ -1,20 +1,8 @@ import { gql } from '@apollo/client'; export const OPEN_CHANNEL = gql` - mutation OpenChannel( - $amount: Float! - $partnerPublicKey: String! - $tokensPerVByte: Float - $isPrivate: Boolean - $pushTokens: Float - ) { - openChannel( - amount: $amount - partnerPublicKey: $partnerPublicKey - tokensPerVByte: $tokensPerVByte - isPrivate: $isPrivate - pushTokens: $pushTokens - ) { + mutation OpenChannel($input: OpenChannelParams!) { + openChannel(input: $input) { transactionId transactionOutputIndex } diff --git a/src/client/src/graphql/types.ts b/src/client/src/graphql/types.ts index 0f610172..9606a8a3 100644 --- a/src/client/src/graphql/types.ts +++ b/src/client/src/graphql/types.ts @@ -567,7 +567,7 @@ export type MutationCloseChannelArgs = { }; export type MutationCreateAddressArgs = { - type?: InputMaybe; + type?: Scalars['String']['input']; }; export type MutationCreateBaseInvoiceArgs = { @@ -648,11 +648,7 @@ export type MutationLnUrlWithdrawArgs = { }; export type MutationOpenChannelArgs = { - amount: Scalars['Float']['input']; - isPrivate?: InputMaybe; - partnerPublicKey: Scalars['String']['input']; - pushTokens?: InputMaybe; - tokensPerVByte?: InputMaybe; + input: OpenChannelParams; }; export type MutationPayArgs = { @@ -807,6 +803,17 @@ export type OnChainBalance = { pending: Scalars['String']['output']; }; +export type OpenChannelParams = { + base_fee_mtokens?: InputMaybe; + chain_fee_tokens_per_vbyte?: InputMaybe; + channel_size?: InputMaybe; + fee_rate?: InputMaybe; + give_tokens?: InputMaybe; + is_max_funding?: InputMaybe; + is_private?: InputMaybe; + partner_public_key: Scalars['String']['input']; +}; + export type OpenOrCloseChannel = { __typename?: 'OpenOrCloseChannel'; transactionId: Scalars['String']['output']; diff --git a/src/client/src/views/home/quickActions/openChannel/OpenChannel.tsx b/src/client/src/views/home/quickActions/openChannel/OpenChannel.tsx index 24e046d8..5a492df1 100644 --- a/src/client/src/views/home/quickActions/openChannel/OpenChannel.tsx +++ b/src/client/src/views/home/quickActions/openChannel/OpenChannel.tsx @@ -48,8 +48,12 @@ export const OpenChannelCard = ({ const [fee, setFee] = useState(0); const [publicKey, setPublicKey] = useState(initialPublicKey); const [privateChannel, setPrivateChannel] = useState(false); + const [isMaxFunding, setIsMaxFunding] = useState(false); const [type, setType] = useState('fee'); + const [feeRate, setFeeRate] = useState(null); + const [baseFee, setBaseFee] = useState(null); + const [openChannel, { loading }] = useOpenChannelMutation({ onError: error => toast.error(getErrorContent(error)), onCompleted: () => { @@ -59,7 +63,7 @@ export const OpenChannelCard = ({ refetchQueries: ['GetChannels', 'GetPendingChannels'], }); - const canOpen = publicKey !== '' && size > 0 && fee > 0; + const canOpen = publicKey !== '' && (size > 0 || isMaxFunding) && fee > 0; const pushAmount = pushType === 'none' @@ -164,13 +168,57 @@ export const OpenChannelCard = ({ /> )} + + + {renderButton( + () => { + setIsMaxFunding(true); + setSize(0); + }, + 'Yes', + isMaxFunding + )} + {renderButton(() => setIsMaxFunding(false), 'No', !isMaxFunding)} + + + {!isMaxFunding ? ( + setSize(Number(value))} + /> + ) : null} + + { + if (value == null) { + setFeeRate(null); + } else { + setFeeRate(Number(value)); + } + }} + /> setSize(Number(value))} + inputCallback={value => { + if (value == null) { + setBaseFee(null); + } else { + setBaseFee(Number(value)); + } + }} /> {fetchFees && !dontShow && ( @@ -243,11 +291,16 @@ export const OpenChannelCard = ({ onClick={() => openChannel({ variables: { - amount: size, - partnerPublicKey: publicKey || '', - tokensPerVByte: fee, - isPrivate: privateChannel, - pushTokens: pushAmount, + input: { + channel_size: size, + partner_public_key: publicKey || '', + is_private: privateChannel, + is_max_funding: isMaxFunding, + give_tokens: pushAmount, + chain_fee_tokens_per_vbyte: fee, + base_fee_mtokens: baseFee == null ? undefined : baseFee + '', + fee_rate: feeRate, + }, }, }) } diff --git a/src/server/modules/api/channels/channels.resolver.ts b/src/server/modules/api/channels/channels.resolver.ts index ac70ba3b..46aa4ae6 100644 --- a/src/server/modules/api/channels/channels.resolver.ts +++ b/src/server/modules/api/channels/channels.resolver.ts @@ -10,11 +10,13 @@ import { getChannelAge } from './channels.helpers'; import { Channel, ClosedChannel, + OpenChannelParams, OpenOrCloseChannel, PendingChannel, SingleChannel, UpdateRoutingFeesParams, } from './channels.types'; +import { GraphQLError } from 'graphql'; @Resolver() export class ChannelsResolver { @@ -130,34 +132,54 @@ export class ChannelsResolver { @Mutation(() => OpenOrCloseChannel) async openChannel( @CurrentUser() user: UserId, - @Args('amount') local_tokens: number, - @Args('partnerPublicKey') partner_public_key: string, - @Args('isPrivate', { nullable: true }) is_private: boolean, - @Args('pushTokens', { nullable: true, defaultValue: 0 }) pushTokens: number, - @Args('tokensPerVByte', { nullable: true }) - chain_fee_tokens_per_vbyte: number + @Args('input') input: OpenChannelParams ) { + const { + channel_size = 0, + partner_public_key, + is_private, + is_max_funding, + give_tokens = 0, + chain_fee_tokens_per_vbyte, + base_fee_mtokens, + fee_rate, + } = input; + + if (!channel_size && !is_max_funding) { + throw new GraphQLError('You need to specify a channel size.'); + } + + this.logger.info('Starting opening channel attempt', { input }); + let public_key = partner_public_key; if (partner_public_key.indexOf('@') >= 0) { + this.logger.info('Connecting to new peer', { partner_public_key }); + const parts = partner_public_key.split('@'); public_key = parts[0]; await this.nodeService.addPeer(user.id, public_key, parts[1], false); + + this.logger.info(`Connected to new peer`, { partner_public_key }); } const openParams = { - is_private, - local_tokens, - chain_fee_tokens_per_vbyte, + local_tokens: channel_size, partner_public_key: public_key, - give_tokens: Math.min(pushTokens, local_tokens), + ...(is_private ? { is_private } : {}), + ...(give_tokens ? { give_tokens } : {}), + ...(chain_fee_tokens_per_vbyte ? { chain_fee_tokens_per_vbyte } : {}), + ...(is_max_funding ? { is_max_funding } : {}), + ...(base_fee_mtokens ? { base_fee_mtokens } : {}), + ...(fee_rate ? { fee_rate } : {}), }; - this.logger.info('Opening channel with params', { openParams }); - const info = await this.nodeService.openChannel(user.id, openParams); - this.logger.info('Channel opened'); + this.logger.info('Channel opened with params', { + params: openParams, + result: info, + }); return { transactionId: info.transaction_id, diff --git a/src/server/modules/api/channels/channels.types.ts b/src/server/modules/api/channels/channels.types.ts index 851cb7df..77019d5f 100644 --- a/src/server/modules/api/channels/channels.types.ts +++ b/src/server/modules/api/channels/channels.types.ts @@ -262,3 +262,23 @@ export class UpdateRoutingFeesParams { @Field({ nullable: true }) min_htlc_mtokens?: string; } + +@InputType() +export class OpenChannelParams { + @Field() + partner_public_key: string; + @Field({ nullable: true }) + channel_size: number; + @Field({ nullable: true }) + is_private: boolean; + @Field({ nullable: true }) + is_max_funding: boolean; + @Field({ nullable: true, defaultValue: 0 }) + give_tokens: number; + @Field({ nullable: true }) + chain_fee_tokens_per_vbyte: number; + @Field({ nullable: true }) + base_fee_mtokens: string; + @Field({ nullable: true }) + fee_rate: number; +}