Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ body:json {
],
"identifier": "{{senderWalletAddress}}",
"limits": {
"debitAmount": {{quoteDebitAmount}},
"receiveAmount": {{quoteReceiveAmount}}
"debitAmount": {{quoteDebitAmount}}
}
}
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ body:json {
],
"identifier": "{{senderWalletAddress}}",
"limits": {
"debitAmount": {{quoteDebitAmount}},
"receiveAmount": {{quoteReceiveAmount}}
"debitAmount": {{quoteDebitAmount}}
}
}
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,6 @@ body:json {
"value": "8000",
"assetCode": "USD",
"assetScale": 2
},
"receiveAmount": {
"value": "8000",
"assetCode": "USD",
"assetScale": 2
}
}
}
Expand Down
113 changes: 73 additions & 40 deletions localenv/mock-account-servicing-entity/app/routes/mock-idp._index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ interface GrantAmount {
currencyDisplayCode: string
}

export enum AmountType {
DEBIT = 'debit',
RECEIVE = 'receive',
UNLIMITED = 'unlimited'
}

export function loader() {
return json({ defaultIdpSecret: CONFIG.idpSecret })
}
Expand All @@ -45,8 +51,8 @@ function ConsentScreenBody({
}: {
_thirdPartyUri: string
thirdPartyName: string
price: GrantAmount
costToUser: GrantAmount
price: GrantAmount | null
costToUser: GrantAmount | null
interactId: string
nonce: string
returnUrl: string
Expand Down Expand Up @@ -80,6 +86,15 @@ function ConsentScreenBody({
)}
</div>
</div>
<div className='row mt-2'>
<div className='col-12'>
{!price && !costToUser && (
<p>
{thirdPartyName} is requesting grant for an unlimited amount
</p>
)}
</div>
</div>
<div className='row mt-2'>
<div className='col-12'>Do you consent?</div>
</div>
Expand Down Expand Up @@ -297,21 +312,29 @@ export default function ConsentScreen({ idpSecretParam }: ConsentScreenProps) {
)
returnUrlObject.searchParams.append(
'currencyDisplayCode',
outgoingPaymentAccess && outgoingPaymentAccess.limits
? outgoingPaymentAccess.limits.debitAmount.assetCode
: null
outgoingPaymentAccess?.limits?.debitAmount?.assetCode ??
outgoingPaymentAccess?.limits?.receiveAmount?.assetCode ??
null
)
returnUrlObject.searchParams.append(
'sendAmountValue',
outgoingPaymentAccess && outgoingPaymentAccess.limits
? outgoingPaymentAccess.limits.debitAmount.value
: null
'amountValue',
outgoingPaymentAccess?.limits?.debitAmount?.value ??
outgoingPaymentAccess?.limits?.receiveAmount?.value ??
null
)
returnUrlObject.searchParams.append(
'sendAmountScale',
outgoingPaymentAccess && outgoingPaymentAccess.limits
? outgoingPaymentAccess.limits.debitAmount.assetScale
: null
'amountScale',
outgoingPaymentAccess?.limits?.debitAmount?.assetScale ??
outgoingPaymentAccess?.limits?.receiveAmount?.assetScale ??
null
)
returnUrlObject.searchParams.append(
'amountType',
outgoingPaymentAccess?.limits?.receiveAmount
? AmountType.RECEIVE
: outgoingPaymentAccess?.limits?.debitAmount
? AmountType.DEBIT
: AmountType.UNLIMITED
)
setCtx({
...ctx,
Expand All @@ -337,33 +360,43 @@ export default function ConsentScreen({ idpSecretParam }: ConsentScreenProps) {
ctx.errors.length === 0 &&
ctx.ready &&
ctx.outgoingPaymentAccess &&
(!ctx.price || !ctx.costToUser)
!ctx.price &&
!ctx.costToUser
) {
if (
ctx.outgoingPaymentAccess.limits &&
ctx.outgoingPaymentAccess.limits.debitAmount &&
ctx.outgoingPaymentAccess.limits.receiveAmount
) {
const { receiveAmount, debitAmount } = ctx.outgoingPaymentAccess.limits
setCtx({
...ctx,
price: {
amount:
Number(receiveAmount.value) /
Math.pow(10, receiveAmount.assetScale),
currencyDisplayCode: receiveAmount.assetCode
},
costToUser: {
amount:
Number(debitAmount.value) / Math.pow(10, debitAmount.assetScale),
currencyDisplayCode: debitAmount.assetCode
}
})
} else {
setCtx({
...ctx,
errors: [new Error('missing or incomplete outgoing payment access')]
})
if (ctx.outgoingPaymentAccess.limits) {
if (
ctx.outgoingPaymentAccess.limits.debitAmount &&
ctx.outgoingPaymentAccess.limits.receiveAmount
) {
setCtx({
...ctx,
errors: [
new Error('only one of receiveAmount or debitAmount allowed')
]
})
} else {
const { receiveAmount, debitAmount } =
ctx.outgoingPaymentAccess.limits
setCtx({
...ctx,
...(receiveAmount && {
price: {
amount:
Number(receiveAmount.value) /
Math.pow(10, receiveAmount.assetScale),
currencyDisplayCode: receiveAmount.assetCode
}
}),
...(debitAmount && {
costToUser: {
amount:
Number(debitAmount.value) /
Math.pow(10, debitAmount.assetScale),
currencyDisplayCode: debitAmount.assetCode
}
})
})
}
}
}
}, [ctx, setCtx])
Expand All @@ -379,7 +412,7 @@ export default function ConsentScreen({ idpSecretParam }: ConsentScreenProps) {
</div>
{ctx.ready ? (
<>
{ctx.errors.length > 0 || !ctx.price || !ctx.costToUser ? (
{ctx.errors.length > 0 ? (
<>
<h2 className='display-6'>Failed</h2>
<ul>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { CONFIG as config } from '~/lib/parse_config.server'
import { Button } from '~/components'
import { CheckCircleSolid, XCircle } from '~/components/icons'
import { InstanceConfig } from '~/lib/types'
import { AmountType } from './mock-idp._index'

export function loader() {
return json({
Expand All @@ -24,26 +25,39 @@ function AuthorizedView({
amount,
interactId,
nonce,
authServerDomain
authServerDomain,
amountType
}: {
thirdPartyName: string
currencyDisplayCode: string
amount: number
interactId: string
nonce: string
authServerDomain: string
amountType: string
}) {
let message = `You gave ${thirdPartyName} permission to `
switch (amountType) {
case AmountType.RECEIVE:
message += `receive ${currencyDisplayCode} ${amount.toFixed(2)} in your account.`
break
case AmountType.DEBIT:
message += `send ${currencyDisplayCode} ${amount.toFixed(2)} out of your account.`
break
case AmountType.UNLIMITED:
message += 'have unlimited access to your account.'
break
default:
message = 'Type of authorization is missing'
}
return (
<div className='bg-white rounded-md p-8 px-16'>
<div className='row mt-2 flex flex-row items-center justify-around'>
<div>
<CheckCircleSolid className='w-16 h-16 text-green-400 flex-shrink-0 mr-6' />
</div>
<div>
<p>
You gave {thirdPartyName} permission to send {currencyDisplayCode}{' '}
{amount.toFixed(2)} out of your account.
</p>
<p>{message}</p>
</div>
</div>
<div className='row mt-2'>
Expand Down Expand Up @@ -114,8 +128,9 @@ export default function Consent() {
thirdPartyUri: queryParams.get('thirdPartyUri'),
currencyDisplayCode: queryParams.get('currencyDisplayCode'),
amount:
Number(queryParams.get('sendAmountValue')) /
Math.pow(10, Number(queryParams.get('sendAmountScale')))
Number(queryParams.get('amountValue')) /
Math.pow(10, Number(queryParams.get('amountScale'))),
amountType: queryParams.get('amountType')
})

useEffect(() => {
Expand Down Expand Up @@ -168,6 +183,7 @@ export default function Consent() {
interactId={ctx.interactId}
nonce={ctx.nonce}
authServerDomain={authServerDomain}
amountType={ctx.amountType || ''}
/>
) : (
<RejectedView
Expand Down
21 changes: 21 additions & 0 deletions packages/auth/src/access/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { GNAPErrorCode } from '../shared/gnapErrors'

export enum AccessError {
OnlyOneAccessAmountAllowed = 'only one access amount allowed'
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
export const isAccessError = (o: any): o is AccessError =>
Object.values(AccessError).includes(o)

export const errorToHTTPCode: {
[key in AccessError]: number
} = {
[AccessError.OnlyOneAccessAmountAllowed]: 400
}

export const errorToGNAPCode: {
[key in AccessError]: GNAPErrorCode
} = {
[AccessError.OnlyOneAccessAmountAllowed]: GNAPErrorCode.InvalidRequest
}
38 changes: 33 additions & 5 deletions packages/auth/src/access/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { AppServices } from '../app'
import { AccessService } from './service'
import { Grant, GrantState, StartMethod, FinishMethod } from '../grant/model'
import { IncomingPaymentRequest, OutgoingPaymentRequest } from './types'
import { AccessError } from './errors'
import { generateNonce, generateToken } from '../shared/utils'
import { AccessType, AccessAction } from '@interledger/open-payments'
import { Access } from './model'
Expand Down Expand Up @@ -75,11 +76,6 @@ describe('Access Service', (): void => {
assetCode: 'usd',
assetScale: 9
},
receiveAmount: {
value: '2000000000',
assetCode: 'usd',
assetScale: 9
},
expiresAt: new Date().toISOString(),
receiver: 'https://wallet.com/alice'
}
Expand All @@ -99,6 +95,38 @@ describe('Access Service', (): void => {
expect(access[0].type).toEqual(AccessType.OutgoingPayment)
expect(access[0].limits).toEqual(outgoingPaymentLimit)
})

test('Does not create outgoing payment access when both receiveAmount and debitAmount are provided to limits', async (): Promise<void> => {
const outgoingPaymentLimit = {
debitAmount: {
value: '1000000000',
assetCode: 'usd',
assetScale: 9
},
receiveAmount: {
value: '1000000000',
assetCode: 'usd',
assetScale: 9
},
expiresAt: new Date().toISOString(),
receiver: 'https://wallet.com/alice'
}

const outgoingPaymentAccess: OutgoingPaymentRequest = {
type: 'outgoing-payment',
actions: [AccessAction.Create, AccessAction.Read, AccessAction.List],
limits: outgoingPaymentLimit
}

try {
await accessService.createAccess(grant.id, [outgoingPaymentAccess])
fail(
'Expected createAccess to throw OnlyOneAccessAmountAllowed, but no error was thrown'
)
} catch (err) {
expect(err).toBe(AccessError.OnlyOneAccessAmountAllowed)
}
})
})

describe('getByGrant', (): void => {
Expand Down
13 changes: 13 additions & 0 deletions packages/auth/src/access/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Transaction, TransactionOrKnex } from 'objection'
import { BaseService } from '../shared/baseService'
import { Access } from './model'
import { AccessRequest } from './types'
import { AccessError } from './errors'

export interface AccessService {
createAccess(
Expand Down Expand Up @@ -47,6 +48,8 @@ async function createAccess(
accessRequests: AccessRequest[],
trx?: Transaction
): Promise<Access[]> {
validateLimits(accessRequests)

const accessRequestsWithGrant = accessRequests.map((access) => {
return { grantId, ...access }
})
Expand All @@ -63,3 +66,13 @@ async function getByGrant(
grantId
})
}

function validateLimits(accessRequests: AccessRequest[]) {
const areBothLimitsSet = accessRequests.some(
(access) => access.limits?.debitAmount && access.limits.receiveAmount
)

if (areBothLimitsSet) {
throw AccessError.OnlyOneAccessAmountAllowed
}
}
Loading
Loading