From c83d5bb6d8cc6c553eae876eed76f7a8a6322f87 Mon Sep 17 00:00:00 2001 From: Spencer T Brody Date: Tue, 25 Jun 2024 14:25:59 -0400 Subject: [PATCH] feat: include js-ceramic and ceramic-one version in request creation metric (#1232) --- src/__tests__/ceramic_integration.test.ts | 684 ------------------ src/ancillary/anchor-request-params-parser.ts | 18 +- .../__tests__/request-controller.test.ts | 16 +- src/controllers/request-controller.ts | 8 +- src/settings.ts | 2 +- 5 files changed, 22 insertions(+), 706 deletions(-) delete mode 100644 src/__tests__/ceramic_integration.test.ts diff --git a/src/__tests__/ceramic_integration.test.ts b/src/__tests__/ceramic_integration.test.ts deleted file mode 100644 index d114c3de..00000000 --- a/src/__tests__/ceramic_integration.test.ts +++ /dev/null @@ -1,684 +0,0 @@ -import 'reflect-metadata' -import 'dotenv/config' -import { - jest, - beforeAll, - beforeEach, - describe, - afterEach, - afterAll, - expect, - test, -} from '@jest/globals' - -import { Ceramic } from '@ceramicnetwork/core' -import { AnchorStatus, fetchJson, IpfsApi, Stream } from '@ceramicnetwork/common' -import { ServiceMetrics as Metrics } from '@ceramicnetwork/observability' - -import * as Ctl from 'ipfsd-ctl' -import * as ipfsClient from 'ipfs-http-client' -import { path } from 'go-ipfs' - -import express from 'express' -import { makeGanache } from './make-ganache.util.js' -import type { GanacheServer } from './make-ganache.util.js' -import tmp from 'tmp-promise' -import getPort from 'get-port' -import type { Knex } from 'knex' -import { clearTables, createDbConnection, createReplicaDbConnection } from '../db-connection.js' -import { CeramicAnchorApp } from '../app.js' -import { config } from 'node-config-ts' -import cloneDeep from 'lodash.clonedeep' -import { TileDocument } from '@ceramicnetwork/stream-tile' -import { filter } from 'rxjs/operators' -import { firstValueFrom, timeout, throwError } from 'rxjs' -import { DID } from 'dids' -import { Ed25519Provider } from 'key-did-provider-ed25519' -import { randomBytes } from '@stablelib/random' -import * as KeyDidResolver from 'key-did-resolver' -import { Utils } from '../utils.js' -import { RequestStatus } from '../models/request.js' -import { AnchorService } from '../services/anchor-service.js' -import { METRIC_NAMES } from '../settings.js' -import { Server } from 'http' -import type { Injector } from 'typed-inject' -import { createInjector } from 'typed-inject' -import { teeDbConnection } from './tee-db-connection.util.js' -import { CARFactory, type CAR } from 'cartonne' -import { InMemoryWitnessService } from '../services/witness-service.js' - -process.env.NODE_ENV = 'test' - -const randomNumber = Math.floor(Math.random() * 10000) -const TOPIC = `/ceramic/local/${randomNumber}` - -const ipfsHttpModule = { - create: (ipfsEndpoint: string) => { - return ipfsClient.create({ - url: ipfsEndpoint, - }) - }, -} - -const createFactory = () => { - return Ctl.createFactory( - { - ipfsHttpModule, - ipfsOptions: { - repoAutoMigrate: true, - }, - }, - { - go: { - ipfsBin: path(), - }, - } - ) -} - -/** - * Create an IPFS instance - */ -async function createIPFS(apiPort?: number): Promise { - const tmpFolder = await tmp.dir({ unsafeCleanup: true }) - const swarmPort = await getPort() - const effectiveApiPort = apiPort || (await getPort()) - const gatewayPort = await getPort() - - const config = { - repo: `${tmpFolder.path}/ipfs${swarmPort}/`, - config: { - Addresses: { - Swarm: [`/ip4/127.0.0.1/tcp/${swarmPort}`], - Gateway: `/ip4/127.0.0.1/tcp/${gatewayPort}`, - API: `/ip4/127.0.0.1/tcp/${effectiveApiPort}`, - }, - Pubsub: { - Enabled: true, - SeenMessagesTTL: '10m', - }, - Discovery: { DNS: { Enabled: false }, webRTCStar: { Enabled: false } }, - Bootstrap: [], - }, - } - - const ipfsd = await createFactory().spawn({ - type: 'go', - ipfsOptions: config, - disposable: true, - }) - - console.log(`starting IPFS node with config: ${JSON.stringify(config, null, 2)}`) - const started = await ipfsd.start() - return started.api -} - -async function swarmConnect(a: IpfsApi, b: IpfsApi) { - const addressB = (await b.id()).addresses[0] - await a.swarm.connect(addressB) -} - -async function makeCeramicCore( - ipfs: IpfsApi, - anchorServiceUrl: string, - ethereumRpcUrl: URL | undefined -): Promise { - const tmpFolder = await tmp.dir({ unsafeCleanup: true }) - const ceramic = await Ceramic.create(ipfs, { - networkName: 'local', - pubsubTopic: TOPIC, - stateStoreDirectory: tmpFolder.path, - anchorServiceUrl, - // TODO CDB-2317 Remove `indexing` config when Ceramic Core allows that - indexing: { - db: 'TODO', - allowQueriesBeforeHistoricalSync: false, - disableComposedb: true, - enableHistoricalSync: false, - }, - ethereumRpcUrl: ethereumRpcUrl?.href, - }) - ceramic.did = makeDID() - await ceramic.did.authenticate() - return ceramic -} - -function makeDID(): DID { - const seed = randomBytes(32) - const provider = new Ed25519Provider(seed) - const resolver = KeyDidResolver.getResolver() - return new DID({ provider, resolver }) -} - -class FauxAnchorLauncher { - port: number - server: Server - start(port: number) { - const app = express() - app.all('/', (req, res) => { - res.send({ status: 'success' }) - }) - this.server = app.listen(port, () => { - console.log(`Listening on port ${port}`) - }) - } - stop() { - return new Promise((resolve) => this.server.close(resolve)) - } -} - -function makeAnchorLauncher(port: number): FauxAnchorLauncher { - const launcher = new FauxAnchorLauncher() - launcher.start(port) - return launcher -} - -interface MinimalCASConfig { - ipfsPort: number - ganachePort: number - mode: string - port: number - useSmartContractAnchors: boolean -} - -async function makeCAS( - container: Injector, - dbConnection: Knex, - minConfig: MinimalCASConfig, - replicaDbConnection: { connection: Knex; type: string } -): Promise { - const configCopy = cloneDeep(config) - configCopy.mode = minConfig.mode - configCopy.port = minConfig.port - configCopy.anchorControllerEnabled = true - configCopy.merkleDepthLimit = 0 - configCopy.minStreamCount = 1 - configCopy.ipfsConfig.url = `http://localhost:${minConfig.ipfsPort}` - configCopy.ipfsConfig.pubsubTopic = TOPIC - configCopy.blockchain.connectors.ethereum.network = 'ganache' - configCopy.blockchain.connectors.ethereum.rpc.port = String(minConfig.ganachePort) - configCopy.useSmartContractAnchors = minConfig.useSmartContractAnchors - configCopy.carStorage = { - mode: 'inmemory', - } - configCopy.witnessStorage = { - mode: 'inmemory', - } - return new CeramicAnchorApp( - container - .provideValue('config', configCopy) - .provideValue('dbConnection', dbConnection) - .provideValue('replicaDbConnection', replicaDbConnection) - ) -} - -async function anchorUpdate( - stream: Stream, - anchorApp: CeramicAnchorApp, - anchorService: AnchorService -): Promise { - // The anchor request is not guaranteed to already have been sent to the CAS when the create/update - // promise resolves, so we wait a bit to give the ceramic node time to actually send the request - // before triggering the anchor. - // TODO(js-ceramic #1919): Remove this once Ceramic won't return from a request that makes an - // anchor without having already made the anchor request against the CAS. - await Utils.delay(5000) - await anchorService.emitAnchorEventIfReady() - await anchorApp.anchor() - await waitForAnchor(stream) -} - -async function waitForAnchor(stream: Stream, timeoutMS = 30 * 1000): Promise { - await firstValueFrom( - stream.pipe( - filter((state) => [AnchorStatus.ANCHORED, AnchorStatus.FAILED].includes(state.anchorStatus)), - timeout({ - each: timeoutMS, - with: () => - throwError( - () => new Error(`Timeout waiting for stream ${stream.id.toString()} to become anchored`) - ), - }) - ) - ) -} - -describe('Ceramic Integration Test', () => { - jest.setTimeout(60 * 1000 * 10) - - let ipfsApiPort1: number - let ipfsApiPort2: number - - let ipfs1: IpfsApi // Used by CAS1 directly - let ipfs2: IpfsApi // Used by CAS2 directly - let ipfs3: IpfsApi // Used by main ceramic 1 - let ipfs4: IpfsApi // Used by main ceramic 2 - - let ceramic1: Ceramic // First main Ceramic node used by the tests - let ceramic2: Ceramic // Second main Ceramic node used by the tests - - let dbConnection1: Knex - let dbConnection2: Knex - - let replicaDbConnection1: { connection: Knex; type: string } - let replicaDbConnection2: { connection: Knex; type: string } - - let casPort1: number - let cas1: CeramicAnchorApp - let anchorService1: AnchorService - let cas2: CeramicAnchorApp - let anchorService2: AnchorService - - let ganacheServer: GanacheServer - let anchorLauncher: FauxAnchorLauncher - - beforeAll(async () => { - ipfsApiPort1 = await getPort() - ipfsApiPort2 = await getPort() - if (process.env['CAS_USE_IPFS_STORAGE']) { - ;[ipfs1, ipfs2, ipfs3, ipfs4] = await Promise.all([ - createIPFS(ipfsApiPort1), - createIPFS(ipfsApiPort2), - createIPFS(), - createIPFS(), - ]) - } else { - ;[ipfs3, ipfs4] = await Promise.all([ - createIPFS(ipfsApiPort1), - createIPFS(ipfsApiPort2), - createIPFS(), - createIPFS(), - ]) - } - - // Now make sure all ipfs nodes are connected to all other ipfs nodes - const ipfsNodes = process.env['CAS_USE_IPFS_STORAGE'] - ? [ipfs1, ipfs2, ipfs3, ipfs4] - : [ipfs3, ipfs4] - for (const [i] of ipfsNodes.entries()) { - for (const [j] of ipfsNodes.entries()) { - if (i == j) { - continue - } - await swarmConnect(ipfsNodes[i], ipfsNodes[j]) - } - } - - // Start up Ganache - ganacheServer = await makeGanache() - - // Start faux anchor launcher - anchorLauncher = makeAnchorLauncher(8001) - }) - - beforeEach(async () => { - await clearTables(dbConnection1) - await clearTables(dbConnection2) - }) - - afterAll(async () => { - if (process.env['CAS_USE_IPFS_STORAGE']) { - await Promise.all([ipfs1.stop(), ipfs2.stop(), ipfs3.stop(), ipfs4.stop()]) - } else { - await Promise.all([ipfs3.stop(), ipfs4.stop()]) - } - await ganacheServer.close() - await anchorLauncher.stop() - }) - - // TODO_WS2-3238_1 : update tests to test with replica db connection as well - // TODO_WS2-3238_2 : make hermetic env have replica db connection - describe('Using anchor version 1', () => { - beforeAll(async () => { - const useSmartContractAnchors = true - - // Start anchor services - dbConnection1 = await createDbConnection() - replicaDbConnection1 = await createReplicaDbConnection() - casPort1 = await getPort() - - cas1 = await makeCAS( - createInjector(), - dbConnection1, - { - mode: 'server', - ipfsPort: ipfsApiPort1, - ganachePort: ganacheServer.port, - port: casPort1, - useSmartContractAnchors, - }, - replicaDbConnection1 - ) - await cas1.start() - anchorService1 = cas1.container.resolve('anchorService') - dbConnection2 = await teeDbConnection(dbConnection1) - replicaDbConnection2 = await createReplicaDbConnection() - const casPort2 = await getPort() - cas2 = await makeCAS( - createInjector(), - dbConnection2, - { - mode: 'server', - ipfsPort: ipfsApiPort2, - ganachePort: ganacheServer.port, - port: casPort2, - useSmartContractAnchors, - }, - replicaDbConnection2 - ) - await cas2.start() - anchorService2 = cas2.container.resolve('anchorService') - - // Finally make the Ceramic nodes that will be used in the tests. - ceramic1 = await makeCeramicCore(ipfs3, `http://localhost:${casPort1}`, ganacheServer.url) - ceramic2 = await makeCeramicCore(ipfs4, `http://localhost:${casPort2}`, ganacheServer.url) - - // Speed up polling interval to speed up test - ceramic1.context.anchorService.pollInterval = 100 - ceramic2.context.anchorService.pollInterval = 100 - - // The two user-facing ceramic nodes need to have the same DID Provider so that they can modify - // each others streams. - const did = makeDID() - await did.authenticate() - ceramic1.did = did - ceramic2.did = did - }) - - afterAll(async () => { - await cas1.stop() - await cas2.stop() - await Promise.all([dbConnection1.destroy(), dbConnection2.destroy()]) - await Promise.all([ - replicaDbConnection1.connection.destroy(), - replicaDbConnection2.connection.destroy(), - ]) - await Promise.all([ceramic1.close(), ceramic2.close()]) - }) - - beforeEach(async () => { - console.log(`Starting test: ${expect.getState().currentTestName}`) - }) - - afterEach(async () => { - console.log(`Finished test: ${expect.getState().currentTestName}`) - jest.restoreAllMocks() - }) - - describe('Multiple CAS instances in same process works', () => { - // This test uses a very old approach for anchoring. Moreover, we will never be deploying multiple CAS instances. - test.skip( - 'Anchors on different CAS instances are independent', - async () => { - const doc1 = await TileDocument.create(ceramic1, { foo: 1 }, null, { anchor: true }) - const doc2 = await TileDocument.create(ceramic2, { foo: 1 }, null, { anchor: true }) - - expect(doc1.state.anchorStatus).toEqual(AnchorStatus.PENDING) - expect(doc2.state.anchorStatus).toEqual(AnchorStatus.PENDING) - - // Test that anchoring on CAS1 doesn't anchor requests made against CAS2 - await anchorUpdate(doc1, cas1, anchorService1) - expect(doc1.state.anchorStatus).toEqual(AnchorStatus.ANCHORED) - expect(doc2.state.anchorStatus).toEqual(AnchorStatus.PENDING) - - // Now test that anchoring on CAS2 doesn't anchor requests made against CAS1 - await doc1.update({ foo: 2 }, null, { anchor: true }) - await anchorUpdate(doc2, cas2, anchorService2) - expect(doc1.state.anchorStatus).toEqual(AnchorStatus.PENDING) - expect(doc2.state.anchorStatus).toEqual(AnchorStatus.ANCHORED) - - console.log('Test complete: Anchors on different CAS instances are independent') - }, - 60 * 1000 * 3 - ) - - test( - 'Multiple anchors for same stream', - async () => { - const doc1 = await TileDocument.create(ceramic1, { foo: 1 }, null, { anchor: true }) - expect(doc1.state.anchorStatus).toEqual(AnchorStatus.PENDING) - await anchorUpdate(doc1, cas1, anchorService1) - expect(doc1.state.anchorStatus).toEqual(AnchorStatus.ANCHORED) - - // Now that genesis commit has been anchored do an update and make sure anchoring works again - await doc1.update({ foo: 2 }, null, { anchor: true }) - await anchorUpdate(doc1, cas1, anchorService1) - expect(doc1.state.anchorStatus).toEqual(AnchorStatus.ANCHORED) - expect(doc1.content).toEqual({ foo: 2 }) - - console.log('Test complete: Multiple anchors for same stream') - }, - 60 * 1000 * 3 - ) - - test( - 'Multiple anchors in a batch', - async () => { - const doc1 = await TileDocument.create(ceramic1, { foo: 1 }, null, { anchor: true }) - const doc2 = await TileDocument.create(ceramic1, { foo: 2 }, null, { anchor: true }) - - expect(doc1.state.anchorStatus).toEqual(AnchorStatus.PENDING) - expect(doc2.state.anchorStatus).toEqual(AnchorStatus.PENDING) - - await anchorUpdate(doc1, cas1, anchorService1) - expect(doc1.state.anchorStatus).toEqual(AnchorStatus.ANCHORED) - await waitForAnchor(doc2) - expect(doc2.state.anchorStatus).toEqual(AnchorStatus.ANCHORED) - - console.log('Test complete: Multiple anchors in a batch') - }, - 60 * 1000 * 3 - ) - }) - - test('Metrics produced on anchors', async () => { - jest.setTimeout(60 * 100 * 2) - - const metricsCountSpy = jest.spyOn(Metrics, 'count') - - const initialContent = { foo: 0 } - const doc1 = await TileDocument.create(ceramic1, initialContent, null, { anchor: true }) - expect(doc1.state.anchorStatus).toEqual(AnchorStatus.PENDING) - - await anchorUpdate(doc1, cas1, anchorService1) - - expect(metricsCountSpy).toHaveBeenCalledWith(METRIC_NAMES.ANCHOR_SUCCESS, 1) - - console.log('Test complete: Metrics counts anchor attempts') - }) - - test.skip('Can retrieve completed request when the request CID was not the stream tip when anchored', async () => { - const doc1 = await TileDocument.create(ceramic1, { foo: 1 }, null, { anchor: true }) - const originalTip = doc1.tip - await doc1.update({ foo: 2 }, null, { anchor: true }) - const nextTip = doc1.tip - - expect(doc1.state.anchorStatus).toEqual(AnchorStatus.PENDING) - - await anchorUpdate(doc1, cas1, anchorService1) - - expect(doc1.state.anchorStatus).toEqual(AnchorStatus.ANCHORED) - - const nextTipRequest = await fetchJson( - `http://localhost:${casPort1}/api/v0/requests/${nextTip.toString()}` - ) - expect(RequestStatus[nextTipRequest.status]).toEqual(RequestStatus.COMPLETED) - - const originalTipRequest = await fetchJson( - `http://localhost:${casPort1}/api/v0/requests/${originalTip.toString()}` - ) - expect(RequestStatus[originalTipRequest.status]).toEqual(RequestStatus.COMPLETED) - }) - - test('Can retreive completed request that was marked COMPLETE because its stream was already anchored', async () => { - const doc1 = await TileDocument.create(ceramic1, { foo: 1 }, null, { anchor: false }) - const tipWithNoRequest = doc1.tip - await doc1.update({ foo: 2 }, null, { anchor: true }) - const tipWithRequest = doc1.tip - - expect(doc1.state.anchorStatus).toEqual(AnchorStatus.PENDING) - await anchorUpdate(doc1, cas1, anchorService1) - expect(doc1.state.anchorStatus).toEqual(AnchorStatus.ANCHORED) - - const tipWithRequestInfo = await fetchJson( - `http://localhost:${casPort1}/api/v0/requests/${tipWithRequest.toString()}` - ) - expect(RequestStatus[tipWithRequestInfo.status]).toEqual(RequestStatus.COMPLETED) - - await fetchJson(`http://localhost:${casPort1}/api/v0/requests`, { - method: 'POST', - body: { - streamId: doc1.id.toString(), - docId: doc1.id.toString(), - cid: tipWithNoRequest.toString(), - }, - }) - - const tipWithNoRequestBeforeAnchorInfo = await fetchJson( - `http://localhost:${casPort1}/api/v0/requests/${tipWithNoRequest.toString()}` - ) - expect(RequestStatus[tipWithNoRequestBeforeAnchorInfo.status]).toEqual(RequestStatus.PENDING) - - await anchorService1.emitAnchorEventIfReady() - await cas1.anchor() - - const tipWithNoRequestAfterAnchorInfo = await fetchJson( - `http://localhost:${casPort1}/api/v0/requests/${tipWithNoRequest.toString()}` - ) - expect(RequestStatus[tipWithNoRequestAfterAnchorInfo.status]).toEqual(RequestStatus.COMPLETED) - }) - }) -}) - -describe('CAR file', () => { - test('do not depend on ipfs', async () => { - // Preparation: start a Ceramic node and an instance of CAS - const ipfsApiPort = await getPort() - const casIPFS = await createIPFS(ipfsApiPort) - const ganacheServer = await makeGanache() - const dbConnection = await createDbConnection() - const dummyReplicaDbConnection = await createReplicaDbConnection() - const casPort = await getPort() - const cas = await makeCAS( - createInjector(), - dbConnection, - { - mode: 'server', - ipfsPort: ipfsApiPort, - ganachePort: ganacheServer.port, - port: casPort, - useSmartContractAnchors: true, - }, - dummyReplicaDbConnection - ) - await cas.start() - - const ceramicIPFS = await createIPFS(await getPort()) - const ceramic = await makeCeramicCore( - ceramicIPFS, - `http://localhost:${casPort}`, - ganacheServer.url - ) - - // Poll more often to speed up the test - const anchorService = ceramic.context.anchorService as any - anchorService.pollInterval = 100 - - // CAS: Do not publish to IPFS - const carFactory = new CARFactory() - const carFile = carFactory.build() - jest - .spyOn(cas.container.resolve('ipfsService'), 'storeRecord') - .mockImplementation(async (record) => { - return carFile.put(record) - }) - // CAS: Do not publish over pubsub - // Now the only way a Ceramic node can get an anchor commit is a witness CAR through polling - jest - .spyOn(cas.container.resolve('ipfsService'), 'publishAnchorCommit') - .mockImplementation(async () => { - // Do Nothing - }) - - // Intercept witness CAR built on CAS side - let witnessCAR: CAR - const witnessService = new InMemoryWitnessService() - const spyBuildWitnessCAR = jest - .spyOn(cas.container.resolve('witnessService'), 'build') - .mockImplementation((anchorCommitCID, merkleCAR) => { - witnessCAR = witnessService.build(anchorCommitCID, merkleCAR) - return witnessCAR - }) - - const spyIpfsDagGet = jest.spyOn(ceramic.ipfs.dag, 'get') - const spyImportCAR = jest.spyOn(ceramic.dispatcher, 'importCAR') - - // Start the meat of the test: create a tile stream, and anchor it - const tile = await TileDocument.create(ceramic as any, { foo: 'blah' }, null, { anchor: true }) - await cas.container.resolve('anchorService').anchorRequests() - await waitForAnchor(tile) - - // CAS builds a witness CAR - expect(spyBuildWitnessCAR).toBeCalledTimes(1) - // Ceramic node imports witness CAR prepared by CAS - expect(spyImportCAR).toHaveBeenCalledWith(witnessCAR) - // Ceramic node only retrieves a genesis and an anchor commits in `handleCommit` - expect(spyIpfsDagGet.mock.calls.length).toEqual(2) - - // Teardown - await ceramic.close() - await cas.stop() - await ganacheServer.close() - await casIPFS.stop() - }) -}) - -describe('Metrics Options', () => { - test('cas starts with a typical instance identifier', async () => { - const ipfsApiPort = await getPort() - const casIPFS = await createIPFS(ipfsApiPort) - const ganacheServer = await makeGanache() - const dbConnection = await createDbConnection() - const dummyReplicaDbConnection = await createReplicaDbConnection() - const casPort = await getPort() - const cas = await makeCAS( - createInjector(), - dbConnection, - { - mode: 'server', - ipfsPort: ipfsApiPort, - ganachePort: ganacheServer.port, - port: casPort, - useSmartContractAnchors: true, - metrics: { - instanceIdentifier: '234fffffffffffffffffffffffffffffffff9726129', - }, - }, - dummyReplicaDbConnection - ) - await cas.start() - // Teardown - await cas.stop() - - const cas2 = await makeCAS( - createInjector(), - dbConnection, - { - mode: 'server', - ipfsPort: ipfsApiPort, - ganachePort: ganacheServer.port, - port: casPort, - useSmartContractAnchors: true, - metrics: { - instanceIdentifier: '', - }, - }, - dummyReplicaDbConnection - ) - await cas2.start() - await cas2.stop() - - await ganacheServer.close() - await casIPFS.stop() - }) -}) diff --git a/src/ancillary/anchor-request-params-parser.ts b/src/ancillary/anchor-request-params-parser.ts index 8e85dd28..f61821c8 100644 --- a/src/ancillary/anchor-request-params-parser.ts +++ b/src/ancillary/anchor-request-params-parser.ts @@ -28,16 +28,18 @@ import { const carFactory = new CARFactory() carFactory.codecs.add(DAG_JOSE) -export const RequestAnchorParamsV1 = sparse( +const RequestAnchorParamsV3 = sparse( { streamId: string.pipe(streamIdAsString), cid: string.pipe(cidAsString), - timestamp: optional(date), + timestamp: date, + jsCeramicVersion: optional(string), + ceramicOneVersion: optional(string), }, - 'RequestAnchorParamsV1' + 'RequestAnchorParamsV3' ) -type RequestAnchorParamsV1 = TypeOf +export type RequestAnchorParamsV3 = TypeOf const RequestAnchorParamsV2Root = strict({ streamId: uint8array.pipe(streamIdAsBytes), @@ -56,12 +58,12 @@ export const RequestAnchorParamsV2 = sparse({ export type RequestAnchorParamsV2 = TypeOf -export type RequestAnchorParams = RequestAnchorParamsV1 | RequestAnchorParamsV2 +export type RequestAnchorParams = RequestAnchorParamsV3 | RequestAnchorParamsV2 /** * Encode request params for logging purposes. */ -export const RequestAnchorParamsCodec = union([RequestAnchorParamsV1, RequestAnchorParamsV2]) +export const RequestAnchorParamsCodec = union([RequestAnchorParamsV3, RequestAnchorParamsV2]) export class AnchorRequestCarFileDecoder implements Decoder { readonly name = 'RequestAnchorParamsV2' @@ -95,8 +97,8 @@ export class AnchorRequestParamsParser { parse(req: ExpReq): Validation { if (req.get('Content-Type') !== 'application/vnd.ipld.car') { // Legacy requests - Metrics.count(METRIC_NAMES.CTRL_LEGACY_REQUESTED, 1) - return validate(RequestAnchorParamsV1, req.body) + Metrics.count(METRIC_NAMES.CTRL_JSON_REQUESTED, 1) + return validate(RequestAnchorParamsV3, req.body) } else { // Next version of anchor requests, using the CAR file format // TODO: CDB-2212 Store the car file somewhere for future reference/validation of signatures diff --git a/src/controllers/__tests__/request-controller.test.ts b/src/controllers/__tests__/request-controller.test.ts index 0d85cde9..0b8cffb8 100644 --- a/src/controllers/__tests__/request-controller.test.ts +++ b/src/controllers/__tests__/request-controller.test.ts @@ -212,7 +212,7 @@ describe('createRequest', () => { expect(createdRequest.origin).toEqual(origin) }) - test('timestamp is empty', async () => { + test('timestamp is required', async () => { const cid = randomCID() const streamId = randomStreamID() const now = new Date() @@ -229,17 +229,7 @@ describe('createRequest', () => { const requestRepository = container.resolve('requestRepository') await expect(requestRepository.findByCid(cid)).resolves.toBeUndefined() await controller.createRequest(req, res) - expect(res.status).toBeCalledWith(StatusCodes.CREATED) - const createdRequest = await requestRepository.findByCid(cid) - expectPresent(createdRequest) - expect(createdRequest.cid).toEqual(cid.toString()) - expect(createdRequest.status).toEqual(RequestStatus.PENDING) - expect(createdRequest.streamId).toEqual(streamId.toString()) - expect(createdRequest.message).toEqual('Request is pending.') - expect(isClose(createdRequest.timestamp.getTime(), now.getTime())).toBeTruthy() - expect(isClose(createdRequest.createdAt.getTime(), now.getTime())).toBeTruthy() - expect(isClose(createdRequest.updatedAt.getTime(), now.getTime())).toBeTruthy() - expect(createdRequest.origin).not.toBeNull() + expect(res.status).toBeCalledWith(StatusCodes.BAD_REQUEST) }) test('mark previous submissions REPLACED', async () => { @@ -249,6 +239,7 @@ describe('createRequest', () => { body: { cid: cid.toString(), streamId: streamId.toString(), + timestamp: new Date().toISOString(), }, }) const requestRepository = container.resolve('requestRepository') @@ -332,6 +323,7 @@ describe('createRequest', () => { body: { cid: cid.toString(), streamId: streamId.toString(), + timestamp: new Date().toISOString(), }, }) const validationQueueService = container.resolve('validationQueueService') diff --git a/src/controllers/request-controller.ts b/src/controllers/request-controller.ts index 25bb3919..da3fee9a 100644 --- a/src/controllers/request-controller.ts +++ b/src/controllers/request-controller.ts @@ -12,6 +12,7 @@ import { AnchorRequestParamsParser, RequestAnchorParams, RequestAnchorParamsCodec, + RequestAnchorParamsV3, } from '../ancillary/anchor-request-params-parser.js' import bodyParser from 'body-parser' import { type RequestService, RequestDoesNotExistError } from '../services/request-service.js' @@ -124,7 +125,12 @@ export class RequestController { // request was newly created if (body) { - Metrics.count(METRIC_NAMES.CTRL_NEW_ANCHOR_REQUEST, 1, { source: parseOrigin(req) }) + const v3Params = requestParams as RequestAnchorParamsV3 + Metrics.count(METRIC_NAMES.CTRL_NEW_ANCHOR_REQUEST, 1, { + source: parseOrigin(req), + jsCeramicVersion: v3Params.jsCeramicVersion, + ceramicOneVersion: v3Params.ceramicOneVersion, + }) return res.status(StatusCodes.CREATED).json(body) } diff --git a/src/settings.ts b/src/settings.ts index f080aa33..27b5ebb0 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -84,7 +84,7 @@ export enum METRIC_NAMES { CTRL_NEW_ANCHOR_REQUEST = 'ctrl_new_anchor_request', CTRL_FOUND_EXISTING_REQUEST = 'ctrl_found_existing_request', CTRL_CAR_REQUESTED = 'ctrl_car_requested', - CTRL_LEGACY_REQUESTED = 'ctrl_legacy_requested', + CTRL_JSON_REQUESTED = 'ctrl_json_requested', CTRL_INVALID_REQUEST = 'ctrl_invalid_request', CTRL_ERROR_CREATING_REQUEST = 'ctrl_error_creating_request', CTRL_REQUEST_NOT_FOUND = 'ctrl_request_not_found',