Skip to content

Commit

Permalink
feat(cloud-function): getTokensInfoCF (#246)
Browse files Browse the repository at this point in the history
  • Loading branch information
cajubelt authored Sep 8, 2023
1 parent dcbce6a commit 35c5721
Show file tree
Hide file tree
Showing 17 changed files with 1,142 additions and 27 deletions.
18 changes: 18 additions & 0 deletions .gcloudignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# This file specifies files that are *not* uploaded to Google Cloud
# using gcloud. It follows the same syntax as .gitignore, with the addition of
# "#!include" directives (which insert the entries of the given .gitignore-style
# file at that point).
#
# For more information, run:
# $ gcloud topic gcloudignore
#
.gcloudignore
# If you would like to upload your .git directory, .gitignore file or files
# from your .gitignore file, remove the corresponding line
# below:
.git
.gitignore

node_modules
#!include:.gitignore
!dist
59 changes: 59 additions & 0 deletions .github/workflows/workflow.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,62 @@ jobs:
check-latest: true
- run: yarn
- run: yarn update:rtdb --project=mainnet --database-url=https://celo-mobile-mainnet.firebaseio.com/
deploy-dev:
name: Deploy dev cloud function
if: github.ref == 'refs/heads/main'
needs:
- lint
- test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: google-github-actions/auth@v1
with:
project_id: celo-mobile-alfajores
credentials_json: ${{ secrets.ALFAJORES_SERVICE_ACCOUNT_KEY }}
- uses: google-github-actions/setup-gcloud@v1
with:
install_components: 'beta'
- uses: actions/setup-node@v3
with:
node-version: '20'
check-latest: true
- uses: actions/cache@v3
with:
path: |
node_modules
*/*/node_modules
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- run: yarn
- run: yarn build
- run: yarn deploy:dev getTokensInfoCF
deploy-prod:
name: Deploy prod cloud function
if: github.ref == 'refs/heads/main'
needs:
- lint
- test
- deploy-dev
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: google-github-actions/auth@v1
with:
project_id: celo-mobile-mainnet
credentials_json: ${{ secrets.MAINNET_SERVICE_ACCOUNT_KEY }}
- uses: google-github-actions/setup-gcloud@v1
with:
install_components: 'beta'
- uses: actions/setup-node@v3
with:
node-version: '20'
check-latest: true
- uses: actions/cache@v3
with:
path: |
node_modules
*/*/node_modules
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- run: yarn
- run: yarn build
- run: yarn deploy:prod getTokensInfoCF
2 changes: 2 additions & 0 deletions config-dev.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ENVIRONMENT: 'testnet'
GCLOUD_PROJECT: 'celo-mobile-alfajores'
2 changes: 2 additions & 0 deletions config-prod.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ENVIRONMENT: 'mainnet'
GCLOUD_PROJECT: 'celo-mobile-mainnet'
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version": "0.0.1",
"description": "Package for storing and updating static RTDB data",
"private": true,
"main": "dist/index.js",
"engines": {
"node": "^20"
},
Expand All @@ -20,7 +21,9 @@
"test:ci": "yarn test --ci --coverage",
"supercheck": "yarn format && yarn lint:fix && yarn typecheck && yarn test",
"diff": "ts-node scripts/diff.ts",
"update:rtdb": "ts-node scripts/update-rtdb.ts"
"update:rtdb": "ts-node scripts/update-rtdb.ts",
"deploy:dev": "gcloud functions deploy --project=celo-mobile-alfajores --runtime=nodejs20 --trigger-http --allow-unauthenticated --security-level=secure-always --ingress-settings=internal-and-gclb --env-vars-file=config-dev.yaml",
"deploy:prod": "gcloud functions deploy --project=celo-mobile-mainnet --runtime=nodejs20 --trigger-http --allow-unauthenticated --security-level=secure-always --ingress-settings=internal-and-gclb --env-vars-file=config-prod.yaml"
},
"prettier": "@valora/prettier-config",
"repository": "git@github.com:valora-inc/rtdb-data.git",
Expand All @@ -31,6 +34,8 @@
"license": "MIT",
"dependencies": {
"@types/node": "^17.0.45",
"@valora/http-handler": "^0.0.1",
"@valora/logging": "^1.3.5",
"dotenv": "^16.3.1",
"firebase-admin": "^11.10.1",
"joi": "^17.10.1",
Expand All @@ -41,6 +46,7 @@
},
"devDependencies": {
"@google-cloud/bigquery": "^7.2.0",
"@google-cloud/functions-framework": "^3.3.0",
"@types/jest": "^29.5.4",
"@types/json-diff": "^1.0.0",
"@typescript-eslint/eslint-plugin": "^6.5.0",
Expand Down
4 changes: 2 additions & 2 deletions scripts/diff.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
/* eslint-disable no-console */
import { diffString } from 'json-diff'
import { loadConfig } from '../src/config'
import { loadUpdateRTDBConfig } from '../src/config'
import { FirebaseClient } from '../src/clients/firebase-client'
import { getCeloRTDBMetadata } from '../src'

async function main() {
const config = loadConfig()
const config = loadUpdateRTDBConfig()
const firebaseClient = new FirebaseClient(config)

const projectMetadata = getCeloRTDBMetadata(config.project)
Expand Down
4 changes: 2 additions & 2 deletions scripts/update-rtdb.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/* eslint-disable no-console */
import { diffString } from 'json-diff'
import { loadConfig } from '../src/config'
import { loadUpdateRTDBConfig } from '../src/config'
import { FirebaseClient } from '../src/clients/firebase-client'
import { deleteMissingKeysUpdateRequest } from '../src/utils/utils'
import { getCeloRTDBMetadata } from '../src'

async function main() {
const config = loadConfig()
const config = loadUpdateRTDBConfig()
const firebaseClient = new FirebaseClient(config)

const projectMetadata = getCeloRTDBMetadata(config.project)
Expand Down
6 changes: 3 additions & 3 deletions src/clients/firebase-client.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import * as admin from 'firebase-admin'
import { mapNestedJsonIntoPlain } from '../utils/utils'
import { Config } from '../types'
import { UpdateRTDBConfig } from '../types'

export class FirebaseClient {
config: Config
config: UpdateRTDBConfig
firebaseApp: admin.app.App
firebaseDb: admin.database.Database

constructor(config: Config) {
constructor(config: UpdateRTDBConfig) {
this.config = config
this.firebaseApp = admin.initializeApp({
credential: admin.credential.applicationDefault(),
Expand Down
20 changes: 18 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import yargs from 'yargs'
import * as dotenv from 'dotenv'
import { Config } from './types'
import { TokensInfoCFConfig, UpdateRTDBConfig } from './types'

export function loadConfig(): Config {
export function loadUpdateRTDBConfig(): UpdateRTDBConfig {
dotenv.config()

const argv = yargs
Expand All @@ -24,3 +24,19 @@ export function loadConfig(): Config {
databaseUrl: argv['database-url'],
}
}

export function loadCloudFunctionConfig(): TokensInfoCFConfig {
return yargs
.env('')
.option('environment', {
description: 'Blockchain environment to use',
choices: ['mainnet', 'testnet'] as const,
demandOption: true,
})
.option('gcloud-project', {
description: 'Valora Google Cloud project to deploy to',
choices: ['celo-mobile-mainnet', 'celo-mobile-alfajores'] as const,
demandOption: true,
})
.parseSync()
}
22 changes: 22 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { _getTokensInfoHttpFunction } from './index'
import mocked = jest.mocked
import { loadCloudFunctionConfig } from './config'

jest.mock('./config')

describe('index', () => {
it('_getTokensInfoHttpFunction', async () => {
const req = {} as any
const res = { status: jest.fn().mockReturnThis(), send: jest.fn() } as any
mocked(loadCloudFunctionConfig).mockReturnValue({
environment: 'mainnet',
gcloudProject: 'celo-mobile-mainnet',
})
await _getTokensInfoHttpFunction(req, res)
expect(res.status).toHaveBeenCalledWith(200)
expect(res.send).toHaveBeenCalledWith({
celo: expect.any(Array),
ethereum: expect.any(Array),
})
})
})
19 changes: 17 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import TestnetAddressesExtraInfo from './data/testnet/addresses-extra-info.json'
import AddressesExtraInfoSchema from './schemas/addresses-extra-info'
import { addTokenIds, transformCeloTokensForRTDB } from './utils/transforms'
import { RTDBAddressToTokenInfoSchema } from './schemas/tokens-info'
import { HttpFunction } from '@google-cloud/functions-framework'
import { wrap } from './wrap'
import { loadCloudFunctionConfig } from './config'

export function getCeloRTDBMetadata(environment: Environment): RTDBMetadata[] {
const [tokensInfo, addressesExtraInfo] =
Expand All @@ -37,8 +40,7 @@ export function getCeloRTDBMetadata(environment: Environment): RTDBMetadata[] {
]
}

// TODO(ACT-908): serve this data with a cloud function https://linear.app/valora/issue/ACT-908/createupdate-cloud-function-to-return-new-tokens-info
export function getTokensInfo(
export function _getTokensInfo(
environment: Environment,
): Record<Network, TokenInfo[]> {
return environment === 'mainnet'
Expand All @@ -63,3 +65,16 @@ export function getTokensInfo(
),
}
}

export const _getTokensInfoHttpFunction: HttpFunction = async (_req, res) => {
const { environment } = loadCloudFunctionConfig()
const tokensInfo = _getTokensInfo(environment)
res.status(200).send({ ...tokensInfo })
}

// named this way to avoid collision with cloud function getTokensInfo from valora-rest-api.
// TODO deprecate getTokensInfo cloud function from valora-rest-api
export const getTokensInfoCF: HttpFunction = wrap({
loadConfig: loadCloudFunctionConfig,
httpFunction: _getTokensInfoHttpFunction,
})
21 changes: 21 additions & 0 deletions src/log.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { createLogger, createLoggingMiddleware } from '@valora/logging'
import { ValoraGcloudProject } from './types'

export const logger = createLogger({
redact: {
paths: ['req.headers.authorization', 'req.headers.cookie'],
censor: '***REDACTED***',
},
})

let loggingMiddleware: ReturnType<typeof createLoggingMiddleware>

export const getLoggingMiddleware = (gcloudProject: ValoraGcloudProject) => {
if (!loggingMiddleware) {
loggingMiddleware = createLoggingMiddleware({
logger,
projectId: gcloudProject,
})
}
return loggingMiddleware
}
6 changes: 3 additions & 3 deletions src/schemas.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getCeloRTDBMetadata, getTokensInfo } from './index'
import { getCeloRTDBMetadata, _getTokensInfo } from './index'
import { Environment, Network, TokenInfo } from './types'
import {
RTDBAddressToTokenInfoSchema,
Expand Down Expand Up @@ -112,7 +112,7 @@ describe('Schema validation', () => {

describe('Tokens info data', () => {
const tokensInfo: TokenInfo[] = (['mainnet', 'testnet'] as const)
.map(getTokensInfo)
.map(_getTokensInfo)
.flatMap(Object.values)
.flat()
it.each(tokensInfo)('tokenInfo %o', (tokenInfo) => {
Expand All @@ -131,7 +131,7 @@ describe('Schema validation', () => {
for (const environment of ['mainnet', 'testnet'] as const) {
for (const network of [Network.celo, Network.ethereum] as const) {
for (const tokenInfo of Object.values(
getTokensInfo(environment)[network],
_getTokensInfo(environment)[network],
)) {
if (tokenInfo.address) {
addresses[environment][network].push(tokenInfo.address)
Expand Down
11 changes: 10 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,16 @@ export interface RTDBMetadata {
overrideType: OverrideType
}

export interface Config {
export interface UpdateRTDBConfig {
project: Environment
databaseUrl: string
}

export interface TokensInfoCFConfig {
gcloudProject: ValoraGcloudProject
environment: Environment
}

export interface TokenInfo {
name: string
symbol: string
Expand All @@ -50,3 +55,7 @@ export enum NetworkId { // environment-specific
}

export type Environment = 'mainnet' | 'testnet'

export type ValoraGcloudProject =
| 'celo-mobile-alfajores'
| 'celo-mobile-mainnet'
40 changes: 40 additions & 0 deletions src/wrap.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import mocked = jest.mocked
import { getLoggingMiddleware, logger } from './log'
import { wrap } from './wrap'
import { Request, Response } from '@google-cloud/functions-framework'
import { asyncHandler } from '@valora/http-handler'

jest.mock('./log')
jest.mock('@valora/http-handler')

describe('wrap', () => {
it('wraps a function with logging middleware and async handler', () => {
const mockLoggingMiddleware = jest
.fn()
.mockImplementation((req, res, next) => next(req, res)) // this mock just lets us check that 'next' is set to the httpFunction parameter given to 'wrap'
mocked(getLoggingMiddleware).mockReturnValue(mockLoggingMiddleware)
const mockHttpFunction = jest.fn()
const mockAsyncHttpFunction = jest.fn()
mocked(asyncHandler).mockReturnValue(mockAsyncHttpFunction)
const mockLoadConfig = jest
.fn()
.mockReturnValue({ gcloudProject: 'test-gcloud-project' })
const wrapped = wrap({
httpFunction: mockHttpFunction,
loadConfig: mockLoadConfig,
})
expect(mockLoadConfig).not.toHaveBeenCalled() // don't call loadConfig until the wrapped function is called
expect(asyncHandler).toHaveBeenCalledWith(mockHttpFunction, logger)
const mockReq = {} as Request
const mockRes = {} as Response
wrapped(mockReq, mockRes)
expect(mockLoadConfig).toHaveBeenCalled()
expect(getLoggingMiddleware).toHaveBeenCalledWith('test-gcloud-project')
expect(mockLoggingMiddleware).toHaveBeenCalledWith(
mockReq,
mockRes,
expect.any(Function),
)
expect(mockAsyncHttpFunction).toHaveBeenCalledWith(mockReq, mockRes)
})
})
22 changes: 22 additions & 0 deletions src/wrap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {
HttpFunction,
Request,
Response,
} from '@google-cloud/functions-framework'
import { getLoggingMiddleware, logger } from './log'
import { ValoraGcloudProject } from './types'
import { asyncHandler } from '@valora/http-handler'

export function wrap({
httpFunction,
loadConfig,
}: {
httpFunction: HttpFunction
loadConfig: () => { gcloudProject: ValoraGcloudProject }
}): HttpFunction {
const asyncHttpFunction = asyncHandler(httpFunction, logger)
return (req: Request, res: Response) => {
const loggingMiddleware = getLoggingMiddleware(loadConfig().gcloudProject)
return loggingMiddleware(req, res, () => asyncHttpFunction(req, res))
}
}
Loading

0 comments on commit 35c5721

Please sign in to comment.