From b7cb563b8b949cdba7c361698643acdb82124632 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Thu, 9 Apr 2026 12:11:45 +0100 Subject: [PATCH 1/8] feat: add chomp-api-service --- .github/CODEOWNERS | 3 + README.md | 5 + packages/chomp-api-service/CHANGELOG.md | 14 ++ packages/chomp-api-service/LICENSE | 20 ++ packages/chomp-api-service/README.md | 15 ++ packages/chomp-api-service/jest.config.js | 26 +++ packages/chomp-api-service/package.json | 76 ++++++++ .../chomp-api-service-method-action-types.ts | 19 ++ .../src/chomp-api-service.test.ts | 136 ++++++++++++++ .../src/chomp-api-service.ts | 174 ++++++++++++++++++ packages/chomp-api-service/src/index.ts | 13 ++ .../chomp-api-service/tsconfig.build.json | 14 ++ packages/chomp-api-service/tsconfig.json | 12 ++ packages/chomp-api-service/typedoc.json | 7 + yarn.lock | 23 +++ 15 files changed, 557 insertions(+) create mode 100644 packages/chomp-api-service/CHANGELOG.md create mode 100644 packages/chomp-api-service/LICENSE create mode 100644 packages/chomp-api-service/README.md create mode 100644 packages/chomp-api-service/jest.config.js create mode 100644 packages/chomp-api-service/package.json create mode 100644 packages/chomp-api-service/src/chomp-api-service-method-action-types.ts create mode 100644 packages/chomp-api-service/src/chomp-api-service.test.ts create mode 100644 packages/chomp-api-service/src/chomp-api-service.ts create mode 100644 packages/chomp-api-service/src/index.ts create mode 100644 packages/chomp-api-service/tsconfig.build.json create mode 100644 packages/chomp-api-service/tsconfig.json create mode 100644 packages/chomp-api-service/typedoc.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7da370ce3fe..d7de5a8aef5 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -39,6 +39,7 @@ ## Earn Team /packages/earn-controller @MetaMask/earn +/packages/chomp-api-service @MetaMask/earn ## Social AI Team /packages/ai-controllers @MetaMask/social-ai @@ -226,3 +227,5 @@ /packages/social-controllers/CHANGELOG.md @MetaMask/social-ai @MetaMask/core-platform /packages/money-account-controller/package.json @MetaMask/accounts-engineers @MetaMask/core-platform /packages/money-account-controller/CHANGELOG.md @MetaMask/accounts-engineers @MetaMask/core-platform +/packages/chomp-api-service/package.json @MetaMask/accounts-engineers @MetaMask/core-platform +/packages/chomp-api-service/CHANGELOG.md @MetaMask/accounts-engineers @MetaMask/core-platform diff --git a/README.md b/README.md index 352fec4e6ec..7e2e6fb75f1 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ Each package in this repository has its own README where you can find installati - [`@metamask/bridge-status-controller`](packages/bridge-status-controller) - [`@metamask/build-utils`](packages/build-utils) - [`@metamask/chain-agnostic-permission`](packages/chain-agnostic-permission) +- [`@metamask/chomp-api-service`](packages/chomp-api-service) - [`@metamask/claims-controller`](packages/claims-controller) - [`@metamask/client-controller`](packages/client-controller) - [`@metamask/compliance-controller`](packages/compliance-controller) @@ -123,6 +124,7 @@ linkStyle default opacity:0.5 bridge_status_controller(["@metamask/bridge-status-controller"]); build_utils(["@metamask/build-utils"]); chain_agnostic_permission(["@metamask/chain-agnostic-permission"]); + chomp_api_service(["@metamask/chomp-api-service"]); claims_controller(["@metamask/claims-controller"]); client_controller(["@metamask/client-controller"]); compliance_controller(["@metamask/compliance-controller"]); @@ -274,6 +276,9 @@ linkStyle default opacity:0.5 bridge_status_controller --> transaction_controller; chain_agnostic_permission --> controller_utils; chain_agnostic_permission --> permission_controller; + chomp_api_service --> base_data_service; + chomp_api_service --> controller_utils; + chomp_api_service --> messenger; claims_controller --> base_controller; claims_controller --> controller_utils; claims_controller --> keyring_controller; diff --git a/packages/chomp-api-service/CHANGELOG.md b/packages/chomp-api-service/CHANGELOG.md new file mode 100644 index 00000000000..0a6ede27d26 --- /dev/null +++ b/packages/chomp-api-service/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- Add `ChompApiService` ([#8361](TODO)) + +[Unreleased]: https://github.com/MetaMask/core/ diff --git a/packages/chomp-api-service/LICENSE b/packages/chomp-api-service/LICENSE new file mode 100644 index 00000000000..c8a0ff6be3a --- /dev/null +++ b/packages/chomp-api-service/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2026 MetaMask + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE diff --git a/packages/chomp-api-service/README.md b/packages/chomp-api-service/README.md new file mode 100644 index 00000000000..a12245d8d5f --- /dev/null +++ b/packages/chomp-api-service/README.md @@ -0,0 +1,15 @@ +# `@metamask/chom-api-service` + +Chomp API data service. + +## Installation + +`yarn add @metamask/chom-api-service` + +or + +`npm install @metamask/chom-api-service` + +## Contributing + +This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/core#readme). diff --git a/packages/chomp-api-service/jest.config.js b/packages/chomp-api-service/jest.config.js new file mode 100644 index 00000000000..ca084133399 --- /dev/null +++ b/packages/chomp-api-service/jest.config.js @@ -0,0 +1,26 @@ +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/configuration + */ + +const merge = require('deepmerge'); +const path = require('path'); + +const baseConfig = require('../../jest.config.packages'); + +const displayName = path.basename(__dirname); + +module.exports = merge(baseConfig, { + // The display name when running multiple projects + displayName, + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 100, + functions: 100, + lines: 100, + statements: 100, + }, + }, +}); diff --git a/packages/chomp-api-service/package.json b/packages/chomp-api-service/package.json new file mode 100644 index 00000000000..203f007a605 --- /dev/null +++ b/packages/chomp-api-service/package.json @@ -0,0 +1,76 @@ +{ + "name": "@metamask/chomp-api-service", + "version": "0.0.0", + "description": "Data service for the Chomp API", + "keywords": [ + "MetaMask", + "Ethereum" + ], + "homepage": "https://github.com/MetaMask/core/tree/main/packages/chomp-api-service#readme", + "bugs": { + "url": "https://github.com/MetaMask/core/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/core.git" + }, + "license": "MIT", + "sideEffects": false, + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + }, + "./package.json": "./package.json" + }, + "main": "./dist/index.cjs", + "types": "./dist/index.d.cts", + "files": [ + "dist/" + ], + "scripts": { + "build": "ts-bridge --project tsconfig.build.json --verbose --clean --no-references", + "build:all": "ts-bridge --project tsconfig.build.json --verbose --clean", + "build:docs": "typedoc", + "changelog:update": "../../scripts/update-changelog.sh @metamask/chomp-api-service", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/chomp-api-service", + "since-latest-release": "../../scripts/since-latest-release.sh", + "test": "NODE_OPTIONS=--experimental-vm-modules jest --reporters=jest-silent-reporter", + "test:clean": "NODE_OPTIONS=--experimental-vm-modules jest --clearCache", + "test:verbose": "NODE_OPTIONS=--experimental-vm-modules jest --verbose", + "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" + }, + "dependencies": { + "@metamask/base-data-service": "^0.1.1", + "@metamask/controller-utils": "^11.20.0", + "@metamask/messenger": "^1.1.1", + "@metamask/superstruct": "^3.1.0", + "@metamask/utils": "^11.9.0", + "@tanstack/query-core": "^4.43.0" + }, + "devDependencies": { + "@metamask/auto-changelog": "^3.4.4", + "@ts-bridge/cli": "^0.6.4", + "@types/jest": "^29.5.14", + "deepmerge": "^4.2.2", + "jest": "^29.7.0", + "nock": "^13.3.1", + "ts-jest": "^29.2.5", + "typedoc": "^0.25.13", + "typedoc-plugin-missing-exports": "^2.0.0", + "typescript": "~5.3.3" + }, + "engines": { + "node": "^18.18 || >=20" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/chomp-api-service/src/chomp-api-service-method-action-types.ts b/packages/chomp-api-service/src/chomp-api-service-method-action-types.ts new file mode 100644 index 00000000000..f08d6330999 --- /dev/null +++ b/packages/chomp-api-service/src/chomp-api-service-method-action-types.ts @@ -0,0 +1,19 @@ +/** + * This file is auto generated. + * Do not edit manually. + */ + +import type { ChompApiService } from './chomp-api-service'; + +/** + * TODO: Add JSDoc description for this action. + */ +export type ChompApiServiceFetchAction = { + type: `ChompApiService:fetch`; + handler: ChompApiService['fetch']; +}; + +/** + * Union of all ChompApiService action types. + */ +export type ChompApiServiceMethodActions = ChompApiServiceFetchAction; diff --git a/packages/chomp-api-service/src/chomp-api-service.test.ts b/packages/chomp-api-service/src/chomp-api-service.test.ts new file mode 100644 index 00000000000..663c7b7fad9 --- /dev/null +++ b/packages/chomp-api-service/src/chomp-api-service.test.ts @@ -0,0 +1,136 @@ +import { DEFAULT_MAX_RETRIES } from '@metamask/controller-utils'; +import { Messenger, MOCK_ANY_NAMESPACE } from '@metamask/messenger'; +import type { + MockAnyNamespace, + MessengerActions, + MessengerEvents, +} from '@metamask/messenger'; +import nock from 'nock'; + +import type { ChompApiServiceMessenger } from './chomp-api-service'; +import { ChompApiService } from './chomp-api-service'; + +describe('ChompApiService', () => { + describe('ChompApiService:fetch', () => { + it('returns the response from the API', async () => { + nock('https://api.chomp.example.com') + .get('/items/abc') + .reply(200, { id: 'abc' }); + const { rootMessenger } = createService(); + + const response = await rootMessenger.call( + 'ChompApiService:fetch', + 'abc', + ); + + expect(response).toStrictEqual({ id: 'abc' }); + }); + + it('throws if the API returns a non-200 status', async () => { + nock('https://api.chomp.example.com') + .get('/items/abc') + .times(DEFAULT_MAX_RETRIES + 1) + .reply(500); + const { rootMessenger } = createService(); + + await expect( + rootMessenger.call('ChompApiService:fetch', 'abc'), + ).rejects.toThrow("Chomp API failed with status '500'"); + }); + + it.each([ + 'not an object', + { missing: 'id' }, + { id: 123 }, + ])( + 'throws if the API returns a malformed response %o', + async (response) => { + nock('https://api.chomp.example.com') + .get('/items/abc') + .reply(200, JSON.stringify(response)); + const { rootMessenger } = createService(); + + await expect( + rootMessenger.call('ChompApiService:fetch', 'abc'), + ).rejects.toThrow('Malformed response received from Chomp API'); + }, + ); + }); + + describe('fetch', () => { + it('does the same thing as the messenger action', async () => { + nock('https://api.chomp.example.com') + .get('/items/abc') + .reply(200, { id: 'abc' }); + const { service } = createService(); + + const response = await service.fetch('abc'); + + expect(response).toStrictEqual({ id: 'abc' }); + }); + }); +}); + +/** + * The type of the messenger populated with all external actions and events + * required by the service under test. + */ +type RootMessenger = Messenger< + MockAnyNamespace, + MessengerActions, + MessengerEvents +>; + +/** + * Constructs the messenger populated with all external actions and events + * required by the service under test. + * + * @returns The root messenger. + */ +function createRootMessenger(): RootMessenger { + return new Messenger({ namespace: MOCK_ANY_NAMESPACE }); +} + +/** + * Constructs the messenger for the service under test. + * + * @param rootMessenger - The root messenger, with all external actions and + * events required by the controller's messenger. + * @returns The service-specific messenger. + */ +function createServiceMessenger( + rootMessenger: RootMessenger, +): ChompApiServiceMessenger { + return new Messenger({ + namespace: 'ChompApiService', + parent: rootMessenger, + }); +} + +/** + * Constructs the service under test. + * + * @param args - The arguments to this function. + * @param args.options - The options that the service constructor takes. All are + * optional and will be filled in with defaults as needed (including + * `messenger`). + * @returns The new service, root messenger, and service messenger. + */ +function createService({ + options = {}, +}: { + options?: Partial[0]>; +} = {}): { + service: ChompApiService; + rootMessenger: RootMessenger; + messenger: ChompApiServiceMessenger; +} { + const rootMessenger = createRootMessenger(); + const messenger = createServiceMessenger(rootMessenger); + const service = new ChompApiService({ + messenger, + ...options, + }); + + return { service, rootMessenger, messenger }; +} diff --git a/packages/chomp-api-service/src/chomp-api-service.ts b/packages/chomp-api-service/src/chomp-api-service.ts new file mode 100644 index 00000000000..b262ea5171e --- /dev/null +++ b/packages/chomp-api-service/src/chomp-api-service.ts @@ -0,0 +1,174 @@ +import { BaseDataService } from '@metamask/base-data-service'; +import type { + DataServiceCacheUpdatedEvent, + DataServiceGranularCacheUpdatedEvent, + DataServiceInvalidateQueriesAction, +} from '@metamask/base-data-service'; +import type { CreateServicePolicyOptions } from '@metamask/controller-utils'; +import { HttpError } from '@metamask/controller-utils'; +import type { Messenger } from '@metamask/messenger'; +import type { Infer } from '@metamask/superstruct'; +import { is, object, string } from '@metamask/superstruct'; +import type { QueryClientConfig } from '@tanstack/query-core'; + +import type { ChompApiServiceMethodActions } from './chomp-api-service-method-action-types'; + +// === GENERAL === + +/** + * The name of the {@link ChompApiService}, used to namespace the service's + * actions and events. + */ +export const serviceName = 'ChompApiService'; + +// === MESSENGER === + +/** + * All of the methods within {@link ChompApiService} that are exposed via the + * messenger. + */ +const MESSENGER_EXPOSED_METHODS = ['fetch'] as const; + +/** + * Invalidates cached queries for {@link ChompApiService}. + */ +export type ChompApiServiceInvalidateQueriesAction = + DataServiceInvalidateQueriesAction; + +/** + * Actions that {@link ChompApiService} exposes to other consumers. + */ +export type ChompApiServiceActions = + | ChompApiServiceMethodActions + | ChompApiServiceInvalidateQueriesAction; + +/** + * Actions from other messengers that {@link ChompApiService} calls. + */ +type AllowedActions = never; + +/** + * Published when {@link ChompApiService}'s cache is updated. + */ +export type ChompApiServiceCacheUpdatedEvent = + DataServiceCacheUpdatedEvent; + +/** + * Published when a key within {@link ChompApiService}'s cache is updated. + */ +export type ChompApiServiceGranularCacheUpdatedEvent = + DataServiceGranularCacheUpdatedEvent; + +/** + * Events that {@link ChompApiService} exposes to other consumers. + */ +export type ChompApiServiceEvents = + | ChompApiServiceCacheUpdatedEvent + | ChompApiServiceGranularCacheUpdatedEvent; + +/** + * Events from other messengers that {@link ChompApiService} subscribes to. + */ +type AllowedEvents = never; + +/** + * The messenger which is restricted to actions and events accessed by + * {@link ChompApiService}. + */ +export type ChompApiServiceMessenger = Messenger< + typeof serviceName, + ChompApiServiceActions | AllowedActions, + ChompApiServiceEvents | AllowedEvents +>; + +// === SERVICE DEFINITION === + +// TODO: Define the response struct to match the actual Chomp API response shape. +const ChompResponseStruct = object({ + // TODO: Replace with real fields. + id: string(), +}); + +/** + * What the API endpoint returns. + */ +type ChompResponse = Infer; + +/** + * The base URL of the Chomp API. + */ +// TODO: Replace with the real Chomp API base URL. +const BASE_URL = 'https://api.chomp.example.com'; + +/** + * This service object is responsible for fetching data from the Chomp API. + */ +export class ChompApiService extends BaseDataService< + typeof serviceName, + ChompApiServiceMessenger +> { + /** + * Constructs a new ChompApiService object. + * + * @param args - The constructor arguments. + * @param args.messenger - The messenger suited for this service. + * @param args.queryClientConfig - Configuration for the underlying TanStack + * Query client. + * @param args.policyOptions - Options to pass to `createServicePolicy`, which + * is used to wrap each request. See {@link CreateServicePolicyOptions}. + */ + constructor({ + messenger, + queryClientConfig = {}, + policyOptions = {}, + }: { + messenger: ChompApiServiceMessenger; + queryClientConfig?: QueryClientConfig; + policyOptions?: CreateServicePolicyOptions; + }) { + super({ + name: serviceName, + messenger, + queryClientConfig, + policyOptions, + }); + + this.messenger.registerMethodActionHandlers( + this, + MESSENGER_EXPOSED_METHODS, + ); + } + + /** + * TODO: Replace with a real description of the endpoint this method calls. + * + * @param id - TODO: Describe the parameter. + * @returns TODO: Describe the return value. + */ + async fetch(id: string): Promise { + // TODO: Build the real URL for the Chomp API endpoint. + const url = new URL(`/items/${id}`, BASE_URL); + + const jsonResponse = await this.fetchQuery({ + queryKey: [`${this.name}:fetch`, id], + queryFn: async () => { + const response = await fetch(url); + + if (!response.ok) { + throw new HttpError( + response.status, + `Chomp API failed with status '${response.status}'`, + ); + } + + return response.json(); + }, + }); + + if (!is(jsonResponse, ChompResponseStruct)) { + throw new Error('Malformed response received from Chomp API'); + } + + return jsonResponse; + } +} diff --git a/packages/chomp-api-service/src/index.ts b/packages/chomp-api-service/src/index.ts new file mode 100644 index 00000000000..4b30fc4773a --- /dev/null +++ b/packages/chomp-api-service/src/index.ts @@ -0,0 +1,13 @@ +export { ChompApiService } from './chomp-api-service'; +export type { + ChompApiServiceMessenger, + ChompApiServiceActions, + ChompApiServiceEvents, + ChompApiServiceInvalidateQueriesAction, + ChompApiServiceCacheUpdatedEvent, + ChompApiServiceGranularCacheUpdatedEvent, +} from './chomp-api-service'; +export type { + ChompApiServiceMethodActions, + ChompApiServiceFetchAction, +} from './chomp-api-service-method-action-types'; diff --git a/packages/chomp-api-service/tsconfig.build.json b/packages/chomp-api-service/tsconfig.build.json new file mode 100644 index 00000000000..c468e8dd1f5 --- /dev/null +++ b/packages/chomp-api-service/tsconfig.build.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [ + { "path": "../messenger/tsconfig.build.json" }, + { "path": "../controller-utils/tsconfig.build.json" }, + { "path": "../base-data-service/tsconfig.build.json" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/chomp-api-service/tsconfig.json b/packages/chomp-api-service/tsconfig.json new file mode 100644 index 00000000000..203994e1c3c --- /dev/null +++ b/packages/chomp-api-service/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "references": [ + { "path": "../messenger" }, + { "path": "../controller-utils" }, + { "path": "../base-data-service" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/chomp-api-service/typedoc.json b/packages/chomp-api-service/typedoc.json new file mode 100644 index 00000000000..c9da015dbf8 --- /dev/null +++ b/packages/chomp-api-service/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} diff --git a/yarn.lock b/yarn.lock index 5feef62cd40..7c960fbabc4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3126,6 +3126,29 @@ __metadata: languageName: unknown linkType: soft +"@metamask/chomp-api-service@workspace:packages/chomp-api-service": + version: 0.0.0-use.local + resolution: "@metamask/chomp-api-service@workspace:packages/chomp-api-service" + dependencies: + "@metamask/auto-changelog": "npm:^3.4.4" + "@metamask/base-data-service": "npm:^0.1.1" + "@metamask/controller-utils": "npm:^11.20.0" + "@metamask/messenger": "npm:^1.1.1" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^11.9.0" + "@tanstack/query-core": "npm:^4.43.0" + "@ts-bridge/cli": "npm:^0.6.4" + "@types/jest": "npm:^29.5.14" + deepmerge: "npm:^4.2.2" + jest: "npm:^29.7.0" + nock: "npm:^13.3.1" + ts-jest: "npm:^29.2.5" + typedoc: "npm:^0.25.13" + typedoc-plugin-missing-exports: "npm:^2.0.0" + typescript: "npm:~5.3.3" + languageName: unknown + linkType: soft + "@metamask/claims-controller@workspace:packages/claims-controller": version: 0.0.0-use.local resolution: "@metamask/claims-controller@workspace:packages/claims-controller" From 4fe98129b6126b386555542862eaa571ab831c43 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Thu, 9 Apr 2026 12:30:40 +0100 Subject: [PATCH 2/8] feat: add stubs for all API methods --- packages/chomp-api-service/CHANGELOG.md | 2 +- .../chomp-api-service-method-action-types.ts | 65 ++++- .../src/chomp-api-service.test.ts | 163 +++++++---- .../src/chomp-api-service.ts | 266 ++++++++++++++---- packages/chomp-api-service/src/index.ts | 21 +- packages/chomp-api-service/src/types.ts | 76 +++++ 6 files changed, 488 insertions(+), 105 deletions(-) create mode 100644 packages/chomp-api-service/src/types.ts diff --git a/packages/chomp-api-service/CHANGELOG.md b/packages/chomp-api-service/CHANGELOG.md index 0a6ede27d26..c050ac9419d 100644 --- a/packages/chomp-api-service/CHANGELOG.md +++ b/packages/chomp-api-service/CHANGELOG.md @@ -9,6 +9,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add `ChompApiService` ([#8361](TODO)) +- Add `ChompApiService` ([#8361](https://github.com/MetaMask/core/pull/8413)) [Unreleased]: https://github.com/MetaMask/core/ diff --git a/packages/chomp-api-service/src/chomp-api-service-method-action-types.ts b/packages/chomp-api-service/src/chomp-api-service-method-action-types.ts index f08d6330999..87454e3759d 100644 --- a/packages/chomp-api-service/src/chomp-api-service-method-action-types.ts +++ b/packages/chomp-api-service/src/chomp-api-service-method-action-types.ts @@ -6,14 +6,69 @@ import type { ChompApiService } from './chomp-api-service'; /** - * TODO: Add JSDoc description for this action. + * Associates an address with a CHOMP profile via POST /v1/auth/address. */ -export type ChompApiServiceFetchAction = { - type: `ChompApiService:fetch`; - handler: ChompApiService['fetch']; +export type ChompApiServiceAssociateAddressAction = { + type: `ChompApiService:associateAddress`; + handler: ChompApiService['associateAddress']; +}; + +/** + * Creates an account upgrade via POST /v1/account-upgrade. + */ +export type ChompApiServiceCreateUpgradeAction = { + type: `ChompApiService:createUpgrade`; + handler: ChompApiService['createUpgrade']; +}; + +/** + * Fetches the upgrade record for an address via GET /v1/account-upgrade/:address. + */ +export type ChompApiServiceGetUpgradeAction = { + type: `ChompApiService:getUpgrade`; + handler: ChompApiService['getUpgrade']; +}; + +/** + * Verifies a delegation via POST /v1/intent/verify-delegation. + */ +export type ChompApiServiceVerifyDelegationAction = { + type: `ChompApiService:verifyDelegation`; + handler: ChompApiService['verifyDelegation']; +}; + +/** + * Submits intents via POST /v1/intent. + */ +export type ChompApiServiceCreateIntentsAction = { + type: `ChompApiService:createIntents`; + handler: ChompApiService['createIntents']; +}; + +/** + * Fetches intents by address via GET /v1/intent/account/:address. + */ +export type ChompApiServiceGetIntentsByAddressAction = { + type: `ChompApiService:getIntentsByAddress`; + handler: ChompApiService['getIntentsByAddress']; +}; + +/** + * Creates a withdrawal for card spend flows. + */ +export type ChompApiServiceCreateWithdrawalAction = { + type: `ChompApiService:createWithdrawal`; + handler: ChompApiService['createWithdrawal']; }; /** * Union of all ChompApiService action types. */ -export type ChompApiServiceMethodActions = ChompApiServiceFetchAction; +export type ChompApiServiceMethodActions = + | ChompApiServiceAssociateAddressAction + | ChompApiServiceCreateUpgradeAction + | ChompApiServiceGetUpgradeAction + | ChompApiServiceVerifyDelegationAction + | ChompApiServiceCreateIntentsAction + | ChompApiServiceGetIntentsByAddressAction + | ChompApiServiceCreateWithdrawalAction; diff --git a/packages/chomp-api-service/src/chomp-api-service.test.ts b/packages/chomp-api-service/src/chomp-api-service.test.ts index 663c7b7fad9..dec0bab3159 100644 --- a/packages/chomp-api-service/src/chomp-api-service.test.ts +++ b/packages/chomp-api-service/src/chomp-api-service.test.ts @@ -1,72 +1,139 @@ -import { DEFAULT_MAX_RETRIES } from '@metamask/controller-utils'; import { Messenger, MOCK_ANY_NAMESPACE } from '@metamask/messenger'; import type { MockAnyNamespace, MessengerActions, MessengerEvents, } from '@metamask/messenger'; -import nock from 'nock'; import type { ChompApiServiceMessenger } from './chomp-api-service'; import { ChompApiService } from './chomp-api-service'; +const BASE_URL = 'https://api.chomp.example.com'; +const MOCK_TOKEN = 'mock-jwt-token'; + describe('ChompApiService', () => { - describe('ChompApiService:fetch', () => { - it('returns the response from the API', async () => { - nock('https://api.chomp.example.com') - .get('/items/abc') - .reply(200, { id: 'abc' }); - const { rootMessenger } = createService(); - - const response = await rootMessenger.call( - 'ChompApiService:fetch', - 'abc', - ); + describe('constructor', () => { + it('can be constructed with baseUrl, getAccessToken, and messenger', () => { + const { service } = createService(); + expect(service).toBeInstanceOf(ChompApiService); + }); - expect(response).toStrictEqual({ id: 'abc' }); + it('accepts a custom fetch implementation', () => { + const customFetch = jest.fn(); + const { service } = createService({ + options: { fetchFn: customFetch as unknown as typeof globalThis.fetch }, + }); + expect(service).toBeInstanceOf(ChompApiService); }); + }); - it('throws if the API returns a non-200 status', async () => { - nock('https://api.chomp.example.com') - .get('/items/abc') - .times(DEFAULT_MAX_RETRIES + 1) - .reply(500); - const { rootMessenger } = createService(); + describe('associateAddress', () => { + // TODO: Test POST /v1/auth/address with correct URL, method, headers + // (Authorization: Bearer ), and JSON body { signature, timestamp, address }. + // TODO: Test that 200 returns { profileId, address, status }. + // TODO: Test that 409 returns the response body (not throws). + // TODO: Test that other non-OK statuses throw. + // TODO: Test response validation rejects malformed responses. + it('is registered as a messenger action', async () => { + const { service } = createService(); + await expect(service.associateAddress({ + signature: '0x123', + timestamp: '2026-01-01T00:00:00Z', + address: '0xabc', + })).rejects.toThrow('Not implemented'); + }); + }); + + describe('createUpgrade', () => { + // TODO: Test POST /v1/account-upgrade with correct URL, method, headers + // (Authorization: Bearer ), and JSON body { r, s, v, yParity, address, chainId, nonce }. + // TODO: Test that 200 returns { signerAddress, status, createdAt }. + // TODO: Test that non-OK statuses throw. + // TODO: Test response validation rejects malformed responses. + it('is registered as a messenger action', async () => { + const { service } = createService(); + await expect(service.createUpgrade({ + r: '0x1', + s: '0x2', + v: 27, + yParity: 0, + address: '0xabc', + chainId: '1', + nonce: '0', + })).rejects.toThrow('Not implemented'); + }); + }); - await expect( - rootMessenger.call('ChompApiService:fetch', 'abc'), - ).rejects.toThrow("Chomp API failed with status '500'"); + describe('getUpgrade', () => { + // TODO: Test GET /v1/account-upgrade/:address with correct URL, method, + // and headers (Authorization: Bearer ). + // TODO: Test that 200 returns the upgrade record. + // TODO: Test that 404 returns null. + // TODO: Test that other non-OK statuses throw. + // TODO: Test response validation rejects malformed responses. + it('is registered as a messenger action', async () => { + const { service } = createService(); + await expect(service.getUpgrade('0xabc')).rejects.toThrow( + 'Not implemented', + ); }); + }); - it.each([ - 'not an object', - { missing: 'id' }, - { id: 123 }, - ])( - 'throws if the API returns a malformed response %o', - async (response) => { - nock('https://api.chomp.example.com') - .get('/items/abc') - .reply(200, JSON.stringify(response)); - const { rootMessenger } = createService(); - - await expect( - rootMessenger.call('ChompApiService:fetch', 'abc'), - ).rejects.toThrow('Malformed response received from Chomp API'); - }, - ); + describe('verifyDelegation', () => { + // TODO: Test POST /v1/intent/verify-delegation with correct URL, method, + // headers (Authorization: Bearer ), and JSON body { signedDelegation, chainId }. + // TODO: Test that 200 returns { valid, delegationHash?, errors? }. + // TODO: Test that non-OK statuses throw. + // TODO: Test response validation rejects malformed responses. + it('is registered as a messenger action', async () => { + const { service } = createService(); + await expect(service.verifyDelegation({ + signedDelegation: '0x123', + chainId: '1', + })).rejects.toThrow('Not implemented'); + }); }); - describe('fetch', () => { - it('does the same thing as the messenger action', async () => { - nock('https://api.chomp.example.com') - .get('/items/abc') - .reply(200, { id: 'abc' }); + describe('createIntents', () => { + // TODO: Test POST /v1/intent with correct URL, method, headers + // (Authorization: Bearer ), and JSON body (array of intents). + // TODO: Test that 200 returns SendIntentResponse[]. + // TODO: Test that non-OK statuses throw. + // TODO: Test response validation rejects malformed responses. + it('is registered as a messenger action', async () => { const { service } = createService(); + await expect(service.createIntents([{ foo: 'bar' }])).rejects.toThrow( + 'Not implemented', + ); + }); + }); - const response = await service.fetch('abc'); + describe('getIntentsByAddress', () => { + // TODO: Test GET /v1/intent/account/:address with correct URL, method, + // and headers (Authorization: Bearer ). + // TODO: Test that 200 returns an array of intents. + // TODO: Test that non-OK statuses throw. + // TODO: Test response validation rejects malformed responses. + it('is registered as a messenger action', async () => { + const { service } = createService(); + await expect(service.getIntentsByAddress('0xabc')).rejects.toThrow( + 'Not implemented', + ); + }); + }); - expect(response).toStrictEqual({ id: 'abc' }); + describe('createWithdrawal', () => { + // TODO: Confirm endpoint path against CHOMP API docs. + // TODO: Test POST to the withdrawal endpoint with correct URL, method, + // headers (Authorization: Bearer ), and JSON body. + // TODO: Test that 200 returns the withdrawal result. + // TODO: Test that non-OK statuses throw. + // TODO: Test response validation rejects malformed responses. + it('is registered as a messenger action', async () => { + const { service } = createService(); + await expect(service.createWithdrawal({})).rejects.toThrow( + 'Not implemented', + ); }); }); }); @@ -128,6 +195,8 @@ function createService({ const rootMessenger = createRootMessenger(); const messenger = createServiceMessenger(rootMessenger); const service = new ChompApiService({ + baseUrl: BASE_URL, + getAccessToken: async () => MOCK_TOKEN, messenger, ...options, }); diff --git a/packages/chomp-api-service/src/chomp-api-service.ts b/packages/chomp-api-service/src/chomp-api-service.ts index b262ea5171e..25b4d2d7d16 100644 --- a/packages/chomp-api-service/src/chomp-api-service.ts +++ b/packages/chomp-api-service/src/chomp-api-service.ts @@ -5,13 +5,23 @@ import type { DataServiceInvalidateQueriesAction, } from '@metamask/base-data-service'; import type { CreateServicePolicyOptions } from '@metamask/controller-utils'; -import { HttpError } from '@metamask/controller-utils'; import type { Messenger } from '@metamask/messenger'; -import type { Infer } from '@metamask/superstruct'; -import { is, object, string } from '@metamask/superstruct'; import type { QueryClientConfig } from '@tanstack/query-core'; import type { ChompApiServiceMethodActions } from './chomp-api-service-method-action-types'; +import type { + AssociateAddressRequest, + AssociateAddressResponse, + CreateUpgradeRequest, + CreateUpgradeResponse, + CreateWithdrawalRequest, + CreateWithdrawalResponse, + GetUpgradeResponse, + SendIntentRequest, + SendIntentResponse, + VerifyDelegationRequest, + VerifyDelegationResponse, +} from './types'; // === GENERAL === @@ -27,7 +37,15 @@ export const serviceName = 'ChompApiService'; * All of the methods within {@link ChompApiService} that are exposed via the * messenger. */ -const MESSENGER_EXPOSED_METHODS = ['fetch'] as const; +const MESSENGER_EXPOSED_METHODS = [ + 'associateAddress', + 'createUpgrade', + 'getUpgrade', + 'verifyDelegation', + 'createIntents', + 'getIntentsByAddress', + 'createWithdrawal', +] as const; /** * Invalidates cached queries for {@link ChompApiService}. @@ -83,46 +101,48 @@ export type ChompApiServiceMessenger = Messenger< // === SERVICE DEFINITION === -// TODO: Define the response struct to match the actual Chomp API response shape. -const ChompResponseStruct = object({ - // TODO: Replace with real fields. - id: string(), -}); - -/** - * What the API endpoint returns. - */ -type ChompResponse = Infer; - -/** - * The base URL of the Chomp API. - */ -// TODO: Replace with the real Chomp API base URL. -const BASE_URL = 'https://api.chomp.example.com'; - /** - * This service object is responsible for fetching data from the Chomp API. + * This service is responsible for communicating with the CHOMP API. + * + * All requests are authenticated via JWT Bearer tokens obtained from the + * `getAccessToken` callback provided at construction time. */ export class ChompApiService extends BaseDataService< typeof serviceName, ChompApiServiceMessenger > { + readonly #baseUrl: string; + + readonly #getAccessToken: () => Promise; + + readonly #fetch: typeof globalThis.fetch; + /** - * Constructs a new ChompApiService object. + * Constructs a new ChompApiService. * * @param args - The constructor arguments. * @param args.messenger - The messenger suited for this service. + * @param args.baseUrl - The base URL of the CHOMP API. + * @param args.getAccessToken - An async callback that returns a valid JWT + * access token for authenticating requests. + * @param args.fetchFn - An optional custom fetch implementation. Defaults to + * the global `fetch`. * @param args.queryClientConfig - Configuration for the underlying TanStack * Query client. - * @param args.policyOptions - Options to pass to `createServicePolicy`, which - * is used to wrap each request. See {@link CreateServicePolicyOptions}. + * @param args.policyOptions - Options to pass to `createServicePolicy`. */ constructor({ messenger, + baseUrl, + getAccessToken, + fetchFn = globalThis.fetch, queryClientConfig = {}, policyOptions = {}, }: { messenger: ChompApiServiceMessenger; + baseUrl: string; + getAccessToken: () => Promise; + fetchFn?: typeof globalThis.fetch; queryClientConfig?: QueryClientConfig; policyOptions?: CreateServicePolicyOptions; }) { @@ -133,6 +153,10 @@ export class ChompApiService extends BaseDataService< policyOptions, }); + this.#baseUrl = baseUrl; + this.#getAccessToken = getAccessToken; + this.#fetch = fetchFn; + this.messenger.registerMethodActionHandlers( this, MESSENGER_EXPOSED_METHODS, @@ -140,35 +164,175 @@ export class ChompApiService extends BaseDataService< } /** - * TODO: Replace with a real description of the endpoint this method calls. + * Builds the standard headers for an authenticated CHOMP API request. * - * @param id - TODO: Describe the parameter. - * @returns TODO: Describe the return value. + * @returns Headers including Authorization and Content-Type. */ - async fetch(id: string): Promise { - // TODO: Build the real URL for the Chomp API endpoint. - const url = new URL(`/items/${id}`, BASE_URL); - - const jsonResponse = await this.fetchQuery({ - queryKey: [`${this.name}:fetch`, id], - queryFn: async () => { - const response = await fetch(url); - - if (!response.ok) { - throw new HttpError( - response.status, - `Chomp API failed with status '${response.status}'`, - ); - } - - return response.json(); - }, - }); + async #authHeaders(): Promise> { + const token = await this.#getAccessToken(); + return { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }; + } + + /** + * Associates an address with a CHOMP profile. + * + * POST /v1/auth/address + * + * TODO: Implement the request using this.fetchQuery or direct POST via + * this.#fetch. Validate the response with a superstruct. Note that a 409 + * response is valid and should be returned (not thrown). + * + * @param request - The association request containing signature, timestamp, + * and address. + * @returns The profile association result. + */ + async associateAddress( + request: AssociateAddressRequest, + ): Promise { + // TODO: POST to `${this.#baseUrl}/v1/auth/address` with JSON body. + // TODO: Include Authorization header via this.#authHeaders(). + // TODO: Return response body on 200 or 409; throw on other non-OK statuses. + // TODO: Validate response shape with superstruct before returning. + const _headers = await this.#authHeaders(); + void request; + throw new Error('Not implemented'); + } - if (!is(jsonResponse, ChompResponseStruct)) { - throw new Error('Malformed response received from Chomp API'); - } + /** + * Creates an account upgrade request. + * + * POST /v1/account-upgrade + * + * TODO: Implement the POST request. Validate the response with a superstruct. + * + * @param request - The upgrade request containing signature components and + * chain details. + * @returns The upgrade result. + */ + async createUpgrade( + request: CreateUpgradeRequest, + ): Promise { + // TODO: POST to `${this.#baseUrl}/v1/account-upgrade` with JSON body. + // TODO: Include Authorization header via this.#authHeaders(). + // TODO: Throw on non-OK responses. + // TODO: Validate response shape with superstruct before returning. + const _headers = await this.#authHeaders(); + void request; + throw new Error('Not implemented'); + } - return jsonResponse; + /** + * Fetches the upgrade record for a given address. + * + * GET /v1/account-upgrade/:address + * + * TODO: Implement the GET request. Return null on 404. Validate the response + * with a superstruct for non-404 responses. + * + * @param address - The address to look up. + * @returns The upgrade record, or null if not found. + */ + async getUpgrade(address: string): Promise { + // TODO: GET `${this.#baseUrl}/v1/account-upgrade/${address}`. + // TODO: Include Authorization header via this.#authHeaders(). + // TODO: Return null on 404, throw on other non-OK statuses. + // TODO: Validate response shape with superstruct before returning. + // TODO: Consider using this.fetchQuery with a queryKey for caching. + const _headers = await this.#authHeaders(); + void address; + throw new Error('Not implemented'); + } + + /** + * Verifies a delegation signature. + * + * POST /v1/intent/verify-delegation + * + * TODO: Implement the POST request. Validate the response with a superstruct. + * + * @param request - The delegation verification request. + * @returns The verification result including validity and optional errors. + */ + async verifyDelegation( + request: VerifyDelegationRequest, + ): Promise { + // TODO: POST to `${this.#baseUrl}/v1/intent/verify-delegation` with JSON body. + // TODO: Include Authorization header via this.#authHeaders(). + // TODO: Throw on non-OK responses. + // TODO: Validate response shape with superstruct before returning. + const _headers = await this.#authHeaders(); + void request; + throw new Error('Not implemented'); + } + + /** + * Submits one or more intents to the CHOMP API. + * + * POST /v1/intent + * + * TODO: Implement the POST request. Validate the response array with a + * superstruct. + * + * @param intents - The array of intents to submit. + * @returns The array of intent responses. + */ + async createIntents( + intents: SendIntentRequest[], + ): Promise { + // TODO: POST to `${this.#baseUrl}/v1/intent` with JSON body. + // TODO: Include Authorization header via this.#authHeaders(). + // TODO: Throw on non-OK responses. + // TODO: Validate response shape with superstruct before returning. + const _headers = await this.#authHeaders(); + void intents; + throw new Error('Not implemented'); + } + + /** + * Fetches intents associated with a given address. + * + * GET /v1/intent/account/:address + * + * TODO: Implement the GET request. Validate the response array with a + * superstruct. + * + * @param address - The address to look up intents for. + * @returns The array of intents for the address. + */ + async getIntentsByAddress( + address: string, + ): Promise { + // TODO: GET `${this.#baseUrl}/v1/intent/account/${address}`. + // TODO: Include Authorization header via this.#authHeaders(). + // TODO: Throw on non-OK responses. + // TODO: Validate response shape with superstruct before returning. + // TODO: Consider using this.fetchQuery with a queryKey for caching. + const _headers = await this.#authHeaders(); + void address; + throw new Error('Not implemented'); + } + + /** + * Creates a withdrawal for card spend flows. + * + * TODO: Confirm the endpoint path against CHOMP API docs. + * TODO: Implement the POST request. Validate the response with a superstruct. + * + * @param request - The withdrawal request. + * @returns The withdrawal result. + */ + async createWithdrawal( + request: CreateWithdrawalRequest, + ): Promise { + // TODO: Confirm endpoint path (e.g. POST `${this.#baseUrl}/v1/withdrawal`). + // TODO: Include Authorization header via this.#authHeaders(). + // TODO: Throw on non-OK responses. + // TODO: Validate response shape with superstruct before returning. + const _headers = await this.#authHeaders(); + void request; + throw new Error('Not implemented'); } } diff --git a/packages/chomp-api-service/src/index.ts b/packages/chomp-api-service/src/index.ts index 4b30fc4773a..bcc77031af5 100644 --- a/packages/chomp-api-service/src/index.ts +++ b/packages/chomp-api-service/src/index.ts @@ -9,5 +9,24 @@ export type { } from './chomp-api-service'; export type { ChompApiServiceMethodActions, - ChompApiServiceFetchAction, + ChompApiServiceAssociateAddressAction, + ChompApiServiceCreateUpgradeAction, + ChompApiServiceGetUpgradeAction, + ChompApiServiceVerifyDelegationAction, + ChompApiServiceCreateIntentsAction, + ChompApiServiceGetIntentsByAddressAction, + ChompApiServiceCreateWithdrawalAction, } from './chomp-api-service-method-action-types'; +export type { + AssociateAddressRequest, + AssociateAddressResponse, + CreateUpgradeRequest, + CreateUpgradeResponse, + GetUpgradeResponse, + VerifyDelegationRequest, + VerifyDelegationResponse, + SendIntentRequest, + SendIntentResponse, + CreateWithdrawalRequest, + CreateWithdrawalResponse, +} from './types'; diff --git a/packages/chomp-api-service/src/types.ts b/packages/chomp-api-service/src/types.ts new file mode 100644 index 00000000000..b3a253cce55 --- /dev/null +++ b/packages/chomp-api-service/src/types.ts @@ -0,0 +1,76 @@ +// === REQUEST TYPES === + +export type AssociateAddressRequest = { + signature: string; + timestamp: string; + address: string; +}; + +export type CreateUpgradeRequest = { + r: string; + s: string; + v: number; + yParity: number; + address: string; + chainId: string; + nonce: string; +}; + +export type VerifyDelegationRequest = { + signedDelegation: string; + chainId: string; +}; + +/** + * A single intent to be submitted to the Chomp API. + * TODO: Define the full shape of an intent once the API schema is confirmed. + */ +export type SendIntentRequest = Record; + +/** + * TODO: Define request shape once the withdrawal endpoint path and schema are + * confirmed against CHOMP API docs. + */ +export type CreateWithdrawalRequest = Record; + +// === RESPONSE TYPES === + +export type AssociateAddressResponse = { + profileId: string; + address: string; + status: string; +}; + +export type CreateUpgradeResponse = { + signerAddress: string; + status: string; + createdAt: string; +}; + +/** + * The upgrade record returned by GET /v1/account-upgrade/:address. + * TODO: Confirm full shape against CHOMP API docs. + */ +export type GetUpgradeResponse = { + signerAddress: string; + status: string; + createdAt: string; +}; + +export type VerifyDelegationResponse = { + valid: boolean; + delegationHash?: string; + errors?: string[]; +}; + +/** + * A single intent response. + * TODO: Define the full shape once the API schema is confirmed. + */ +export type SendIntentResponse = Record; + +/** + * TODO: Define response shape once the withdrawal endpoint path and schema are + * confirmed against CHOMP API docs. + */ +export type CreateWithdrawalResponse = Record; From be043348f479bb3d8b227d0bc9beb31eb1fffe0a Mon Sep 17 00:00:00 2001 From: John Whiles Date: Fri, 10 Apr 2026 11:05:17 +0100 Subject: [PATCH 3/8] feat: add implementation of chomp-api --- .../src/chomp-api-service.test.ts | 484 +++++++++++++++--- .../src/chomp-api-service.ts | 263 +++++++--- packages/chomp-api-service/src/index.ts | 14 +- packages/chomp-api-service/src/types.ts | 93 +++- 4 files changed, 662 insertions(+), 192 deletions(-) diff --git a/packages/chomp-api-service/src/chomp-api-service.test.ts b/packages/chomp-api-service/src/chomp-api-service.test.ts index dec0bab3159..0c79cc63076 100644 --- a/packages/chomp-api-service/src/chomp-api-service.test.ts +++ b/packages/chomp-api-service/src/chomp-api-service.test.ts @@ -1,9 +1,11 @@ +import { DEFAULT_MAX_RETRIES } from '@metamask/controller-utils'; import { Messenger, MOCK_ANY_NAMESPACE } from '@metamask/messenger'; import type { MockAnyNamespace, MessengerActions, MessengerEvents, } from '@metamask/messenger'; +import nock from 'nock'; import type { ChompApiServiceMessenger } from './chomp-api-service'; import { ChompApiService } from './chomp-api-service'; @@ -12,127 +14,445 @@ const BASE_URL = 'https://api.chomp.example.com'; const MOCK_TOKEN = 'mock-jwt-token'; describe('ChompApiService', () => { - describe('constructor', () => { - it('can be constructed with baseUrl, getAccessToken, and messenger', () => { - const { service } = createService(); - expect(service).toBeInstanceOf(ChompApiService); - }); + describe('associateAddress', () => { + it('sends a POST with auth headers and returns the response on 201', async () => { + nock(BASE_URL) + .post('/v1/auth/address', { + signature: '0x123', + timestamp: '2026-01-01T00:00:00Z', + address: '0xabc', + }) + .matchHeader('Authorization', `Bearer ${MOCK_TOKEN}`) + .matchHeader('Content-Type', 'application/json') + .reply(201, { + profileId: 'p1', + address: '0xabc', + status: 'created', + }); + const { rootMessenger } = createService(); + + const result = await rootMessenger.call( + 'ChompApiService:associateAddress', + { + signature: '0x123', + timestamp: '2026-01-01T00:00:00Z', + address: '0xabc', + }, + ); - it('accepts a custom fetch implementation', () => { - const customFetch = jest.fn(); - const { service } = createService({ - options: { fetchFn: customFetch as unknown as typeof globalThis.fetch }, + expect(result).toStrictEqual({ + profileId: 'p1', + address: '0xabc', + status: 'created', }); - expect(service).toBeInstanceOf(ChompApiService); }); - }); - describe('associateAddress', () => { - // TODO: Test POST /v1/auth/address with correct URL, method, headers - // (Authorization: Bearer ), and JSON body { signature, timestamp, address }. - // TODO: Test that 200 returns { profileId, address, status }. - // TODO: Test that 409 returns the response body (not throws). - // TODO: Test that other non-OK statuses throw. - // TODO: Test response validation rejects malformed responses. - it('is registered as a messenger action', async () => { - const { service } = createService(); - await expect(service.associateAddress({ + it('returns the response on 409 without throwing', async () => { + nock(BASE_URL).post('/v1/auth/address').reply(409, { + profileId: 'p1', + address: '0xabc', + status: 'already_associated', + }); + const { service } = createService(); + + const result = await service.associateAddress({ signature: '0x123', timestamp: '2026-01-01T00:00:00Z', address: '0xabc', - })).rejects.toThrow('Not implemented'); + }); + + expect(result).toStrictEqual({ + profileId: 'p1', + address: '0xabc', + status: 'already_associated', + }); + }); + + it('throws on non-201/409 status', async () => { + nock(BASE_URL) + .post('/v1/auth/address') + .times(DEFAULT_MAX_RETRIES + 1) + .reply(500); + const { service } = createService(); + + await expect( + service.associateAddress({ + signature: '0x123', + timestamp: '2026-01-01T00:00:00Z', + address: '0xabc', + }), + ).rejects.toThrow("POST /v1/auth/address failed with status '500'"); + }); + + it('throws on malformed response', async () => { + nock(BASE_URL) + .post('/v1/auth/address') + .reply(201, JSON.stringify({ missing: 'fields' })); + const { service } = createService(); + + await expect( + service.associateAddress({ + signature: '0x123', + timestamp: '2026-01-01T00:00:00Z', + address: '0xabc', + }), + ).rejects.toThrow('At path: profileId -- Expected a string'); }); }); describe('createUpgrade', () => { - // TODO: Test POST /v1/account-upgrade with correct URL, method, headers - // (Authorization: Bearer ), and JSON body { r, s, v, yParity, address, chainId, nonce }. - // TODO: Test that 200 returns { signerAddress, status, createdAt }. - // TODO: Test that non-OK statuses throw. - // TODO: Test response validation rejects malformed responses. - it('is registered as a messenger action', async () => { - const { service } = createService(); - await expect(service.createUpgrade({ - r: '0x1', - s: '0x2', - v: 27, - yParity: 0, - address: '0xabc', - chainId: '1', - nonce: '0', - })).rejects.toThrow('Not implemented'); + const upgradeRequest = { + r: '0x1', + s: '0x2', + v: 27, + yParity: 0, + address: '0xabc', + chainId: '1', + nonce: '0', + }; + + const upgradeResponse = { + signerAddress: '0xdef', + status: 'pending', + createdAt: '2026-01-01T00:00:00Z', + }; + + it('sends a POST with auth headers and returns the response', async () => { + nock(BASE_URL) + .post('/v1/account-upgrade', upgradeRequest) + .matchHeader('Authorization', `Bearer ${MOCK_TOKEN}`) + .reply(200, upgradeResponse); + const { rootMessenger } = createService(); + + const result = await rootMessenger.call( + 'ChompApiService:createUpgrade', + upgradeRequest, + ); + + expect(result).toStrictEqual(upgradeResponse); + }); + + it('throws on non-OK status', async () => { + nock(BASE_URL) + .post('/v1/account-upgrade') + .times(DEFAULT_MAX_RETRIES + 1) + .reply(500); + const { service } = createService(); + + await expect(service.createUpgrade(upgradeRequest)).rejects.toThrow( + "POST /v1/account-upgrade failed with status '500'", + ); + }); + + it('throws on malformed response', async () => { + nock(BASE_URL) + .post('/v1/account-upgrade') + .reply(200, JSON.stringify({ bad: 'data' })); + const { service } = createService(); + + await expect(service.createUpgrade(upgradeRequest)).rejects.toThrow( + 'At path: signerAddress -- Expected a string', + ); }); }); describe('getUpgrade', () => { - // TODO: Test GET /v1/account-upgrade/:address with correct URL, method, - // and headers (Authorization: Bearer ). - // TODO: Test that 200 returns the upgrade record. - // TODO: Test that 404 returns null. - // TODO: Test that other non-OK statuses throw. - // TODO: Test response validation rejects malformed responses. - it('is registered as a messenger action', async () => { + const upgradeRecord = { + signerAddress: '0xdef', + status: 'pending', + createdAt: '2026-01-01T00:00:00Z', + }; + + it('sends a GET with auth headers and returns the upgrade record', async () => { + nock(BASE_URL) + .get('/v1/account-upgrade/0xabc') + .matchHeader('Authorization', `Bearer ${MOCK_TOKEN}`) + .reply(200, upgradeRecord); + const { rootMessenger } = createService(); + + const result = await rootMessenger.call( + 'ChompApiService:getUpgrade', + '0xabc', + ); + + expect(result).toStrictEqual(upgradeRecord); + }); + + it('returns null on 404', async () => { + nock(BASE_URL).get('/v1/account-upgrade/0xabc').reply(404); const { service } = createService(); + + const result = await service.getUpgrade('0xabc'); + + expect(result).toBeNull(); + }); + + it('throws on non-OK/non-404 status', async () => { + nock(BASE_URL) + .get('/v1/account-upgrade/0xabc') + .times(DEFAULT_MAX_RETRIES + 1) + .reply(500); + const { service } = createService(); + await expect(service.getUpgrade('0xabc')).rejects.toThrow( - 'Not implemented', + "Get upgrade request failed with status '500'", + ); + }); + + it('throws on malformed response', async () => { + nock(BASE_URL) + .get('/v1/account-upgrade/0xabc') + .reply(200, JSON.stringify({ bad: 'data' })); + const { service } = createService(); + + await expect(service.getUpgrade('0xabc')).rejects.toThrow( + 'At path: signerAddress -- Expected a string', ); }); }); describe('verifyDelegation', () => { - // TODO: Test POST /v1/intent/verify-delegation with correct URL, method, - // headers (Authorization: Bearer ), and JSON body { signedDelegation, chainId }. - // TODO: Test that 200 returns { valid, delegationHash?, errors? }. - // TODO: Test that non-OK statuses throw. - // TODO: Test response validation rejects malformed responses. - it('is registered as a messenger action', async () => { - const { service } = createService(); - await expect(service.verifyDelegation({ - signedDelegation: '0x123', - chainId: '1', - })).rejects.toThrow('Not implemented'); + const delegationRequest = { + signedDelegation: { + delegate: '0x1' as const, + delegator: '0x2' as const, + authority: '0x3' as const, + caveats: [], + salt: '0x4' as const, + signature: '0x5' as const, + }, + chainId: '0x1' as const, + }; + + it('sends a POST with auth headers and returns the response', async () => { + nock(BASE_URL) + .post('/v1/intent/verify-delegation', delegationRequest) + .matchHeader('Authorization', `Bearer ${MOCK_TOKEN}`) + .reply(200, { valid: true, delegationHash: '0xhash123' }); + const { rootMessenger } = createService(); + + const result = await rootMessenger.call( + 'ChompApiService:verifyDelegation', + delegationRequest, + ); + + expect(result).toStrictEqual({ + valid: true, + delegationHash: '0xhash123', + }); + }); + + it('returns errors when delegation is invalid', async () => { + nock(BASE_URL) + .post('/v1/intent/verify-delegation') + .reply(200, { valid: false, errors: ['bad signature'] }); + const { service } = createService(); + + const result = await service.verifyDelegation(delegationRequest); + + expect(result).toStrictEqual({ + valid: false, + errors: ['bad signature'], + }); + }); + + it('throws on non-OK status', async () => { + nock(BASE_URL) + .post('/v1/intent/verify-delegation') + .times(DEFAULT_MAX_RETRIES + 1) + .reply(400); + const { service } = createService(); + + await expect(service.verifyDelegation(delegationRequest)).rejects.toThrow( + "POST /v1/intent/verify-delegation failed with status '400'", + ); + }); + + it('throws on malformed response', async () => { + nock(BASE_URL) + .post('/v1/intent/verify-delegation') + .reply(200, JSON.stringify({ bad: 'data' })); + const { service } = createService(); + + await expect(service.verifyDelegation(delegationRequest)).rejects.toThrow( + 'At path: valid -- Expected a value of type `boolean`', + ); }); }); describe('createIntents', () => { - // TODO: Test POST /v1/intent with correct URL, method, headers - // (Authorization: Bearer ), and JSON body (array of intents). - // TODO: Test that 200 returns SendIntentResponse[]. - // TODO: Test that non-OK statuses throw. - // TODO: Test response validation rejects malformed responses. - it('is registered as a messenger action', async () => { + const intentRequest = [ + { + account: '0xabc' as const, + delegationHash: '0xdef' as const, + chainId: '0x1' as const, + metadata: { + allowance: '0xff' as const, + tokenSymbol: 'USDC', + tokenAddress: '0x123' as const, + type: 'cash-deposit' as const, + }, + }, + ]; + + const intentResponse = [ + { + delegationHash: '0xdef', + metadata: { + allowance: '0xff', + tokenSymbol: 'USDC', + tokenAddress: '0x123', + type: 'cash-deposit', + }, + createdAt: '2026-01-01T00:00:00Z', + }, + ]; + + it('sends a POST with auth headers and returns the response array', async () => { + nock(BASE_URL) + .post('/v1/intent', intentRequest) + .matchHeader('Authorization', `Bearer ${MOCK_TOKEN}`) + .reply(201, intentResponse); + const { rootMessenger } = createService(); + + const result = await rootMessenger.call( + 'ChompApiService:createIntents', + intentRequest, + ); + + expect(result).toStrictEqual(intentResponse); + }); + + it('throws on non-OK status', async () => { + nock(BASE_URL) + .post('/v1/intent') + .times(DEFAULT_MAX_RETRIES + 1) + .reply(409); const { service } = createService(); - await expect(service.createIntents([{ foo: 'bar' }])).rejects.toThrow( - 'Not implemented', + + await expect(service.createIntents(intentRequest)).rejects.toThrow( + "POST /v1/intent failed with status '409'", + ); + }); + + it('throws on malformed response', async () => { + nock(BASE_URL) + .post('/v1/intent') + .reply(201, JSON.stringify([{ bad: 'data' }])); + const { service } = createService(); + + await expect(service.createIntents(intentRequest)).rejects.toThrow( + 'At path: 0.delegationHash -- Expected a string', ); }); }); describe('getIntentsByAddress', () => { - // TODO: Test GET /v1/intent/account/:address with correct URL, method, - // and headers (Authorization: Bearer ). - // TODO: Test that 200 returns an array of intents. - // TODO: Test that non-OK statuses throw. - // TODO: Test response validation rejects malformed responses. - it('is registered as a messenger action', async () => { + const intentsResponse = [ + { + account: '0xabc', + delegationHash: '0xdef', + chainId: '0x1', + status: 'active', + metadata: { + allowance: '0xff', + tokenAddress: '0x123', + tokenSymbol: 'USDC', + type: 'deposit', + }, + }, + ]; + + it('sends a GET with auth headers and returns the intents array', async () => { + nock(BASE_URL) + .get('/v1/intent/account/0xabc') + .matchHeader('Authorization', `Bearer ${MOCK_TOKEN}`) + .reply(200, intentsResponse); + const { rootMessenger } = createService(); + + const result = await rootMessenger.call( + 'ChompApiService:getIntentsByAddress', + '0xabc', + ); + + expect(result).toStrictEqual(intentsResponse); + }); + + it('returns an empty array when no intents exist', async () => { + nock(BASE_URL).get('/v1/intent/account/0xabc').reply(200, []); + const { service } = createService(); + + const result = await service.getIntentsByAddress('0xabc'); + + expect(result).toStrictEqual([]); + }); + + it('throws on non-OK status', async () => { + nock(BASE_URL) + .get('/v1/intent/account/0xabc') + .times(DEFAULT_MAX_RETRIES + 1) + .reply(500); + const { service } = createService(); + + await expect(service.getIntentsByAddress('0xabc')).rejects.toThrow( + "Get intents request failed with status '500'", + ); + }); + + it('throws on malformed response', async () => { + nock(BASE_URL) + .get('/v1/intent/account/0xabc') + .reply(200, JSON.stringify([{ bad: 'data' }])); const { service } = createService(); + await expect(service.getIntentsByAddress('0xabc')).rejects.toThrow( - 'Not implemented', + 'At path: 0.account -- Expected a string', ); }); }); describe('createWithdrawal', () => { - // TODO: Confirm endpoint path against CHOMP API docs. - // TODO: Test POST to the withdrawal endpoint with correct URL, method, - // headers (Authorization: Bearer ), and JSON body. - // TODO: Test that 200 returns the withdrawal result. - // TODO: Test that non-OK statuses throw. - // TODO: Test response validation rejects malformed responses. - it('is registered as a messenger action', async () => { + const withdrawalRequest = { + chainId: '0x1' as const, + amount: '1000000', + account: '0xabc' as const, + }; + + it('sends a POST with auth headers and returns the response', async () => { + nock(BASE_URL) + .post('/v1/withdrawal', withdrawalRequest) + .matchHeader('Authorization', `Bearer ${MOCK_TOKEN}`) + .reply(200, { success: true }); + const { rootMessenger } = createService(); + + const result = await rootMessenger.call( + 'ChompApiService:createWithdrawal', + withdrawalRequest, + ); + + expect(result).toStrictEqual({ success: true }); + }); + + it('throws on non-OK status', async () => { + nock(BASE_URL) + .post('/v1/withdrawal') + .times(DEFAULT_MAX_RETRIES + 1) + .reply(400); const { service } = createService(); - await expect(service.createWithdrawal({})).rejects.toThrow( - 'Not implemented', + + await expect(service.createWithdrawal(withdrawalRequest)).rejects.toThrow( + "POST /v1/withdrawal failed with status '400'", + ); + }); + + it('throws on malformed response', async () => { + nock(BASE_URL) + .post('/v1/withdrawal') + .reply(200, JSON.stringify({ success: false })); + const { service } = createService(); + + await expect(service.createWithdrawal(withdrawalRequest)).rejects.toThrow( + 'At path: success -- Expected the literal `true`', ); }); }); @@ -196,7 +516,7 @@ function createService({ const messenger = createServiceMessenger(rootMessenger); const service = new ChompApiService({ baseUrl: BASE_URL, - getAccessToken: async () => MOCK_TOKEN, + getAccessToken: async (): Promise => MOCK_TOKEN, messenger, ...options, }); diff --git a/packages/chomp-api-service/src/chomp-api-service.ts b/packages/chomp-api-service/src/chomp-api-service.ts index 25b4d2d7d16..ffe34f79e09 100644 --- a/packages/chomp-api-service/src/chomp-api-service.ts +++ b/packages/chomp-api-service/src/chomp-api-service.ts @@ -4,8 +4,22 @@ import type { DataServiceGranularCacheUpdatedEvent, DataServiceInvalidateQueriesAction, } from '@metamask/base-data-service'; -import type { CreateServicePolicyOptions } from '@metamask/controller-utils'; +import type { + CreateServicePolicyOptions, + ServicePolicy, +} from '@metamask/controller-utils'; +import { createServicePolicy, HttpError } from '@metamask/controller-utils'; import type { Messenger } from '@metamask/messenger'; +import { + array, + boolean, + create, + enums, + literal, + optional, + string, + type, +} from '@metamask/superstruct'; import type { QueryClientConfig } from '@tanstack/query-core'; import type { ChompApiServiceMethodActions } from './chomp-api-service-method-action-types'; @@ -17,6 +31,7 @@ import type { CreateWithdrawalRequest, CreateWithdrawalResponse, GetUpgradeResponse, + IntentEntry, SendIntentRequest, SendIntentResponse, VerifyDelegationRequest, @@ -68,8 +83,9 @@ type AllowedActions = never; /** * Published when {@link ChompApiService}'s cache is updated. */ -export type ChompApiServiceCacheUpdatedEvent = - DataServiceCacheUpdatedEvent; +export type ChompApiServiceCacheUpdatedEvent = DataServiceCacheUpdatedEvent< + typeof serviceName +>; /** * Published when a key within {@link ChompApiService}'s cache is updated. @@ -99,6 +115,58 @@ export type ChompApiServiceMessenger = Messenger< ChompApiServiceEvents | AllowedEvents >; +// === RESPONSE VALIDATION === + +const AssociateAddressResponseStruct = type({ + profileId: string(), + address: string(), + status: string(), +}); + +const UpgradeResponseStruct = type({ + signerAddress: string(), + status: string(), + createdAt: string(), +}); + +const VerifyDelegationResponseStruct = type({ + valid: boolean(), + delegationHash: optional(string()), + errors: optional(array(string())), +}); + +const SendIntentResponseArrayStruct = array( + type({ + delegationHash: string(), + metadata: type({ + allowance: string(), + tokenSymbol: string(), + tokenAddress: string(), + type: enums(['cash-deposit', 'cash-withdrawal']), + }), + createdAt: string(), + }), +); + +const IntentEntryArrayStruct = array( + type({ + account: string(), + delegationHash: string(), + chainId: string(), + status: enums(['active', 'revoked']), + metadata: type({ + allowance: string(), + tokenAddress: string(), + tokenSymbol: string(), + type: enums(['deposit', 'withdraw']), + }), + }), +); + +const CreateWithdrawalResponseStruct = type({ + success: literal(true), +}); + // === SERVICE DEFINITION === /** @@ -115,7 +183,7 @@ export class ChompApiService extends BaseDataService< readonly #getAccessToken: () => Promise; - readonly #fetch: typeof globalThis.fetch; + readonly #mutationPolicy: ServicePolicy; /** * Constructs a new ChompApiService. @@ -125,8 +193,6 @@ export class ChompApiService extends BaseDataService< * @param args.baseUrl - The base URL of the CHOMP API. * @param args.getAccessToken - An async callback that returns a valid JWT * access token for authenticating requests. - * @param args.fetchFn - An optional custom fetch implementation. Defaults to - * the global `fetch`. * @param args.queryClientConfig - Configuration for the underlying TanStack * Query client. * @param args.policyOptions - Options to pass to `createServicePolicy`. @@ -135,14 +201,12 @@ export class ChompApiService extends BaseDataService< messenger, baseUrl, getAccessToken, - fetchFn = globalThis.fetch, queryClientConfig = {}, policyOptions = {}, }: { messenger: ChompApiServiceMessenger; baseUrl: string; getAccessToken: () => Promise; - fetchFn?: typeof globalThis.fetch; queryClientConfig?: QueryClientConfig; policyOptions?: CreateServicePolicyOptions; }) { @@ -155,7 +219,7 @@ export class ChompApiService extends BaseDataService< this.#baseUrl = baseUrl; this.#getAccessToken = getAccessToken; - this.#fetch = fetchFn; + this.#mutationPolicy = createServicePolicy(policyOptions); this.messenger.registerMethodActionHandlers( this, @@ -176,29 +240,54 @@ export class ChompApiService extends BaseDataService< }; } + /** + * Makes an authenticated POST request to the CHOMP API. + * + * @param path - The URL path relative to the base URL. + * @param body - The request body to serialize as JSON. + * @param acceptedStatuses - HTTP status codes that should be returned rather + * than treated as errors (e.g. 409 for conflict). + * @returns The raw fetch Response. + */ + async #postJson( + path: string, + body: unknown, + acceptedStatuses: number[] = [], + ): Promise { + const headers = await this.#authHeaders(); + return this.#mutationPolicy.execute(async () => { + const response = await fetch(new URL(path, this.#baseUrl), { + method: 'POST', + headers, + body: JSON.stringify(body), + }); + + if (!response.ok && !acceptedStatuses.includes(response.status)) { + throw new HttpError( + response.status, + `POST ${path} failed with status '${response.status}'`, + ); + } + + return response; + }); + } + /** * Associates an address with a CHOMP profile. * * POST /v1/auth/address * - * TODO: Implement the request using this.fetchQuery or direct POST via - * this.#fetch. Validate the response with a superstruct. Note that a 409 - * response is valid and should be returned (not thrown). - * * @param request - The association request containing signature, timestamp, * and address. - * @returns The profile association result. + * @returns The profile association result. Returns on both 201 and 409. */ async associateAddress( request: AssociateAddressRequest, ): Promise { - // TODO: POST to `${this.#baseUrl}/v1/auth/address` with JSON body. - // TODO: Include Authorization header via this.#authHeaders(). - // TODO: Return response body on 200 or 409; throw on other non-OK statuses. - // TODO: Validate response shape with superstruct before returning. - const _headers = await this.#authHeaders(); - void request; - throw new Error('Not implemented'); + const response = await this.#postJson('/v1/auth/address', request, [409]); + const json = await response.json(); + return create(json, AssociateAddressResponseStruct); } /** @@ -206,8 +295,6 @@ export class ChompApiService extends BaseDataService< * * POST /v1/account-upgrade * - * TODO: Implement the POST request. Validate the response with a superstruct. - * * @param request - The upgrade request containing signature components and * chain details. * @returns The upgrade result. @@ -215,13 +302,9 @@ export class ChompApiService extends BaseDataService< async createUpgrade( request: CreateUpgradeRequest, ): Promise { - // TODO: POST to `${this.#baseUrl}/v1/account-upgrade` with JSON body. - // TODO: Include Authorization header via this.#authHeaders(). - // TODO: Throw on non-OK responses. - // TODO: Validate response shape with superstruct before returning. - const _headers = await this.#authHeaders(); - void request; - throw new Error('Not implemented'); + const response = await this.#postJson('/v1/account-upgrade', request); + const json = await response.json(); + return create(json, UpgradeResponseStruct); } /** @@ -229,21 +312,39 @@ export class ChompApiService extends BaseDataService< * * GET /v1/account-upgrade/:address * - * TODO: Implement the GET request. Return null on 404. Validate the response - * with a superstruct for non-404 responses. - * * @param address - The address to look up. * @returns The upgrade record, or null if not found. */ async getUpgrade(address: string): Promise { - // TODO: GET `${this.#baseUrl}/v1/account-upgrade/${address}`. - // TODO: Include Authorization header via this.#authHeaders(). - // TODO: Return null on 404, throw on other non-OK statuses. - // TODO: Validate response shape with superstruct before returning. - // TODO: Consider using this.fetchQuery with a queryKey for caching. - const _headers = await this.#authHeaders(); - void address; - throw new Error('Not implemented'); + const jsonResponse = await this.fetchQuery({ + queryKey: [`${this.name}:getUpgrade`, address], + queryFn: async () => { + const headers = await this.#authHeaders(); + const response = await fetch( + new URL(`/v1/account-upgrade/${address}`, this.#baseUrl), + { headers }, + ); + + if (response.status === 404) { + return null; + } + + if (!response.ok) { + throw new HttpError( + response.status, + `Get upgrade request failed with status '${response.status}'`, + ); + } + + return response.json(); + }, + }); + + if (jsonResponse === null) { + return null; + } + + return create(jsonResponse, UpgradeResponseStruct); } /** @@ -251,21 +352,18 @@ export class ChompApiService extends BaseDataService< * * POST /v1/intent/verify-delegation * - * TODO: Implement the POST request. Validate the response with a superstruct. - * * @param request - The delegation verification request. * @returns The verification result including validity and optional errors. */ async verifyDelegation( request: VerifyDelegationRequest, ): Promise { - // TODO: POST to `${this.#baseUrl}/v1/intent/verify-delegation` with JSON body. - // TODO: Include Authorization header via this.#authHeaders(). - // TODO: Throw on non-OK responses. - // TODO: Validate response shape with superstruct before returning. - const _headers = await this.#authHeaders(); - void request; - throw new Error('Not implemented'); + const response = await this.#postJson( + '/v1/intent/verify-delegation', + request, + ); + const json = await response.json(); + return create(json, VerifyDelegationResponseStruct); } /** @@ -273,22 +371,15 @@ export class ChompApiService extends BaseDataService< * * POST /v1/intent * - * TODO: Implement the POST request. Validate the response array with a - * superstruct. - * * @param intents - The array of intents to submit. * @returns The array of intent responses. */ async createIntents( intents: SendIntentRequest[], ): Promise { - // TODO: POST to `${this.#baseUrl}/v1/intent` with JSON body. - // TODO: Include Authorization header via this.#authHeaders(). - // TODO: Throw on non-OK responses. - // TODO: Validate response shape with superstruct before returning. - const _headers = await this.#authHeaders(); - void intents; - throw new Error('Not implemented'); + const response = await this.#postJson('/v1/intent', intents); + const json = await response.json(); + return create(json, SendIntentResponseArrayStruct) as SendIntentResponse[]; } /** @@ -296,43 +387,47 @@ export class ChompApiService extends BaseDataService< * * GET /v1/intent/account/:address * - * TODO: Implement the GET request. Validate the response array with a - * superstruct. - * * @param address - The address to look up intents for. * @returns The array of intents for the address. */ - async getIntentsByAddress( - address: string, - ): Promise { - // TODO: GET `${this.#baseUrl}/v1/intent/account/${address}`. - // TODO: Include Authorization header via this.#authHeaders(). - // TODO: Throw on non-OK responses. - // TODO: Validate response shape with superstruct before returning. - // TODO: Consider using this.fetchQuery with a queryKey for caching. - const _headers = await this.#authHeaders(); - void address; - throw new Error('Not implemented'); + async getIntentsByAddress(address: string): Promise { + const jsonResponse = await this.fetchQuery({ + queryKey: [`${this.name}:getIntentsByAddress`, address], + queryFn: async () => { + const headers = await this.#authHeaders(); + const response = await fetch( + new URL(`/v1/intent/account/${address}`, this.#baseUrl), + { headers }, + ); + + if (!response.ok) { + throw new HttpError( + response.status, + `Get intents request failed with status '${response.status}'`, + ); + } + + return response.json(); + }, + }); + + return create(jsonResponse, IntentEntryArrayStruct) as IntentEntry[]; } /** * Creates a withdrawal for card spend flows. * - * TODO: Confirm the endpoint path against CHOMP API docs. - * TODO: Implement the POST request. Validate the response with a superstruct. + * POST /v1/withdrawal * - * @param request - The withdrawal request. + * @param request - The withdrawal request containing chainId, amount + * (decimal or hex string), and account address. * @returns The withdrawal result. */ async createWithdrawal( request: CreateWithdrawalRequest, ): Promise { - // TODO: Confirm endpoint path (e.g. POST `${this.#baseUrl}/v1/withdrawal`). - // TODO: Include Authorization header via this.#authHeaders(). - // TODO: Throw on non-OK responses. - // TODO: Validate response shape with superstruct before returning. - const _headers = await this.#authHeaders(); - void request; - throw new Error('Not implemented'); + const response = await this.#postJson('/v1/withdrawal', request); + const json = await response.json(); + return create(json, CreateWithdrawalResponseStruct); } } diff --git a/packages/chomp-api-service/src/index.ts b/packages/chomp-api-service/src/index.ts index bcc77031af5..f38958ca3cd 100644 --- a/packages/chomp-api-service/src/index.ts +++ b/packages/chomp-api-service/src/index.ts @@ -8,7 +8,6 @@ export type { ChompApiServiceGranularCacheUpdatedEvent, } from './chomp-api-service'; export type { - ChompApiServiceMethodActions, ChompApiServiceAssociateAddressAction, ChompApiServiceCreateUpgradeAction, ChompApiServiceGetUpgradeAction, @@ -22,11 +21,16 @@ export type { AssociateAddressResponse, CreateUpgradeRequest, CreateUpgradeResponse, + CreateWithdrawalRequest, + CreateWithdrawalResponse, + DelegationCaveat, GetUpgradeResponse, - VerifyDelegationRequest, - VerifyDelegationResponse, + IntentEntry, + IntentMetadataRequest, + IntentMetadataResponse, SendIntentRequest, SendIntentResponse, - CreateWithdrawalRequest, - CreateWithdrawalResponse, + SignedDelegation, + VerifyDelegationRequest, + VerifyDelegationResponse, } from './types'; diff --git a/packages/chomp-api-service/src/types.ts b/packages/chomp-api-service/src/types.ts index b3a253cce55..382e7bc5280 100644 --- a/packages/chomp-api-service/src/types.ts +++ b/packages/chomp-api-service/src/types.ts @@ -1,3 +1,22 @@ +import type { Hex } from '@metamask/utils'; + +// === COMMON TYPES === + +export type DelegationCaveat = { + enforcer: Hex; + terms: Hex; + args: Hex; +}; + +export type SignedDelegation = { + delegate: Hex; + delegator: Hex; + authority: Hex; + caveats: DelegationCaveat[]; + salt: Hex; + signature: Hex; +}; + // === REQUEST TYPES === export type AssociateAddressRequest = { @@ -17,21 +36,30 @@ export type CreateUpgradeRequest = { }; export type VerifyDelegationRequest = { - signedDelegation: string; - chainId: string; + signedDelegation: SignedDelegation; + chainId: Hex; }; -/** - * A single intent to be submitted to the Chomp API. - * TODO: Define the full shape of an intent once the API schema is confirmed. - */ -export type SendIntentRequest = Record; +export type IntentMetadataRequest = { + allowance: Hex; + tokenSymbol: string; + tokenAddress: Hex; + type: 'cash-deposit' | 'cash-withdrawal'; +}; -/** - * TODO: Define request shape once the withdrawal endpoint path and schema are - * confirmed against CHOMP API docs. - */ -export type CreateWithdrawalRequest = Record; +export type SendIntentRequest = { + account: Hex; + delegationHash: Hex; + chainId: Hex; + metadata: IntentMetadataRequest; +}; + +export type CreateWithdrawalRequest = { + chainId: Hex; + /** Decimal integer or 0x-prefixed hex string representing the amount. */ + amount: string; + account: Hex; +}; // === RESPONSE TYPES === @@ -49,7 +77,6 @@ export type CreateUpgradeResponse = { /** * The upgrade record returned by GET /v1/account-upgrade/:address. - * TODO: Confirm full shape against CHOMP API docs. */ export type GetUpgradeResponse = { signerAddress: string; @@ -63,14 +90,38 @@ export type VerifyDelegationResponse = { errors?: string[]; }; -/** - * A single intent response. - * TODO: Define the full shape once the API schema is confirmed. - */ -export type SendIntentResponse = Record; +export type IntentMetadataResponse = { + allowance: Hex; + tokenSymbol: string; + tokenAddress: Hex; + type: 'cash-deposit' | 'cash-withdrawal'; +}; + +export type SendIntentResponse = { + delegationHash: string; + metadata: IntentMetadataResponse; + createdAt: string; +}; /** - * TODO: Define response shape once the withdrawal endpoint path and schema are - * confirmed against CHOMP API docs. + * The shape returned by GET /v1/intent/account/:address for each intent. + * + * Note: the metadata `type` uses 'deposit' | 'withdraw' here, whereas the + * create-intent endpoint uses 'cash-deposit' | 'cash-withdrawal'. */ -export type CreateWithdrawalResponse = Record; +export type IntentEntry = { + account: Hex; + delegationHash: Hex; + chainId: Hex; + status: 'active' | 'revoked'; + metadata: { + allowance: Hex; + tokenAddress: Hex; + tokenSymbol: string; + type: 'deposit' | 'withdraw'; + }; +}; + +export type CreateWithdrawalResponse = { + success: true; +}; From 1cbab67ec1e2f448407488cb2b55fcd360de9688 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Mon, 13 Apr 2026 13:52:12 +0100 Subject: [PATCH 4/8] chore: fix spelling of chomp --- packages/chomp-api-service/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/chomp-api-service/README.md b/packages/chomp-api-service/README.md index a12245d8d5f..d1b2943f487 100644 --- a/packages/chomp-api-service/README.md +++ b/packages/chomp-api-service/README.md @@ -1,14 +1,14 @@ -# `@metamask/chom-api-service` +# `@metamask/chomp-api-service` Chomp API data service. ## Installation -`yarn add @metamask/chom-api-service` +`yarn add @metamask/chomp-api-service` or -`npm install @metamask/chom-api-service` +`npm install @metamask/chomp-api-service` ## Contributing From 31834cc9a650c552ed5b0757bdefae22eeea7bd3 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Mon, 13 Apr 2026 13:53:36 +0100 Subject: [PATCH 5/8] feat: use the fetchQuery for non cached calls --- .../src/chomp-api-service.ts | 188 ++++++++++++------ yarn.lock | 32 ++- 2 files changed, 157 insertions(+), 63 deletions(-) diff --git a/packages/chomp-api-service/src/chomp-api-service.ts b/packages/chomp-api-service/src/chomp-api-service.ts index ffe34f79e09..82c5de4bcb3 100644 --- a/packages/chomp-api-service/src/chomp-api-service.ts +++ b/packages/chomp-api-service/src/chomp-api-service.ts @@ -4,11 +4,8 @@ import type { DataServiceGranularCacheUpdatedEvent, DataServiceInvalidateQueriesAction, } from '@metamask/base-data-service'; -import type { - CreateServicePolicyOptions, - ServicePolicy, -} from '@metamask/controller-utils'; -import { createServicePolicy, HttpError } from '@metamask/controller-utils'; +import type { CreateServicePolicyOptions } from '@metamask/controller-utils'; +import { HttpError } from '@metamask/controller-utils'; import type { Messenger } from '@metamask/messenger'; import { array, @@ -183,8 +180,6 @@ export class ChompApiService extends BaseDataService< readonly #getAccessToken: () => Promise; - readonly #mutationPolicy: ServicePolicy; - /** * Constructs a new ChompApiService. * @@ -219,7 +214,6 @@ export class ChompApiService extends BaseDataService< this.#baseUrl = baseUrl; this.#getAccessToken = getAccessToken; - this.#mutationPolicy = createServicePolicy(policyOptions); this.messenger.registerMethodActionHandlers( this, @@ -240,39 +234,6 @@ export class ChompApiService extends BaseDataService< }; } - /** - * Makes an authenticated POST request to the CHOMP API. - * - * @param path - The URL path relative to the base URL. - * @param body - The request body to serialize as JSON. - * @param acceptedStatuses - HTTP status codes that should be returned rather - * than treated as errors (e.g. 409 for conflict). - * @returns The raw fetch Response. - */ - async #postJson( - path: string, - body: unknown, - acceptedStatuses: number[] = [], - ): Promise { - const headers = await this.#authHeaders(); - return this.#mutationPolicy.execute(async () => { - const response = await fetch(new URL(path, this.#baseUrl), { - method: 'POST', - headers, - body: JSON.stringify(body), - }); - - if (!response.ok && !acceptedStatuses.includes(response.status)) { - throw new HttpError( - response.status, - `POST ${path} failed with status '${response.status}'`, - ); - } - - return response; - }); - } - /** * Associates an address with a CHOMP profile. * @@ -285,9 +246,32 @@ export class ChompApiService extends BaseDataService< async associateAddress( request: AssociateAddressRequest, ): Promise { - const response = await this.#postJson('/v1/auth/address', request, [409]); - const json = await response.json(); - return create(json, AssociateAddressResponseStruct); + const jsonResponse = await this.fetchQuery({ + queryKey: [`${this.name}:associateAddress`, request], + staleTime: 0, + queryFn: async () => { + const headers = await this.#authHeaders(); + const response = await fetch( + new URL('/v1/auth/address', this.#baseUrl), + { + method: 'POST', + headers, + body: JSON.stringify(request), + }, + ); + + if (!response.ok && response.status !== 409) { + throw new HttpError( + response.status, + `POST /v1/auth/address failed with status '${response.status}'`, + ); + } + + return response.json(); + }, + }); + + return create(jsonResponse, AssociateAddressResponseStruct); } /** @@ -302,9 +286,32 @@ export class ChompApiService extends BaseDataService< async createUpgrade( request: CreateUpgradeRequest, ): Promise { - const response = await this.#postJson('/v1/account-upgrade', request); - const json = await response.json(); - return create(json, UpgradeResponseStruct); + const jsonResponse = await this.fetchQuery({ + queryKey: [`${this.name}:createUpgrade`, request], + staleTime: 0, + queryFn: async () => { + const headers = await this.#authHeaders(); + const response = await fetch( + new URL('/v1/account-upgrade', this.#baseUrl), + { + method: 'POST', + headers, + body: JSON.stringify(request), + }, + ); + + if (!response.ok) { + throw new HttpError( + response.status, + `POST /v1/account-upgrade failed with status '${response.status}'`, + ); + } + + return response.json(); + }, + }); + + return create(jsonResponse, UpgradeResponseStruct); } /** @@ -358,12 +365,32 @@ export class ChompApiService extends BaseDataService< async verifyDelegation( request: VerifyDelegationRequest, ): Promise { - const response = await this.#postJson( - '/v1/intent/verify-delegation', - request, - ); - const json = await response.json(); - return create(json, VerifyDelegationResponseStruct); + const jsonResponse = await this.fetchQuery({ + queryKey: [`${this.name}:verifyDelegation`, request], + staleTime: 0, + queryFn: async () => { + const headers = await this.#authHeaders(); + const response = await fetch( + new URL('/v1/intent/verify-delegation', this.#baseUrl), + { + method: 'POST', + headers, + body: JSON.stringify(request), + }, + ); + + if (!response.ok) { + throw new HttpError( + response.status, + `POST /v1/intent/verify-delegation failed with status '${response.status}'`, + ); + } + + return response.json(); + }, + }); + + return create(jsonResponse, VerifyDelegationResponseStruct); } /** @@ -377,9 +404,32 @@ export class ChompApiService extends BaseDataService< async createIntents( intents: SendIntentRequest[], ): Promise { - const response = await this.#postJson('/v1/intent', intents); - const json = await response.json(); - return create(json, SendIntentResponseArrayStruct) as SendIntentResponse[]; + const jsonResponse = await this.fetchQuery({ + queryKey: [`${this.name}:createIntents`, intents], + staleTime: 0, + queryFn: async () => { + const headers = await this.#authHeaders(); + const response = await fetch(new URL('/v1/intent', this.#baseUrl), { + method: 'POST', + headers, + body: JSON.stringify(intents), + }); + + if (!response.ok) { + throw new HttpError( + response.status, + `POST /v1/intent failed with status '${response.status}'`, + ); + } + + return response.json(); + }, + }); + + return create( + jsonResponse, + SendIntentResponseArrayStruct, + ) as SendIntentResponse[]; } /** @@ -426,8 +476,28 @@ export class ChompApiService extends BaseDataService< async createWithdrawal( request: CreateWithdrawalRequest, ): Promise { - const response = await this.#postJson('/v1/withdrawal', request); - const json = await response.json(); - return create(json, CreateWithdrawalResponseStruct); + const jsonResponse = await this.fetchQuery({ + queryKey: [`${this.name}:createWithdrawal`, request], + staleTime: 0, + queryFn: async () => { + const headers = await this.#authHeaders(); + const response = await fetch(new URL('/v1/withdrawal', this.#baseUrl), { + method: 'POST', + headers, + body: JSON.stringify(request), + }); + + if (!response.ok) { + throw new HttpError( + response.status, + `POST /v1/withdrawal failed with status '${response.status}'`, + ); + } + + return response.json(); + }, + }); + + return create(jsonResponse, CreateWithdrawalResponseStruct); } } diff --git a/yarn.lock b/yarn.lock index 7c960fbabc4..286ba00f21a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3126,7 +3126,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/chomp-api-service@workspace:packages/chomp-api-service": +"@metamask/chomp-api-service@npm:^0.0.0, @metamask/chomp-api-service@workspace:packages/chomp-api-service": version: 0.0.0-use.local resolution: "@metamask/chomp-api-service@workspace:packages/chomp-api-service" dependencies: @@ -3431,7 +3431,7 @@ __metadata: languageName: node linkType: hard -"@metamask/delegation-controller@workspace:packages/delegation-controller": +"@metamask/delegation-controller@npm:^2.1.0, @metamask/delegation-controller@workspace:packages/delegation-controller": version: 0.0.0-use.local resolution: "@metamask/delegation-controller@workspace:packages/delegation-controller" dependencies: @@ -4426,7 +4426,7 @@ __metadata: languageName: node linkType: hard -"@metamask/money-account-controller@workspace:packages/money-account-controller": +"@metamask/money-account-controller@npm:^0.1.0, @metamask/money-account-controller@workspace:packages/money-account-controller": version: 0.0.0-use.local resolution: "@metamask/money-account-controller@workspace:packages/money-account-controller" dependencies: @@ -4452,6 +4452,30 @@ __metadata: languageName: unknown linkType: soft +"@metamask/money-account-upgrade-controller@workspace:packages/money-account-upgrade-controller": + version: 0.0.0-use.local + resolution: "@metamask/money-account-upgrade-controller@workspace:packages/money-account-upgrade-controller" + dependencies: + "@metamask/auto-changelog": "npm:^3.4.4" + "@metamask/base-controller": "npm:^9.0.1" + "@metamask/chomp-api-service": "npm:^0.0.0" + "@metamask/delegation-controller": "npm:^2.1.0" + "@metamask/keyring-controller": "npm:^25.2.0" + "@metamask/messenger": "npm:^1.1.1" + "@metamask/money-account-controller": "npm:^0.1.0" + "@metamask/utils": "npm:^11.9.0" + "@ts-bridge/cli": "npm:^0.6.4" + "@types/jest": "npm:^29.5.14" + deepmerge: "npm:^4.2.2" + jest: "npm:^29.2.5" + ts-jest: "npm:^29.2.5" + tsx: "npm:^4.20.5" + typedoc: "npm:^0.25.13" + typedoc-plugin-missing-exports: "npm:^2.0.0" + typescript: "npm:~5.3.3" + languageName: unknown + linkType: soft + "@metamask/multichain-account-service@npm:^8.0.1, @metamask/multichain-account-service@workspace:packages/multichain-account-service": version: 0.0.0-use.local resolution: "@metamask/multichain-account-service@workspace:packages/multichain-account-service" @@ -11293,7 +11317,7 @@ __metadata: languageName: node linkType: hard -"jest@npm:^29.7.0": +"jest@npm:^29.2.5, jest@npm:^29.7.0": version: 29.7.0 resolution: "jest@npm:29.7.0" dependencies: From e181d0a49cd855c2e5db50cb856e254ef2e6a79f Mon Sep 17 00:00:00 2001 From: John Whiles Date: Mon, 13 Apr 2026 13:53:52 +0100 Subject: [PATCH 6/8] fix: small error in changelog --- packages/chomp-api-service/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/chomp-api-service/CHANGELOG.md b/packages/chomp-api-service/CHANGELOG.md index c050ac9419d..77f2d8816a9 100644 --- a/packages/chomp-api-service/CHANGELOG.md +++ b/packages/chomp-api-service/CHANGELOG.md @@ -9,6 +9,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add `ChompApiService` ([#8361](https://github.com/MetaMask/core/pull/8413)) +- Add `ChompApiService` ([#8413](https://github.com/MetaMask/core/pull/8413)) [Unreleased]: https://github.com/MetaMask/core/ From edf7c9f4636fc76ffbbbb48eebda4799f38671ff Mon Sep 17 00:00:00 2001 From: John Whiles Date: Mon, 13 Apr 2026 14:05:41 +0100 Subject: [PATCH 7/8] fix: update tsconfig files --- tsconfig.build.json | 3 +++ tsconfig.json | 3 +++ 2 files changed, 6 insertions(+) diff --git a/tsconfig.build.json b/tsconfig.build.json index b02f3a882f5..d4c697105b7 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -58,6 +58,9 @@ { "path": "./packages/chain-agnostic-permission/tsconfig.build.json" }, + { + "path": "./packages/chomp-api-service/tsconfig.build.json" + }, { "path": "./packages/claims-controller/tsconfig.build.json" }, diff --git a/tsconfig.json b/tsconfig.json index 7eb3aadc457..94314f83d9e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -59,6 +59,9 @@ { "path": "./packages/chain-agnostic-permission" }, + { + "path": "./packages/chomp-api-service" + }, { "path": "./packages/claims-controller" }, From d097212ad566482904a84526ff4176f05c1a914c Mon Sep 17 00:00:00 2001 From: John Whiles Date: Mon, 13 Apr 2026 14:09:10 +0100 Subject: [PATCH 8/8] feat: validate hex strings and share upgrade reponse type --- .../src/chomp-api-service.test.ts | 2 +- .../src/chomp-api-service.ts | 29 ++++++++++--------- packages/chomp-api-service/src/index.ts | 3 +- packages/chomp-api-service/src/types.ts | 11 +------ 4 files changed, 19 insertions(+), 26 deletions(-) diff --git a/packages/chomp-api-service/src/chomp-api-service.test.ts b/packages/chomp-api-service/src/chomp-api-service.test.ts index 0c79cc63076..03a101d55bb 100644 --- a/packages/chomp-api-service/src/chomp-api-service.test.ts +++ b/packages/chomp-api-service/src/chomp-api-service.test.ts @@ -406,7 +406,7 @@ describe('ChompApiService', () => { const { service } = createService(); await expect(service.getIntentsByAddress('0xabc')).rejects.toThrow( - 'At path: 0.account -- Expected a string', + 'At path: 0.account -- Expected a value of type `Hex string`', ); }); }); diff --git a/packages/chomp-api-service/src/chomp-api-service.ts b/packages/chomp-api-service/src/chomp-api-service.ts index 82c5de4bcb3..813833cb7af 100644 --- a/packages/chomp-api-service/src/chomp-api-service.ts +++ b/packages/chomp-api-service/src/chomp-api-service.ts @@ -11,12 +11,14 @@ import { array, boolean, create, + define, enums, literal, optional, string, type, } from '@metamask/superstruct'; +import { isStrictHexString } from '@metamask/utils'; import type { QueryClientConfig } from '@tanstack/query-core'; import type { ChompApiServiceMethodActions } from './chomp-api-service-method-action-types'; @@ -24,10 +26,9 @@ import type { AssociateAddressRequest, AssociateAddressResponse, CreateUpgradeRequest, - CreateUpgradeResponse, + UpgradeResponse, CreateWithdrawalRequest, CreateWithdrawalResponse, - GetUpgradeResponse, IntentEntry, SendIntentRequest, SendIntentResponse, @@ -114,6 +115,10 @@ export type ChompApiServiceMessenger = Messenger< // === RESPONSE VALIDATION === +const HexStringStruct = define('Hex string', (value) => + isStrictHexString(value), +); + const AssociateAddressResponseStruct = type({ profileId: string(), address: string(), @@ -136,9 +141,9 @@ const SendIntentResponseArrayStruct = array( type({ delegationHash: string(), metadata: type({ - allowance: string(), + allowance: HexStringStruct, tokenSymbol: string(), - tokenAddress: string(), + tokenAddress: HexStringStruct, type: enums(['cash-deposit', 'cash-withdrawal']), }), createdAt: string(), @@ -147,13 +152,13 @@ const SendIntentResponseArrayStruct = array( const IntentEntryArrayStruct = array( type({ - account: string(), - delegationHash: string(), - chainId: string(), + account: HexStringStruct, + delegationHash: HexStringStruct, + chainId: HexStringStruct, status: enums(['active', 'revoked']), metadata: type({ - allowance: string(), - tokenAddress: string(), + allowance: HexStringStruct, + tokenAddress: HexStringStruct, tokenSymbol: string(), type: enums(['deposit', 'withdraw']), }), @@ -283,9 +288,7 @@ export class ChompApiService extends BaseDataService< * chain details. * @returns The upgrade result. */ - async createUpgrade( - request: CreateUpgradeRequest, - ): Promise { + async createUpgrade(request: CreateUpgradeRequest): Promise { const jsonResponse = await this.fetchQuery({ queryKey: [`${this.name}:createUpgrade`, request], staleTime: 0, @@ -322,7 +325,7 @@ export class ChompApiService extends BaseDataService< * @param address - The address to look up. * @returns The upgrade record, or null if not found. */ - async getUpgrade(address: string): Promise { + async getUpgrade(address: string): Promise { const jsonResponse = await this.fetchQuery({ queryKey: [`${this.name}:getUpgrade`, address], queryFn: async () => { diff --git a/packages/chomp-api-service/src/index.ts b/packages/chomp-api-service/src/index.ts index f38958ca3cd..0052e7f8dde 100644 --- a/packages/chomp-api-service/src/index.ts +++ b/packages/chomp-api-service/src/index.ts @@ -20,11 +20,10 @@ export type { AssociateAddressRequest, AssociateAddressResponse, CreateUpgradeRequest, - CreateUpgradeResponse, CreateWithdrawalRequest, CreateWithdrawalResponse, DelegationCaveat, - GetUpgradeResponse, + UpgradeResponse, IntentEntry, IntentMetadataRequest, IntentMetadataResponse, diff --git a/packages/chomp-api-service/src/types.ts b/packages/chomp-api-service/src/types.ts index 382e7bc5280..e68092e22de 100644 --- a/packages/chomp-api-service/src/types.ts +++ b/packages/chomp-api-service/src/types.ts @@ -69,16 +69,7 @@ export type AssociateAddressResponse = { status: string; }; -export type CreateUpgradeResponse = { - signerAddress: string; - status: string; - createdAt: string; -}; - -/** - * The upgrade record returned by GET /v1/account-upgrade/:address. - */ -export type GetUpgradeResponse = { +export type UpgradeResponse = { signerAddress: string; status: string; createdAt: string;