Skip to content

Commit

Permalink
Wire price service in the evaluation step
Browse files Browse the repository at this point in the history
  • Loading branch information
wcalderipe committed Jan 30, 2024
1 parent 162d892 commit 24e80ba
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 40 deletions.
8 changes: 1 addition & 7 deletions apps/orchestration/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,7 @@ const withSwagger = (app: INestApplication): INestApplication => {
.setVersion('1.0')
.build()
)
SwaggerModule.setup('docs', app, document, {
swaggerOptions: {
// Temporary disable the "Try it out" button while the API is just a
// placeholder.
supportedSubmitMethods: []
}
})
SwaggerModule.setup('docs', app, document)

return app
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
generateSignature
} from '@app/orchestration/__test__/fixture/authorization-request.fixture'
import { generateTransferFeed } from '@app/orchestration/__test__/fixture/transfer-feed.fixture'
import { FIAT_ID_USD } from '@app/orchestration/orchestration.constant'
import { AuthorizationRequestService } from '@app/orchestration/policy-engine/core/service/authorization-request.service'
import {
Approval,
Expand All @@ -18,20 +19,22 @@ import {
} from '@app/orchestration/policy-engine/http/client/authz-application.client'
import { AuthorizationRequestRepository } from '@app/orchestration/policy-engine/persistence/repository/authorization-request.repository'
import { AuthorizationRequestProcessingProducer } from '@app/orchestration/policy-engine/queue/producer/authorization-request-processing.producer'
import { PriceService } from '@app/orchestration/price/core/service/price.service'
import { Transfer } from '@app/orchestration/shared/core/type/transfer-feed.type'
import { TransferFeedService } from '@app/orchestration/transfer-feed/core/service/transfer-feed.service'
import { AccountId, Action, AssetId, Decision } from '@narval/authz-shared'
import { Action, Decision, getAccountId, getAssetId } from '@narval/authz-shared'
import { Intents, TransferNative } from '@narval/transaction-request-intent'
import { Test, TestingModule } from '@nestjs/testing'
import { mock } from 'jest-mock-extended'
import { MockProxy, mock } from 'jest-mock-extended'
import { times } from 'lodash/fp'

describe(AuthorizationRequestService.name, () => {
let module: TestingModule
let authzRequestRepositoryMock: AuthorizationRequestRepository
let authzRequestProcessingProducerMock: AuthorizationRequestProcessingProducer
let transferFeedServiceMock: TransferFeedService
let authzApplicationClientMock: AuthzApplicationClient
let authzRequestRepositoryMock: MockProxy<AuthorizationRequestRepository>
let authzRequestProcessingProducerMock: MockProxy<AuthorizationRequestProcessingProducer>
let transferFeedServiceMock: MockProxy<TransferFeedService>
let authzApplicationClientMock: MockProxy<AuthzApplicationClient>
let priceServiceMock: MockProxy<PriceService>
let service: AuthorizationRequestService

const authzRequest: AuthorizationRequest = generateAuthorizationRequest({
Expand All @@ -43,6 +46,7 @@ describe(AuthorizationRequestService.name, () => {
authzRequestProcessingProducerMock = mock<AuthorizationRequestProcessingProducer>()
transferFeedServiceMock = mock<TransferFeedService>()
authzApplicationClientMock = mock<AuthzApplicationClient>()
priceServiceMock = mock<PriceService>()

module = await Test.createTestingModule({
providers: [
Expand All @@ -62,6 +66,10 @@ describe(AuthorizationRequestService.name, () => {
{
provide: AuthzApplicationClient,
useValue: authzApplicationClientMock
},
{
provide: PriceService,
useValue: priceServiceMock
}
]
}).compile()
Expand All @@ -77,9 +85,14 @@ describe(AuthorizationRequestService.name, () => {
approvals: [approval]
}

it('creates a new approval and evaluates the authorization request', async () => {
jest.spyOn(authzRequestRepositoryMock, 'update').mockResolvedValue(updatedAuthzRequest)
beforeEach(() => {
// To isolate the approve scenario, prevents the evaluation procedure to
// run by mocking it.
jest.spyOn(service, 'evaluate').mockResolvedValue(updatedAuthzRequest)
})

it('creates a new approval and evaluates the authorization request', async () => {
authzRequestRepositoryMock.update.mockResolvedValue(updatedAuthzRequest)

await service.approve(authzRequest.id, approval)

Expand All @@ -92,28 +105,32 @@ describe(AuthorizationRequestService.name, () => {
})

describe('evaluate', () => {
const matic = getAssetId('eip155:137/slip44:966')

const evaluationResponse: EvaluationResponse = {
decision: Decision.PERMIT,
request: authzRequest.request,
attestation: generateSignature(),
// TODO (@wcalderipe, 25/01/24): Revisit the types of
// @narval/transaction-request-intent with @pierre and start using a
// shared library.
transactionRequestIntent: {
type: Intents.TRANSFER_NATIVE,
amount: '1000000000000000000',
to: 'eip155:137:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4' as AccountId,
from: 'eip155:137:0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b' as AccountId,
token: 'eip155:137/slip44/966' as AssetId
to: getAccountId('eip155:137/0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4'),
from: getAccountId('eip155:137/0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b'),
token: matic
}
}

const transfers: Transfer[] = times(() => generateTransferFeed({ orgId: authzRequest.orgId }), 2)

beforeEach(() => {
jest.spyOn(authzApplicationClientMock, 'evaluation').mockResolvedValue(evaluationResponse)
jest.spyOn(authzRequestRepositoryMock, 'update').mockResolvedValue(authzRequest)
jest.spyOn(transferFeedServiceMock, 'findByOrgId').mockResolvedValue(transfers)
authzApplicationClientMock.evaluation.mockResolvedValue(evaluationResponse)
authzRequestRepositoryMock.update.mockResolvedValue(authzRequest)
transferFeedServiceMock.findByOrgId.mockResolvedValue(transfers)
priceServiceMock.getPrices.mockResolvedValue({
[matic]: {
[FIAT_ID_USD]: 0.99
}
})
})

it('calls authz application client', async () => {
Expand Down Expand Up @@ -147,6 +164,15 @@ describe(AuthorizationRequestService.name, () => {
})
})

it('calls price service', async () => {
await service.evaluate(authzRequest)

expect(priceServiceMock.getPrices).toHaveBeenCalledWith({
from: [matic],
to: [FIAT_ID_USD]
})
})

it('tracks approved transfer when signing a transaction', async () => {
await service.evaluate(authzRequest)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { FIAT_ID_USD } from '@app/orchestration/orchestration.constant'
import {
Approval,
AuthorizationRequest,
Expand All @@ -7,9 +8,10 @@ import {
import { AuthzApplicationClient } from '@app/orchestration/policy-engine/http/client/authz-application.client'
import { AuthorizationRequestRepository } from '@app/orchestration/policy-engine/persistence/repository/authorization-request.repository'
import { AuthorizationRequestProcessingProducer } from '@app/orchestration/policy-engine/queue/producer/authorization-request-processing.producer'
import { PriceService } from '@app/orchestration/price/core/service/price.service'
import { Transfer } from '@app/orchestration/shared/core/type/transfer-feed.type'
import { TransferFeedService } from '@app/orchestration/transfer-feed/core/service/transfer-feed.service'
import { Action, HistoricalTransfer } from '@narval/authz-shared'
import { Action, Decision, HistoricalTransfer } from '@narval/authz-shared'
import { Intents } from '@narval/transaction-request-intent'
import { Injectable, Logger } from '@nestjs/common'
import { mapValues, omit } from 'lodash/fp'
Expand All @@ -18,9 +20,9 @@ import { v4 as uuid } from 'uuid'

const getStatus = (decision: string): AuthorizationRequestStatus => {
const statuses: Map<string, AuthorizationRequestStatus> = new Map([
['Permit', AuthorizationRequestStatus.PERMITTED],
['Forbid', AuthorizationRequestStatus.FORBIDDEN],
['Confirm', AuthorizationRequestStatus.APPROVING]
[Decision.PERMIT, AuthorizationRequestStatus.PERMITTED],
[Decision.FORBID, AuthorizationRequestStatus.FORBIDDEN],
[Decision.CONFIRM, AuthorizationRequestStatus.APPROVING]
])

const status = statuses.get(decision)
Expand All @@ -40,7 +42,8 @@ export class AuthorizationRequestService {
private authzRequestRepository: AuthorizationRequestRepository,
private authzRequestProcessingProducer: AuthorizationRequestProcessingProducer,
private authzApplicationClient: AuthzApplicationClient,
private transferFeedService: TransferFeedService
private transferFeedService: TransferFeedService,
private priceService: PriceService
) {}

async create(input: CreateAuthorizationRequest): Promise<AuthorizationRequest> {
Expand Down Expand Up @@ -136,6 +139,11 @@ export class AuthorizationRequestService {
if (authzRequest.request.action === Action.SIGN_TRANSACTION && status === AuthorizationRequestStatus.PERMITTED) {
const intent = evaluation.transactionRequestIntent
if (intent && intent.type === Intents.TRANSFER_NATIVE) {
const prices = await this.priceService.getPrices({
from: [intent.token],
to: [FIAT_ID_USD]
})

const transfer = {
orgId: authzRequest.orgId,
from: intent.from,
Expand All @@ -145,9 +153,7 @@ export class AuthorizationRequestService {
initiatedBy: authzRequest.authentication.pubKey,
createdAt: new Date(),
amount: BigInt(intent.amount),
rates: {
'fiat:usd': 0.99
}
rates: prices[intent.token]
}

await this.transferFeedService.track(transfer)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { AuthzApplicationClient } from '@app/orchestration/policy-engine/http/cl
import { AuthorizationRequestRepository } from '@app/orchestration/policy-engine/persistence/repository/authorization-request.repository'
import { AuthorizationRequestProcessingConsumer } from '@app/orchestration/policy-engine/queue/consumer/authorization-request-processing.consumer'
import { AuthorizationRequestProcessingProducer } from '@app/orchestration/policy-engine/queue/producer/authorization-request-processing.producer'
import { PriceService } from '@app/orchestration/price/core/service/price.service'
import { PersistenceModule } from '@app/orchestration/shared/module/persistence/persistence.module'
import { TestPrismaService } from '@app/orchestration/shared/module/persistence/service/test-prisma.service'
import { QueueModule } from '@app/orchestration/shared/module/queue/queue.module'
Expand Down Expand Up @@ -100,6 +101,10 @@ describe(AuthorizationRequestProcessingConsumer.name, () => {
{
provide: AuthzApplicationClient,
useValue: mock<AuthzApplicationClient>()
},
{
provide: PriceService,
useValue: mock<PriceService>()
}
]
}).compile()
Expand Down
23 changes: 17 additions & 6 deletions apps/orchestration/src/price/core/service/price.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { CoinGeckoClient } from '@app/orchestration/price/http/client/coin-gecko
import { SimplePrice } from '@app/orchestration/price/http/client/coin-gecko/coin-gecko.type'
import CoinGeckoAssetIdIndex from '@app/orchestration/price/resource/coin-gecko-asset-id-index.json'
import { CHAINS } from '@app/orchestration/shared/core/lib/chains.lib'
import { FiatId, Price } from '@app/orchestration/shared/core/type/price.type'
import { FiatId, Prices } from '@app/orchestration/shared/core/type/price.type'
import { AssetId, getAssetId, isCoin, parseAsset } from '@narval/authz-shared'
import { HttpStatus, Injectable } from '@nestjs/common'
import { HttpStatus, Injectable, Logger } from '@nestjs/common'
import { compact } from 'lodash/fp'

type GetPricesOption = {
Expand All @@ -15,9 +15,13 @@ type GetPricesOption = {

@Injectable()
export class PriceService {
private logger = new Logger(PriceService.name)

constructor(private coinGeckoClient: CoinGeckoClient) {}

async getPrices(options: GetPricesOption): Promise<Price> {
async getPrices(options: GetPricesOption): Promise<Prices> {
this.logger.log('Get prices', options)

const from = options.from.map(this.getCoinGeckoId)

if (from.some((id) => id === null)) {
Expand All @@ -28,18 +32,25 @@ export class PriceService {
})
}

const prices = await this.coinGeckoClient.getSimplePrice({
const simplePrice = await this.coinGeckoClient.getSimplePrice({
data: {
ids: compact(from),
vs_currencies: options.to.map(this.getCoinGeckoCurrencyId),
precision: 18
}
})

return this.buildPrice(prices)
const prices = this.buildPrices(simplePrice)

this.logger.log('Received prices', {
options,
prices
})

return prices
}

private buildPrice(prices: SimplePrice): Price {
private buildPrices(prices: SimplePrice): Prices {
return Object.keys(prices).reduce((acc, coinId) => {
const assetId = this.getAssetId(coinId)

Expand Down
5 changes: 3 additions & 2 deletions apps/orchestration/src/price/price.module.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { PriceService } from '@app/orchestration/price/core/service/price.service'
import { CoinGeckoClient } from '@app/orchestration/price/http/client/coin-gecko/coin-gecko.client'
import { HttpModule } from '@nestjs/axios'
import { Module } from '@nestjs/common'

@Module({
imports: [HttpModule],
providers: [CoinGeckoClient],
exports: [CoinGeckoClient]
providers: [CoinGeckoClient, PriceService],
exports: [PriceService]
})
export class PriceModule {}
1 change: 1 addition & 0 deletions packages/transaction-request-intent/src/lib/typeguards.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Address, AssetType, Hex } from '@narval/authz-shared'
// eslint-disable-next-line no-restricted-imports
import { isAddress } from 'viem'
import { AssetTypeAndUnknown, Misc } from './domain'
import { SupportedMethodId } from './supported-methods'
Expand Down
1 change: 1 addition & 0 deletions packages/transaction-request-intent/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
toAssetId
} from '@narval/authz-shared'
import { SetOptional } from 'type-fest'
// eslint-disable-next-line no-restricted-imports
import { Address, isAddress } from 'viem'
import {
AssetTypeAndUnknown,
Expand Down

0 comments on commit 24e80ba

Please sign in to comment.