Skip to content

Commit 92a86d1

Browse files
authored
feat: network, client, asset managers in ts; minor refinements in rs and ts to further align with legacy utils-ts (#280)
* chore: wip network, client, asset manager tweaks * chore: wip * feat(ts): batch asset bulk opt operations * feat(rust): batch asset bulk operations * refactor: expand supported network aliases in network client in rs and ts * feat: add default retryable http client in ts (adopted from legacy utils-ts) * chore: prettier * fix: bult opt out rust test * chore: revert algorand changes (out of pr scope) * chore: typo * chore: tests wip * chore: consolidating test helpers * chore: consolidate lint and format scripts; ensure all packages run lint as part of build * chore: further removal of dependencies on algosdk * chore: pr comments * refactor: pr comments * chore: adding retry to default fetch client; minor clippy tweaks * fix: limit account information endpoint to json only
1 parent 79bf5cd commit 92a86d1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+4679
-912
lines changed

.github/workflows/api_ci.yml

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,13 +129,10 @@ jobs:
129129
working-directory: packages/typescript
130130
run: npm run lint --workspace ${{ matrix.workspace }}
131131

132-
- name: Build all TypeScript packages
132+
- name: Build workspace
133133
working-directory: packages/typescript
134134
run: |
135-
npm run build --workspace @algorandfoundation/algokit-common
136-
npm run build --workspace @algorandfoundation/algokit-abi
137-
npm run build --workspace @algorandfoundation/algokit-transact
138-
npm run build --workspace ${{ matrix.workspace }}
135+
npm run build
139136
140137
- name: Update package links after build
141138
working-directory: packages/typescript

api/oas_generator/rust_oas_generator/parser/oas_parser.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -787,12 +787,12 @@ def _parse_parameter(self, param_data: dict[str, Any]) -> Parameter | None:
787787
if not name:
788788
return None
789789

790-
# Skip `format` query parameter when constrained to msgpack only
790+
# Skip `format` query parameter when constrained to a single format (json or msgpack)
791791
in_location = param_data.get("in", "query")
792792
if name == "format" and in_location == "query":
793793
schema_obj = param_data.get("schema", {}) or {}
794794
enum_vals = schema_obj.get("enum")
795-
if isinstance(enum_vals, list) and len(enum_vals) == 1 and enum_vals[0] == "msgpack":
795+
if isinstance(enum_vals, list) and len(enum_vals) == 1 and enum_vals[0] in ("msgpack", "json"):
796796
return None
797797

798798
schema = param_data.get("schema", {})

api/oas_generator/ts_oas_generator/generator/template_engine.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -385,13 +385,13 @@ def _process_parameters(self, params: list[Schema], spec: Schema) -> list[Parame
385385

386386
# Extract parameter details
387387
raw_name = str(param.get("name"))
388-
# Skip `format` query param when it's constrained to only msgpack
388+
# Skip `format` query param when it's constrained to a single format (json or msgpack)
389389
location_candidate = param.get(constants.OperationKey.IN, constants.ParamLocation.QUERY)
390390
if location_candidate == constants.ParamLocation.QUERY and raw_name == constants.FORMAT_PARAM_NAME:
391391
schema_obj = param.get("schema", {}) or {}
392392
enum_vals = schema_obj.get(constants.SchemaKey.ENUM)
393-
if isinstance(enum_vals, list) and len(enum_vals) == 1 and enum_vals[0] == "msgpack":
394-
# Endpoint only supports msgpack; do not expose/append `format`
393+
if isinstance(enum_vals, list) and len(enum_vals) == 1 and enum_vals[0] in ("msgpack", "json"):
394+
# Endpoint only supports a single format; do not expose/append `format`
395395
continue
396396
var_name = self._sanitize_variable_name(ts_camel_case(raw_name), used_names)
397397
used_names.add(var_name)

api/oas_generator/ts_oas_generator/templates/base/src/core/client-config.ts.j2

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,12 @@ export interface ClientConfig {
1111
password?: string;
1212
headers?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);
1313
encodePath?: (path: string) => string;
14+
/** Optional override for retry attempts; values <= 1 disable retries. This is the canonical field. */
15+
maxRetries?: number;
16+
/** Optional cap on exponential backoff delay in milliseconds. */
17+
maxBackoffMs?: number;
18+
/** Optional list of HTTP status codes that should trigger a retry. */
19+
retryStatusCodes?: number[];
20+
/** Optional list of Node.js/System error codes that should trigger a retry. */
21+
retryErrorCodes?: string[];
1422
}
15-
Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,120 @@
11
import { BaseHttpRequest, type ApiRequestOptions } from './base-http-request';
22
import { request } from './request';
33

4+
const RETRY_STATUS_CODES = [408, 413, 429, 500, 502, 503, 504];
5+
const RETRY_ERROR_CODES = [
6+
'ETIMEDOUT',
7+
'ECONNRESET',
8+
'EADDRINUSE',
9+
'ECONNREFUSED',
10+
'EPIPE',
11+
'ENOTFOUND',
12+
'ENETUNREACH',
13+
'EAI_AGAIN',
14+
'EPROTO',
15+
];
16+
17+
const DEFAULT_MAX_TRIES = 5;
18+
const DEFAULT_MAX_BACKOFF_MS = 10_000;
19+
20+
const toNumber = (value: unknown): number | undefined => {
21+
if (typeof value === 'number') {
22+
return Number.isNaN(value) ? undefined : value;
23+
}
24+
if (typeof value === 'string') {
25+
const parsed = Number(value);
26+
return Number.isNaN(parsed) ? undefined : parsed;
27+
}
28+
return undefined;
29+
};
30+
31+
const extractStatus = (error: unknown): number | undefined => {
32+
if (!error || typeof error !== 'object') {
33+
return undefined;
34+
}
35+
const candidate = error as { status?: unknown; response?: { status?: unknown } };
36+
return toNumber(candidate.status ?? candidate.response?.status);
37+
};
38+
39+
const extractCode = (error: unknown): string | undefined => {
40+
if (!error || typeof error !== 'object') {
41+
return undefined;
42+
}
43+
const candidate = error as { code?: unknown; cause?: { code?: unknown } };
44+
const raw = candidate.code ?? candidate.cause?.code;
45+
return typeof raw === 'string' ? raw : undefined;
46+
};
47+
48+
const delay = async (ms: number): Promise<void> =>
49+
new Promise((resolve) => {
50+
setTimeout(resolve, ms);
51+
});
52+
53+
const normalizeTries = (maxRetries?: number): number => {
54+
const candidate = maxRetries;
55+
if (typeof candidate !== 'number' || !Number.isFinite(candidate)) {
56+
return DEFAULT_MAX_TRIES;
57+
}
58+
const rounded = Math.floor(candidate);
59+
return rounded <= 1 ? 1 : rounded;
60+
};
61+
62+
const normalizeBackoff = (maxBackoffMs?: number): number => {
63+
if (typeof maxBackoffMs !== 'number' || !Number.isFinite(maxBackoffMs)) {
64+
return DEFAULT_MAX_BACKOFF_MS;
65+
}
66+
const normalized = Math.floor(maxBackoffMs);
67+
return normalized <= 0 ? 0 : normalized;
68+
};
69+
470
export class FetchHttpRequest extends BaseHttpRequest {
571
async request<T>(options: ApiRequestOptions): Promise<T> {
6-
return request(this.config, options);
72+
const maxTries = normalizeTries(this.config.maxRetries);
73+
const maxBackoffMs = normalizeBackoff(this.config.maxBackoffMs);
74+
75+
let attempt = 1;
76+
let lastError: unknown;
77+
while (attempt <= maxTries) {
78+
try {
79+
return await request(this.config, options);
80+
} catch (error) {
81+
lastError = error;
82+
if (!this.shouldRetry(error, attempt, maxTries)) {
83+
throw error;
84+
}
85+
86+
const backoff = attempt === 1 ? 0 : Math.min(1000 * 2 ** (attempt - 1), maxBackoffMs);
87+
if (backoff > 0) {
88+
await delay(backoff);
89+
}
90+
attempt += 1;
91+
}
92+
}
93+
94+
throw lastError ?? new Error(`Request failed after ${maxTries} attempt(s)`)
95+
}
96+
97+
private shouldRetry(error: unknown, attempt: number, maxTries: number): boolean {
98+
if (attempt >= maxTries) {
99+
return false;
100+
}
101+
102+
const status = extractStatus(error);
103+
if (status !== undefined) {
104+
const retryStatuses = this.config.retryStatusCodes ?? RETRY_STATUS_CODES;
105+
if (retryStatuses.includes(status)) {
106+
return true;
107+
}
108+
}
109+
110+
const code = extractCode(error);
111+
if (code) {
112+
const retryCodes = this.config.retryErrorCodes ?? RETRY_ERROR_CODES;
113+
if (retryCodes.includes(code)) {
114+
return true;
115+
}
116+
}
117+
118+
return false;
7119
}
8120
}

api/scripts/convert-openapi.ts

Lines changed: 52 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ interface FieldTransform {
4141
addItems?: Record<string, any>; // properties to add to the target property, e.g., {"x-custom": true}
4242
}
4343

44-
interface MsgpackOnlyEndpoint {
44+
interface FilterEndpoint {
4545
path: string; // Exact path to match (e.g., "/v2/blocks/{round}")
4646
methods?: string[]; // HTTP methods to apply to (default: ["get"])
4747
}
@@ -54,7 +54,8 @@ interface ProcessingConfig {
5454
vendorExtensionTransforms?: VendorExtensionTransform[];
5555
requiredFieldTransforms?: RequiredFieldTransform[];
5656
fieldTransforms?: FieldTransform[];
57-
msgpackOnlyEndpoints?: MsgpackOnlyEndpoint[];
57+
msgpackOnlyEndpoints?: FilterEndpoint[];
58+
jsonOnlyEndpoints?: FilterEndpoint[];
5859
// If true, strip APIVn prefixes from component schemas and update refs (KMD)
5960
stripKmdApiVersionPrefixes?: boolean;
6061
}
@@ -480,18 +481,18 @@ function transformRequiredFields(spec: OpenAPISpec, requiredFieldTransforms: Req
480481
}
481482
482483
/**
483-
* Enforce msgpack-only format for specific endpoints by removing JSON support
484-
*
485-
* This function modifies endpoints to only support msgpack format, aligning with
486-
* Go and JavaScript SDK implementations that hardcode these endpoints to msgpack.
484+
* Enforce a single endpoint format (json or msgpack) by stripping the opposite one
487485
*/
488-
function enforceMsgpackOnlyEndpoints(spec: OpenAPISpec, endpoints: MsgpackOnlyEndpoint[]): number {
486+
function enforceEndpointFormat(spec: OpenAPISpec, endpoints: FilterEndpoint[], targetFormat: "json" | "msgpack"): number {
489487
let modifiedCount = 0;
490488
491489
if (!spec.paths || !endpoints?.length) {
492490
return modifiedCount;
493491
}
494492
493+
const targetContentType = targetFormat === "json" ? "application/json" : "application/msgpack";
494+
const otherContentType = targetFormat === "json" ? "application/msgpack" : "application/json";
495+
495496
for (const endpoint of endpoints) {
496497
const pathObj = spec.paths[endpoint.path];
497498
if (!pathObj) {
@@ -507,54 +508,58 @@ function enforceMsgpackOnlyEndpoints(spec: OpenAPISpec, endpoints: MsgpackOnlyEn
507508
continue;
508509
}
509510
510-
// Look for format parameter in query parameters
511+
// Query parameter: format
511512
if (operation.parameters && Array.isArray(operation.parameters)) {
512513
for (const param of operation.parameters) {
513-
// Handle both inline parameters and $ref parameters
514514
const paramObj = param.$ref ? resolveRef(spec, param.$ref) : param;
515-
516515
if (paramObj && paramObj.name === "format" && paramObj.in === "query") {
517-
// OpenAPI 3.0 has schema property containing the type information
518516
const schemaObj = paramObj.schema || paramObj;
519-
520-
// Check if it has an enum with both json and msgpack
521517
if (schemaObj.enum && Array.isArray(schemaObj.enum)) {
522-
if (schemaObj.enum.includes("json") && schemaObj.enum.includes("msgpack")) {
523-
// Remove json from enum, keep only msgpack
524-
schemaObj.enum = ["msgpack"];
525-
// Update default if it was json
526-
if (schemaObj.default === "json") {
527-
schemaObj.default = "msgpack";
518+
const values: string[] = schemaObj.enum;
519+
if (values.includes("json") || values.includes("msgpack")) {
520+
if (values.length !== 1 || values[0] !== targetFormat) {
521+
schemaObj.enum = [targetFormat];
522+
if (schemaObj.default !== targetFormat) schemaObj.default = targetFormat;
523+
modifiedCount++;
524+
console.log(`ℹ️ Enforced ${targetFormat}-only for ${endpoint.path} (${method}) parameter`);
528525
}
529-
// Don't modify the description - preserve original documentation
530-
modifiedCount++;
531-
console.log(`ℹ️ Enforced msgpack-only for ${endpoint.path} (${method}) parameter`);
532526
}
533527
} else if (schemaObj.type === "string" && !schemaObj.enum) {
534-
// If no enum is specified, add one with only msgpack
535-
schemaObj.enum = ["msgpack"];
536-
schemaObj.default = "msgpack";
537-
// Don't modify the description - preserve original documentation
528+
schemaObj.enum = [targetFormat];
529+
schemaObj.default = targetFormat;
538530
modifiedCount++;
539-
console.log(`ℹ️ Enforced msgpack-only for ${endpoint.path} (${method}) parameter`);
531+
console.log(`ℹ️ Enforced ${targetFormat}-only for ${endpoint.path} (${method}) parameter`);
540532
}
541533
}
542534
}
543535
}
544536
545-
// Also check for format in response content types
537+
// Request body content types
538+
if (operation.requestBody && typeof operation.requestBody === "object") {
539+
const rbRaw: any = operation.requestBody;
540+
const rb: any = rbRaw.$ref ? resolveRef(spec, rbRaw.$ref) || rbRaw : rbRaw;
541+
if (rb && rb.content && rb.content[otherContentType] && rb.content[targetContentType]) {
542+
delete rb.content[otherContentType];
543+
modifiedCount++;
544+
console.log(`ℹ️ Removed ${otherContentType} request content-type for ${endpoint.path} (${method})`);
545+
}
546+
}
547+
548+
// Response content types
546549
if (operation.responses) {
547550
for (const [statusCode, response] of Object.entries(operation.responses)) {
548551
if (response && typeof response === "object") {
549552
const responseObj = response as any;
550-
551-
// If response has content with both json and msgpack, remove json
552-
if (responseObj.content) {
553-
if (responseObj.content["application/json"] && responseObj.content["application/msgpack"]) {
554-
delete responseObj.content["application/json"];
555-
modifiedCount++;
556-
console.log(`ℹ️ Removed JSON response content-type for ${endpoint.path} (${method}) - ${statusCode}`);
557-
}
553+
const responseTarget: any = responseObj.$ref ? resolveRef(spec, responseObj.$ref) || responseObj : responseObj;
554+
if (
555+
responseTarget &&
556+
responseTarget.content &&
557+
responseTarget.content[otherContentType] &&
558+
responseTarget.content[targetContentType]
559+
) {
560+
delete responseTarget.content[otherContentType];
561+
modifiedCount++;
562+
console.log(`ℹ️ Removed ${otherContentType} response content-type for ${endpoint.path} (${method}) - ${statusCode}`);
558563
}
559564
}
560565
}
@@ -832,10 +837,16 @@ class OpenAPIProcessor {
832837
833838
// 9. Enforce msgpack-only endpoints if configured
834839
if (this.config.msgpackOnlyEndpoints && this.config.msgpackOnlyEndpoints.length > 0) {
835-
const msgpackCount = enforceMsgpackOnlyEndpoints(spec, this.config.msgpackOnlyEndpoints);
840+
const msgpackCount = enforceEndpointFormat(spec, this.config.msgpackOnlyEndpoints, "msgpack");
836841
console.log(`ℹ️ Enforced msgpack-only format for ${msgpackCount} endpoint parameters/responses`);
837842
}
838843
844+
// 10. Enforce json-only endpoints if configured
845+
if (this.config.jsonOnlyEndpoints && this.config.jsonOnlyEndpoints.length > 0) {
846+
const jsonCount = enforceEndpointFormat(spec, this.config.jsonOnlyEndpoints, "json");
847+
console.log(`ℹ️ Enforced json-only format for ${jsonCount} endpoint parameters/responses`);
848+
}
849+
839850
// Save the processed spec
840851
await SwaggerParser.validate(JSON.parse(JSON.stringify(spec)));
841852
console.log("✅ Specification is valid");
@@ -992,6 +1003,10 @@ async function processAlgodSpec() {
9921003
{ path: "/v2/deltas/txn/group/{id}", methods: ["get"] },
9931004
{ path: "/v2/deltas/{round}/txn/group", methods: ["get"] },
9941005
],
1006+
jsonOnlyEndpoints: [
1007+
{ path: "/v2/accounts/{address}", methods: ["get"] },
1008+
{ path: "/v2/accounts/{address}/assets/{asset-id}", methods: ["get"] },
1009+
],
9951010
};
9961011
9971012
await processAlgorandSpec(config);

0 commit comments

Comments
 (0)