diff --git a/.changeset/little-sloths-cry.md b/.changeset/little-sloths-cry.md new file mode 100644 index 00000000..02d9c156 --- /dev/null +++ b/.changeset/little-sloths-cry.md @@ -0,0 +1,7 @@ +--- +"@mimicprotocol/lib-ts": patch +"@mimicprotocol/cli": patch +"@mimicprotocol/test-ts": patch +--- + +Add api query diff --git a/packages/cli/src/lib/InputsInterfaceGenerator.ts b/packages/cli/src/lib/InputsInterfaceGenerator.ts index 10b18b7d..1c963838 100644 --- a/packages/cli/src/lib/InputsInterfaceGenerator.ts +++ b/packages/cli/src/lib/InputsInterfaceGenerator.ts @@ -44,17 +44,10 @@ function generateImports(inputs: Record): string { } function generateInputsMapping(inputs: Record, originalInputs: ManifestInputs): string { + const variableTypes = new Set(['string', 'Address', 'Bytes', 'BigInt', 'BlockchainToken', 'TokenAmount']) return Object.entries(inputs) .map(([name, type]) => { - const declaration = - type === 'string' || - type === 'Address' || - type === 'Bytes' || - type === 'BigInt' || - type === 'BlockchainToken' || - type === 'TokenAmount' - ? `var ${name}: string | null` - : `const ${name}: ${type}` + const declaration = variableTypes.has(type) ? `var ${name}: string | null` : `const ${name}: ${type}` const originalInput = originalInputs[name] const hasDescription = typeof originalInput === 'object' && !!originalInput.description diff --git a/packages/cli/src/lib/ManifestHandler.ts b/packages/cli/src/lib/ManifestHandler.ts index 297d7308..85851ca3 100644 --- a/packages/cli/src/lib/ManifestHandler.ts +++ b/packages/cli/src/lib/ManifestHandler.ts @@ -4,7 +4,7 @@ import * as fs from 'fs' import { load } from 'js-yaml' import { ZodError } from 'zod' -import { DuplicateEntryError, EmptyManifestError, MoreThanOneEntryError } from '../errors' +import { DuplicateEntryError, EmptyManifestError, GENERIC_SUGGESTION, MoreThanOneEntryError } from '../errors' import { Manifest } from '../types' import { ManifestValidator } from '../validators' @@ -41,9 +41,9 @@ export default { }, } -function mergeIfUnique(list: Record[]) { +function mergeIfUnique(list: Record[] = []) { const merged: Record = {} - for (const obj of list || []) { + for (const obj of list) { const entries = Object.entries(obj) if (entries.length !== 1) throw new MoreThanOneEntryError(entries) const [key, val] = entries[0] @@ -72,9 +72,7 @@ function handleValidationError(command: Command, err: unknown): never { suggestions = err.errors.map((e) => `Fix Field "${e.path.join('.')}" -- ${e.message}`) } else { ;[message, code] = [`Unkown Error: ${err}`, 'UnknownError'] - suggestions = [ - 'Contact the Mimic team for further assistance at our website https://www.mimic.fi/ or discord https://discord.com/invite/cpcyV9EsEg', - ] + suggestions = GENERIC_SUGGESTION } command.error(message, { code, suggestions }) diff --git a/packages/lib-ts/index.ts b/packages/lib-ts/index.ts index 3f018da1..60b9f32e 100644 --- a/packages/lib-ts/index.ts +++ b/packages/lib-ts/index.ts @@ -4,6 +4,7 @@ export * from './src/evm' export * from './src/helpers' export * from './src/intents' export * from './src/log' +export * from './src/queries' export * from './src/storage' export * from './src/svm' export * from './src/tokens' diff --git a/packages/lib-ts/src/environment.ts b/packages/lib-ts/src/environment.ts index 12b93bd4..4ef61a5d 100644 --- a/packages/lib-ts/src/environment.ts +++ b/packages/lib-ts/src/environment.ts @@ -5,6 +5,8 @@ import { evm } from './evm' import { Consensus, ListType, MIMIC_HELPER_ADDRESS } from './helpers' import { Intent } from './intents' import { + ApiQuery, + ApiQueryResponse, EvmCallQuery, EvmCallQueryResponse, RelevantTokensQuery, @@ -38,6 +40,9 @@ export namespace environment { @external('environment', '_subgraphQuery') declare function _subgraphQuery(params: string): string + @external('environment', '_apiQuery') + declare function _apiQuery(params: string): string + @external('environment', '_svmAccountsInfoQuery') declare function _svmAccountsInfoQuery(params: string): string @@ -162,6 +167,20 @@ export namespace environment { return SvmAccountsInfoQueryResponse.fromJson(responseStr).toResult() } + /** + * Executes an HTTP API GET call and returns the raw (stringified) response body. + * @param url - The endpoint URL to call + * @param timestamp - Optional. Cache/snapshot timestamp used to fetch a previously cached response at the given point in time + * @returns A `Result` containing either the response body as a string or an error string + */ + export function apiQuery( + url: string, + timestamp: Date | null = null + ): Result { + const responseStr = _apiQuery(JSON.stringify(ApiQuery.from(url, timestamp))) + return ApiQueryResponse.fromJson(responseStr).toResult() + } + /** * Returns the current execution context containing environment information. * @returns The Context object containing: user, settler, timestamp, consensusThreshold and triggerPayload diff --git a/packages/lib-ts/src/queries/ApiQuery.ts b/packages/lib-ts/src/queries/ApiQuery.ts new file mode 100644 index 00000000..4971fbc8 --- /dev/null +++ b/packages/lib-ts/src/queries/ApiQuery.ts @@ -0,0 +1,36 @@ +import { Result } from '../types' + +import { QueryResponseBase } from './QueryResponse' + +@json +class ApiQueryBase { + constructor(public readonly url: string) {} +} + +@json +export class ApiQuery extends ApiQueryBase { + public readonly timestamp: i64 + + constructor(url: string, timestamp: i64) { + super(url) + this.timestamp = timestamp + } + + static from(url: string, timestamp: Date | null): ApiQueryBase { + return timestamp ? new ApiQuery(url, changetype(timestamp).getTime()) : new ApiQueryBase(url) + } +} + +@json +export class ApiQueryResponse extends QueryResponseBase { + public data: string + + constructor(success: string, data: string, error: string) { + super(success, error) + this.data = data + } + + toResult(): Result { + return this.buildResult(this.data, 'Unknown error getting API response') + } +} diff --git a/packages/lib-ts/src/queries/index.ts b/packages/lib-ts/src/queries/index.ts index c59a2729..f302bf0b 100644 --- a/packages/lib-ts/src/queries/index.ts +++ b/packages/lib-ts/src/queries/index.ts @@ -1,3 +1,4 @@ +export * from './ApiQuery' export * from './EvmCallQuery' export * from './QueryResponse' export * from './RelevantTokensQuery' diff --git a/packages/lib-ts/tests/queries/ApiQuery.spec.ts b/packages/lib-ts/tests/queries/ApiQuery.spec.ts new file mode 100644 index 00000000..495aee85 --- /dev/null +++ b/packages/lib-ts/tests/queries/ApiQuery.spec.ts @@ -0,0 +1,53 @@ +import { ApiQueryResponse } from '../../src/queries' + +describe('ApiQueryResponse', () => { + describe('toResult', () => { + describe('when response is successful', () => { + describe('when data is provided', () => { + it('should return result with data', () => { + const responseData = '{"test": true}' + const response = new ApiQueryResponse('true', responseData, '') + const result = response.toResult() + + expect(result.isOk).toBe(true) + const data = result.unwrap() + expect(data).toBe(responseData) + }) + }) + + describe('when data is empty', () => { + it('should return empty string', () => { + const response = new ApiQueryResponse('true', '', '') + const result = response.toResult() + + expect(result.isOk).toBe(true) + const data = result.unwrap() + expect(data).toBe('') + }) + }) + }) + + describe('when response is not successful', () => { + describe('when error message is provided', () => { + it('should return error with provided message', () => { + const errorMessage = 'Something went wrong' + const response = new ApiQueryResponse('false', '', errorMessage) + const result = response.toResult() + + expect(result.isError).toBe(true) + expect(result.error).toBe(errorMessage) + }) + }) + + describe('when error message is not provided', () => { + it('should return default error message', () => { + const response = new ApiQueryResponse('false', '', '') + const result = response.toResult() + + expect(result.isError).toBe(true) + expect(result.error).toBe('Unknown error getting API response') + }) + }) + }) + }) +})