diff --git a/docs/openapi.yaml b/docs/openapi.yaml index c2fabe2..62cbf6d 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -5,13 +5,94 @@ info: description: A koa microservice that provides API interface for fetching and retrieving ArNS related Smartweave contracts. servers: - - url: '/' + - url: '/v1' +components: + schemas: + ArNsContractState: + type: object + properties: + name: + type: string + description: The name of the contract. + ticker: + type: string + description: The ticker of the contract. + owner: + type: string + description: The owner of the contract. + evolve: + type: string + description: The evolved source code transaction ID of the contract. + canEvolve: + type: boolean + description: Flag indicating if the contract can evolve. + records: + type: object + description: The records in the contract. + balances: + type: object + description: The balances in the contract. + + EvaluationOptions: + type: object + properties: + sourceType: + type: string + enum: ['arweave', 'otherSource'] + description: The source type of the evaluation. + internalWrites: + type: boolean + description: Flag to enable or disable internal writes. + useKVStorage: + type: boolean + description: Indicates if key-value storage is used. + remoteStateSyncEnabled: + type: boolean + description: Flag for enabling remote state synchronization. + waitForConfirmation: + type: boolean + description: Whether to wait for confirmation before proceeding. + updateCacheForEachInteraction: + type: boolean + description: Specifies if the cache should be updated for each interaction. + maxInteractionEvaluationTimeSeconds: + type: integer + description: The maximum time in seconds for interaction evaluation. + throwOnInternalWriteError: + type: boolean + description: Indicates if an error should be thrown on internal write failure. + + ContractInteraction: + type: array + description: The interactions for a contract, including their validity + example: + [ + { + 'height': 1242905, + 'input': + { + 'function': 'evolve', + 'value': 'PdBWdSgiNcLw4ge1kveUf8wORcdSaMJsdVXqlQFMPUg', + }, + 'owner': 'QGWqtJdLLgm2ehFWiiPzMaoFLD50CnGuzZIPEdoDRGQ', + 'valid': true, + 'id': '2wszuZi_rwoOFjowdH7GLbgdeIZBaGbMLXiOuIV-6_0', + }, + ] paths: - /healthcheck: + /contract/{contractTxId}: get: - summary: Simple healthcheck endpoint - description: Simple healthcheck endpoint + summary: Fetches a contract state by its transaction id using Warp + description: Fetches a contract state by its transaction id using Warp + parameters: + - in: path + name: contractTxId + required: true + description: Transaction ID of the contract. + schema: + type: string + example: '3aX8Ck5_IRLA3L9o4BJLOWxJDrmLLIPoUGZxqOfmHDI' responses: '200': description: OK @@ -20,31 +101,43 @@ paths: schema: type: object properties: - status: - type: number - example: 200 - timestamp: - type: Date - example: '2021-08-01T00:00:00.000Z' - message: + contractTxId: type: string - example: 'Hello World!' + example: '3aX8Ck5_IRLA3L9o4BJLOWxJDrmLLIPoUGZxqOfmHDI' + state: + $ref: '#/components/schemas/ArNsContractState' + sortKey: + type: string, + example: '000001301946,0000000000000,d2efe5278648460ed160e1d8a28fb86ab686e36cf14a3321d0a2b10c6851ea99' + evaluationOptions: + $ref: '#/components/schemas/EvaluationOptions' + '404': + description: Contract not found. + '503': + description: Internal server error. - /v1/contract/{contractTxId}: + /contract/{contractTxId}/read: get: - summary: Fetches a contract state by its transaction id using Warp - description: Fetches a contract state by its transaction id using Warp + summary: Get the result of a contract read interaction with input parameters + description: Retrieves the result for a specific contract read interaction with given parameters. parameters: - in: path name: contractTxId required: true - description: Transaction ID of the contract. schema: type: string - example: 'E-pRI1bokGWQBqHnbut9rsHSt9Ypbldos3bAtwg4JMc' + example: '3aX8Ck5_IRLA3L9o4BJLOWxJDrmLLIPoUGZxqOfmHDI' + description: Unique identifier of the contract. + - in: query + name: functionName + required: true + schema: + type: string + example: 'gatewayRegistry' + description: The read interaction on the contract you want to call (e.g. gatewayRegistry, rankedGatewayRegistry, etc) responses: '200': - description: OK + description: Successful response with the price details. content: application/json: schema: @@ -52,29 +145,93 @@ paths: properties: contractTxId: type: string - example: 'contractTxId' - state: + example: '3aX8Ck5_IRLA3L9o4BJLOWxJDrmLLIPoUGZxqOfmHDI' + result: type: object + description: The returned result of the read interaction from the Smartweave contract example: {} evaluationOptions: + $ref: '#/components/schemas/EvaluationOptions' + '400': + description: Bad request if query parameters are missing or invalid. + '404': + description: Contract not found. + + /contract/{contractTxId}/price: + get: + summary: Get contract price for interaction and input + description: Retrieves the price for a specific contract based on given parameters. + parameters: + - in: path + name: contractTxId + required: true + schema: + type: string + example: '3aX8Ck5_IRLA3L9o4BJLOWxJDrmLLIPoUGZxqOfmHDI' + description: Unique identifier of the contract. + - in: query + name: interactionName + required: true + schema: + type: string + example: 'extendRecord' + description: Name of the interaction, e.g., 'buyRecord', 'extendRecord', 'increaseUndernameCount', 'submitAuctionBid'. + - in: query + name: name + required: true + schema: + type: string + example: '1984' + description: Name associated with the record. + - in: query + name: years + required: false + schema: + type: integer + example: 1 + description: Number of years for the contract action, applicable if interactionName is 'extendRecord' or 'buyRecord'. + - in: query + name: qty + required: false + schema: + type: integer + description: The number associated with the interaction, application if interactionName is 'increaseUndernameCount', or 'submitAuctionBid'. + responses: + '200': + description: Successful response with the price details. + content: + application/json: + schema: + type: object + properties: + contractTxId: + type: string + example: '3aX8Ck5_IRLA3L9o4BJLOWxJDrmLLIPoUGZxqOfmHDI' + result: type: object - example: - { - 'sourceType': 'arweave', - 'internalWrites': true, - 'useKVStorage': true, - 'remoteStateSyncEnabled': false, - 'waitForConfirmation': true, - 'updateCacheForEachInteraction': true, - 'maxInteractionEvaluationTimeSeconds': 60, - 'throwOnInternalWriteError': true, - } + description: The returned result of the read interaction from the Smartweave contract + properties: + input: + type: object + description: Input parameters used to calculate the price for the provided interaction + example: + { + 'function': 'priceForInteraction', + 'interactionName': 'buyRecord', + 'name': 'test-name', + 'years': 1, + } + price: + type: number + description: Calculated price of the contract. + evaluationOptions: + $ref: '#/components/schemas/EvaluationOptions' + '400': + description: Bad request if query parameters are missing or invalid. '404': description: Contract not found. - '503': - description: Internal server error. - /v1/contract/{contractTxId}/{field}: + /contract/{contractTxId}/{field}: get: summary: Returns the field in the contract state for a given transaction id description: Returns the field in the contract state for a given transaction id @@ -85,7 +242,7 @@ paths: description: Transaction ID of the contract. schema: type: string - example: 'E-pRI1bokGWQBqHnbut9rsHSt9Ypbldos3bAtwg4JMc' + example: '3aX8Ck5_IRLA3L9o4BJLOWxJDrmLLIPoUGZxqOfmHDI' - in: path name: field required: true @@ -103,29 +260,18 @@ paths: properties: contractTxId: type: string - example: 'E-pRI1bokGWQBqHnbut9rsHSt9Ypbldos3bAtwg4JMc' + example: '3aX8Ck5_IRLA3L9o4BJLOWxJDrmLLIPoUGZxqOfmHDI' field: type: any example: {} evaluationOptions: - type: object - example: - { - 'sourceType': 'arweave', - 'internalWrites': true, - 'useKVStorage': true, - 'remoteStateSyncEnabled': false, - 'waitForConfirmation': true, - 'updateCacheForEachInteraction': true, - 'maxInteractionEvaluationTimeSeconds': 60, - 'throwOnInternalWriteError': true, - } + $ref: '#/components/schemas/EvaluationOptions' '404': description: Contract not found. '503': description: Internal server error. - /v1/contract/{contractTxId}/records: + /contract/{contractTxId}/records: get: summary: Returns the record objects for a given contract, who's object parameters match query parameters parameters: @@ -135,7 +281,7 @@ paths: description: ID of the root contract. schema: type: string - example: 'E-pRI1bokGWQBqHnbut9rsHSt9Ypbldos3bAtwg4JMc' + example: '3aX8Ck5_IRLA3L9o4BJLOWxJDrmLLIPoUGZxqOfmHDI' - in: query name: contractTxId required: true @@ -153,7 +299,7 @@ paths: properties: contractTxId: type: string - example: 'E-pRI1bokGWQBqHnbut9rsHSt9Ypbldos3bAtwg4JMc' + example: '3aX8Ck5_IRLA3L9o4BJLOWxJDrmLLIPoUGZxqOfmHDI' records: type: array example: @@ -176,14 +322,13 @@ paths: }, ] evaluationOptions: - type: object - example: {} + $ref: '#/components/schemas/EvaluationOptions' '404': description: Contract not found. '503': description: Internal server error. - /v1/contract/{contractTxId}/records/{name}: + /contract/{contractTxId}/records/{name}: get: summary: Returns the record object for a given contract and name parameters: @@ -193,7 +338,7 @@ paths: description: Transaction ID of the contract. schema: type: string - example: 'E-pRI1bokGWQBqHnbut9rsHSt9Ypbldos3bAtwg4JMc' + example: '3aX8Ck5_IRLA3L9o4BJLOWxJDrmLLIPoUGZxqOfmHDI' - in: path name: name required: true @@ -211,7 +356,7 @@ paths: properties: contractTxId: type: string - example: 'E-pRI1bokGWQBqHnbut9rsHSt9Ypbldos3bAtwg4JMc' + example: '3aX8Ck5_IRLA3L9o4BJLOWxJDrmLLIPoUGZxqOfmHDI' name: type: string example: 'ario' @@ -237,14 +382,13 @@ paths: type: string example: 'QGWqtJdLLgm2ehFWiiPzMaoFLD50CnGuzZIPEdoDRGQ' evaluationOptions: - type: object - example: {} + $ref: '#/components/schemas/EvaluationOptions' '404': description: Record does not exist in the contract. '503': description: Internal server error. - /v1/contract/{contractTxId}/balances/{walletAddress}: + /contract/{contractTxId}/balances/{walletAddress}: get: summary: Returns the balance given contract and wallet address parameters: @@ -254,7 +398,7 @@ paths: description: Transaction ID of the contract. schema: type: string - example: 'E-pRI1bokGWQBqHnbut9rsHSt9Ypbldos3bAtwg4JMc' + example: '3aX8Ck5_IRLA3L9o4BJLOWxJDrmLLIPoUGZxqOfmHDI' - in: path name: walletAddress required: true @@ -272,7 +416,7 @@ paths: properties: contractTxId: type: string - example: 'E-pRI1bokGWQBqHnbut9rsHSt9Ypbldos3bAtwg4JMc' + example: '3aX8Ck5_IRLA3L9o4BJLOWxJDrmLLIPoUGZxqOfmHDI' address: type: string example: 'QGWqtJdLLgm2ehFWiiPzMaoFLD50CnGuzZIPEdoDRGQ' @@ -280,14 +424,13 @@ paths: type: number example: 994963650 evaluationOptions: - type: object - example: {} + $ref: '#/components/schemas/EvaluationOptions' '404': description: Wallet does not have a balance in the contract. '503': description: Internal server error. - /v1/contract/{contractTxId}/interactions: + /contract/{contractTxId}/interactions: get: summary: Returns the interactions for a given contract sorted in descending order description: Returns the interactions for a given contract sorted in descending order @@ -298,7 +441,7 @@ paths: description: Transaction ID of the contract. schema: type: string - example: 'E-pRI1bokGWQBqHnbut9rsHSt9Ypbldos3bAtwg4JMc' + example: '3aX8Ck5_IRLA3L9o4BJLOWxJDrmLLIPoUGZxqOfmHDI' responses: '200': description: OK @@ -309,41 +452,17 @@ paths: properties: contractTxId: type: string - example: 'E-pRI1bokGWQBqHnbut9rsHSt9Ypbldos3bAtwg4JMc' + example: '3aX8Ck5_IRLA3L9o4BJLOWxJDrmLLIPoUGZxqOfmHDI' interactions: - type: array - example: - [ - { - 'height': 1242905, - 'input': - { - 'function': 'evolve', - 'value': 'PdBWdSgiNcLw4ge1kveUf8wORcdSaMJsdVXqlQFMPUg', - }, - 'owner': 'QGWqtJdLLgm2ehFWiiPzMaoFLD50CnGuzZIPEdoDRGQ', - 'valid': true, - 'id': '2wszuZi_rwoOFjowdH7GLbgdeIZBaGbMLXiOuIV-6_0', - }, - ] + $ref: '#/components/schemas/ContractInteraction' evaluationOptions: - type: object - example: - { - 'sourceType': 'arweave', - 'internalWrites': true, - 'useKVStorage': true, - 'remoteStateSyncEnabled': false, - 'waitForConfirmation': true, - 'updateCacheForEachInteraction': true, - 'maxInteractionEvaluationTimeSeconds': 60, - 'throwOnInternalWriteError': true, - } + $ref: '#/components/schemas/EvaluationOptions' '404': description: Contract not found. '503': description: Internal server error. - /v1/contract/{contractTxId}/interactions/{walletAddress}: + + /contract/{contractTxId}/interactions/{walletAddress}: get: summary: Returns the interactions for a given contract created by a given wallet address parameters: @@ -353,7 +472,7 @@ paths: description: Transaction ID of the contract. schema: type: string - example: 'E-pRI1bokGWQBqHnbut9rsHSt9Ypbldos3bAtwg4JMc' + example: '3aX8Ck5_IRLA3L9o4BJLOWxJDrmLLIPoUGZxqOfmHDI' - in: path name: walletAddress required: true @@ -371,7 +490,7 @@ paths: properties: contractTxId: type: string - example: 'E-pRI1bokGWQBqHnbut9rsHSt9Ypbldos3bAtwg4JMc' + example: '3aX8Ck5_IRLA3L9o4BJLOWxJDrmLLIPoUGZxqOfmHDI' address: type: string example: 'QGWqtJdLLgm2ehFWiiPzMaoFLD50CnGuzZIPEdoDRGQ' @@ -392,14 +511,13 @@ paths: }, ] evaluationOptions: - type: object - example: {} + $ref: '#/components/schemas/EvaluationOptions' '404': description: Contract not found. '503': description: Internal server error. - /v1/wallet/{address}/contracts: + /wallet/{address}/contracts: get: summary: Returns the contracts deployed, transferred to, or controlled by by a given wallet address description: Returns the contracts deployed, transferred to, or controlled by by a given wallet address @@ -449,7 +567,7 @@ paths: '503': description: Internal server error. - /v1/wallet/{address}/contract/{contractTxId}: + /wallet/{address}/contract/{contractTxId}: get: summary: Returns the interactions on a given contract for a given wallet address description: Returns the interactions on a given contract for a given wallet address @@ -467,7 +585,7 @@ paths: description: Transaction ID of the contract. schema: type: string - example: 'E-pRI1bokGWQBqHnbut9rsHSt9Ypbldos3bAtwg4JMc' + example: '3aX8Ck5_IRLA3L9o4BJLOWxJDrmLLIPoUGZxqOfmHDI' responses: '200': description: OK @@ -481,7 +599,7 @@ paths: example: 'QGWqtJdLLgm2ehFWiiPzMaoFLD50CnGuzZIPEdoDRGQ' contractTxId: type: string - example: 'E-pRI1bokGWQBqHnbut9rsHSt9Ypbldos3bAtwg4JMc' + example: '3aX8Ck5_IRLA3L9o4BJLOWxJDrmLLIPoUGZxqOfmHDI' interactions: type: array example: @@ -499,18 +617,7 @@ paths: }, ] evaluationOptions: - type: object - example: - { - 'sourceType': 'arweave', - 'internalWrites': true, - 'useKVStorage': true, - 'remoteStateSyncEnabled': false, - 'waitForConfirmation': true, - 'updateCacheForEachInteraction': true, - 'maxInteractionEvaluationTimeSeconds': 60, - 'throwOnInternalWriteError': true, - } + $ref: '#/components/schemas/EvaluationOptions' '404': description: Wallet not found. '503': diff --git a/src/api/warp.ts b/src/api/warp.ts index e3ae44f..fc187f5 100644 --- a/src/api/warp.ts +++ b/src/api/warp.ts @@ -20,13 +20,8 @@ import { EVALUATION_TIMEOUT_MS, allowedContractTypes, } from '../constants'; -import { - ContractType, - EvaluatedContractState, - EvaluationError, - NotFoundError, - UnknownError, -} from '../types'; +import { ContractType, EvaluatedContractState } from '../types'; +import { EvaluationError, NotFoundError, UnknownError } from '../errors'; import * as _ from 'lodash'; import { EvaluationTimeoutError } from '../errors'; import { createHash } from 'crypto'; @@ -189,6 +184,7 @@ async function readThroughToContractState( cacheKey: cacheKey.toString(), error: error instanceof Error ? error.message : 'Unknown error', }); + throw error; }) .finally(() => { logger?.debug('Removing request from in-flight cache.', { @@ -334,6 +330,7 @@ export async function readThroughToContractReadInteraction( cacheKey: cacheKey.toString(), error: error instanceof Error ? error.message : 'Unknown error', }); + throw error; }) .finally(() => { logger?.debug('Removing request from in-flight cache.', { @@ -345,6 +342,14 @@ export async function readThroughToContractReadInteraction( // await the response const { result } = await requestMap.get(cacheId); + + // we shouldn't return read interactions that don't have a reult + if (!result) { + throw new UnknownError( + `Read interaction failed to evaluate for contract ${contractTxId}.`, + ); + } + logger?.debug('Successfully evaluated read interaction on contract.', { contractTxId, cacheKey: cacheKey.toString(), diff --git a/src/errors.ts b/src/errors.ts index 2679220..daaea59 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -22,3 +22,8 @@ export class EvaluationTimeoutError extends Error { super(`State evaluation exceeded limit of ${EVALUATION_TIMEOUT_MS}ms.`); } } + +export class EvaluationError extends Error {} +export class NotFoundError extends Error {} +export class UnknownError extends Error {} +export class BadRequestError extends Error {} diff --git a/src/middleware/errors.ts b/src/middleware/errors.ts index 8ab4862..49f43d9 100644 --- a/src/middleware/errors.ts +++ b/src/middleware/errors.ts @@ -16,7 +16,7 @@ */ import { KoaContext } from '../types'; import { Next } from 'koa'; -import { BadRequestError, EvaluationError, NotFoundError } from '../types'; +import { BadRequestError, EvaluationError, NotFoundError } from '../errors'; // globally handle errors and return proper status based on their type export async function errorMiddleware(ctx: KoaContext, next: Next) { diff --git a/src/routes/contract.ts b/src/routes/contract.ts index cc6c6d9..22e2aa1 100644 --- a/src/routes/contract.ts +++ b/src/routes/contract.ts @@ -20,10 +20,10 @@ import { ContractRecordResponse, ContractReservedResponse, KoaContext, - NotFoundError, } from '../types'; import { getContractReadInteraction, getContractState } from '../api/warp'; import { getWalletInteractionsForContract } from '../api/graphql'; +import { NotFoundError } from '../errors'; export async function contractHandler(ctx: KoaContext, next: Next) { const { logger, warp } = ctx.state; diff --git a/src/routes/wallet.ts b/src/routes/wallet.ts index 9f91d4d..cf2f1f8 100644 --- a/src/routes/wallet.ts +++ b/src/routes/wallet.ts @@ -15,7 +15,7 @@ * along with this program. If not, see . */ import { Next } from 'koa'; -import { BadRequestError, KoaContext } from '../types'; +import { KoaContext } from '../types'; import { getContractsTransferredToOrControlledByWallet, getDeployedContractsByWallet, @@ -23,6 +23,7 @@ import { import { isValidContractType, validateStateWithTimeout } from '../api/warp'; import { allowedContractTypes } from '../constants'; import * as _ from 'lodash'; +import { BadRequestError } from '../errors'; export async function walletContractHandler(ctx: KoaContext, next: Next) { const { address } = ctx.params;