-
Notifications
You must be signed in to change notification settings - Fork 89
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(open-payments): add create quote #888
Conversation
@@ -32,8 +33,15 @@ export const getQuote: QueryResolvers<ApolloContext>['quote'] = async ( | |||
export const createQuote: MutationResolvers<ApolloContext>['createQuote'] = | |||
async (parent, args, ctx): Promise<ResolversTypes['QuoteResponse']> => { | |||
const quoteService = await ctx.container.use('quoteService') | |||
const options: CreateQuoteOptions = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because graphql doesn't do mutually exclusive types (:disappointed:), I think we need to move the old check from the quote service here
rafiki/packages/backend/src/open_payments/quote/service.ts
Lines 76 to 78 in 4db6f8e
if (options.sendAmount && options.receiveAmount) { | |
return QuoteError.InvalidAmount | |
} |
const options: CreateQuoteOptions = { | |
if (args.input.sendAmount && args.input.receiveAmount) { | |
return { | |
code: errorToCode[QuoteError.InvalidAmount].toString(), | |
success: false, | |
message: errorToMessage[QuoteError.InvalidAmount] | |
} | |
} | |
const options: CreateQuoteOptions = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Initially I was looking at the @oneOf
directive (RFC). Even though it's available in a couple of packages, is still a feature from the future and the behavior could change slightly. It would be nice if we can enforce the types at the GraphQL schema later on.
While playing around with it, I got to this point:
// schema.graphql
input QuoteAmount @oneOf {
sendAmount: AmountInput
receiveAmount: AmountInput
}
input CreateQuoteInput {
paymentPointerId: String!
amount: QuoteAmount
receiver: String!
}
// graphql.generated.ts
export type QuoteAmount =
{ sendAmount: AmountInput; receiveAmount?: never; }
| { sendAmount?: never; receiveAmount: AmountInput; };
export type CreateQuoteInput = {
paymentPointerId: Scalars['String'];
amount?: InputMaybe<QuoteAmount>;
receiver: Scalars['String'];
};
I think it partially fulfills our needs for the CreateQuoteInput
. 🤔
sendAmount: body.sendAmount && parseAmount(body.sendAmount), | ||
receiveAmount: body.receiveAmount && parseAmount(body.receiveAmount), | ||
clientId: ctx.grant?.clientId | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you should be able to make sendAmount
and receiveAmount
mutually exclusive in CreateBody
(like with CreateQuoteOptions
) and leave this as it was before.
This is enforced by the OpenAPI request validator thanks to:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the changes to the CreateBody
type, it's still complaining:
Types of property 'sendAmount' are incompatible. Type 'Amount' is not assignable to type 'never'
But I think we should keep the changes to CreateBody
.
@@ -15,7 +15,15 @@ export async function createOutgoingPayment( | |||
'quoteId' | |||
> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you change options
to 👇 you won't need the changes below
options: CreateTestQuoteOptions &
Omit<CreateOutgoingPaymentOptions, 'quoteId'>
but then it looks like there's some calls to createOutgoingPayment
that would need to updated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔 I think we should keep the type as it was before, since our changes are affecting only the createQuote
function ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think about refactoring createOutgoingPayment
later on, to something like:
function createOutgoingPayment(
deps: IocContract<AppServices>,
quoteOptions: CreateTestQuoteOptions,
outgoingPaymentOptions: Omit<CreateOutgoingPaymentOptions, 'quoteId'>
): Promise<OutgoingPayment>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good, approving with some comments
} | ||
|
||
export interface QuoteRoutes { | ||
get(args: GetArgs): Promise<Quote> | ||
create(args: PostArgs<CreateQuoteArgs>): Promise<Quote> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For create
method and list
methods, I was thinking we would do something like this:
interface CreateArgs { // or something along these lines
paymentPointer: string
accessToken: string
}
```suggestion
create(createArgs: CreateArgs, createQuoteArgs: CreateQuoteArgs): Promise<Quote>
and then
const url = `${paymentPointer}${getRSPath('/quotes')}`
when making the actual POST
call. This is so that the client doesn't need to provide the full route.
Let me know what you think
@wilsonianb had a discussion about it here: #823 (comment)
packages/open-payments/src/types.ts
Outdated
export type CreateQuoteArgs = | ||
RSOperations['create-quote']['requestBody']['content']['application/json'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we would also need to do explicitly provide sendAmount: never
or receiveAmount: never
like you have above here as well, since having just this, typescript won't complain if we do
.create({
url,
accessToken,
body: {
receiver: quote.receiver,
sendAmount: {
assetCode: '4',
assetScale: 4,
value: '4'
},
receiveAmount: {
assetCode: '4',
assetScale: 4,
value: '4'
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could even use the XOR
helper type, if you'd like: https://stackoverflow.com/a/53229567
if (options.sendAmount && options.receiveAmount) { | ||
return QuoteError.InvalidAmount | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@raducristianpopa @wilsonianb
I'm leaning on keeping this business logic sanity check here (even though the OpenAPI validator would prevent it). If there are some issues with validator we would still be ok in the service
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An alternative could be to move the check to routes.ts
(like we had to do with the graphql resolver), but it'd make more sense to just keep the one check here.
So I vote we drop the resolver check in favor of this, and maybe we revisit if/when we have graphql @oneOf
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔 do we only change CreateBody
then, and revert CreateQuoteOptions
?
I think that'd avoid having to change the tests' createOutgoingPayment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔 i think we should keep the changes for the CreateQuoteOptions
type for consistency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My only reservation about that is we'd also need to check in the resolver.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the resolver will need the check in order to be able to call quoteService.create
if the options
type is:
export type CreateQuoteOptions =
| QuoteOptionsWithSendAmount
| QuoteOptionsWithReceiveAmount
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the actual changes to the resolver:
rafiki/packages/backend/src/graphql/resolvers/quote.ts
Lines 33 to 46 in 1b5c01a
export const createQuote: MutationResolvers<ApolloContext>['createQuote'] = | |
async (parent, args, ctx): Promise<ResolversTypes['QuoteResponse']> => { | |
const quoteService = await ctx.container.use('quoteService') | |
const options: CreateQuoteOptions = { | |
paymentPointerId: args.input.paymentPointerId, | |
receiver: args.input.receiver | |
} | |
if (args.input.sendAmount) options.sendAmount = args.input.sendAmount | |
if (args.input.receiveAmount) | |
options.receiveAmount = args.input.receiveAmount | |
return quoteService | |
.create(options) | |
.then((quoteOrErr: Quote | QuoteError) => | |
isQuoteError(quoteOrErr) |
i was able to make a GraphQL request with both
sendAmount
and receiveAmount
.quoteService.create
gets called properly even with the options
type being
export type CreateQuoteOptions =
| QuoteOptionsWithSendAmount
| QuoteOptionsWithReceiveAmount
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😕
I think declaring options
as CreateQuoteOptions
makes the call to quoteService.create
happy.
But I'm surprised we don't need the receiveAmount
assignment in an else if (args.input.receiveAmount)
.
Maybe it's due to:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've just enabled strict type checking for the backend
package, but TypeScript it's still not complaining about it. 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's not complaining because both receiveAmount
and sendAmount
are optional in their respective types
if (options.sendAmount && options.receiveAmount) { | ||
return QuoteError.InvalidAmount | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An alternative could be to move the check to routes.ts
(like we had to do with the graphql resolver), but it'd make more sense to just keep the one check here.
So I vote we drop the resolver check in favor of this, and maybe we revisit if/when we have graphql @oneOf
.
if (options.sendAmount && options.receiveAmount) { | ||
return QuoteError.InvalidAmount | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔 do we only change CreateBody
then, and revert CreateQuoteOptions
?
I think that'd avoid having to change the tests' createOutgoingPayment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good with the latest changes, just need to resolve merge conflicts and good to go
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I gave you more conflicts to resolve...
Resolved the conflicts. My final thoughts:
|
Changes proposed in this pull request
backend
Context
Checklist
fixes #number