From 7dca3fe5b955c82d4f61e83dee0a876acce563b5 Mon Sep 17 00:00:00 2001 From: simplicityf Date: Mon, 29 Jun 2026 14:22:53 +0100 Subject: [PATCH] feat: integrate axios for API requests with automatic retries - Added axios and axios-retry as dependencies. - Refactored auth challenge and dispute client to use axios for HTTP requests. - Implemented automatic retries for transient failures in API calls. - Updated tests to mock axios and verify behavior under various scenarios. - Created utility functions for error handling and HTTP client creation. - Added tests for new HTTP client functionality and XDR payload building. --- CHANGELOG.md | 11 + README.md | 5 +- docs/API.md | 15 +- package-lock.json | 1266 +++++++++++++++++++++++++++++++++- package.json | 2 + src/auth/challenge.ts | 47 +- src/escrow/client.ts | 30 +- src/escrow/dispute.ts | 52 +- src/utils/http.ts | 93 +++ src/utils/index.ts | 1 + tests/auth-challenge.test.ts | 56 ++ tests/dispute.test.ts | 47 +- tests/gigs.test.ts | 84 ++- tests/http-retry.test.ts | 121 ++++ tests/xdr-payloads.test.ts | 39 ++ 15 files changed, 1776 insertions(+), 93 deletions(-) create mode 100644 src/utils/http.ts create mode 100644 tests/auth-challenge.test.ts create mode 100644 tests/http-retry.test.ts create mode 100644 tests/xdr-payloads.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 0631317..b5c7e9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [0.2.1] - 2026-06-29 +- Add shared backend API transport in `src/utils/http.ts` using `axios` + `axios-retry` +- Add automatic retries for transient backend failures (`429`, `5xx`, network errors) +- Migrate backend SDK endpoints from `fetch` to shared retry-aware transport: + - `TrustFlowEscrowClient.getGigs` + - `DisputeClient.raiseDispute` / `DisputeClient.getDispute` + - `requestChallenge` / `verifyAndGetToken` +- Add Jest coverage for retry policy and backend transport behavior +- Add Jest tests validating contract argument XDR payload encoding +- Architectural decision: centralize backend HTTP behavior to avoid endpoint-specific retry drift + ## [0.2.0] - 2026-04-28 - Add DisputeClient for dispute management - Add EscrowMonitor for real-time event polling diff --git a/README.md b/README.md index 5b6b305..ac843c4 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,7 @@ See [examples/multisig-escrow.ts](./examples/multisig-escrow.ts) for the full wa - **🔐 Escrow Management**: Create, fund, release, and monitor escrows - **✍️ Multi-Sig Escrows**: M-of-N signature collection for shared backend Escrows via `MultiSigEscrowClient` - **⚖️ Dispute Resolution**: Raise and track disputes with on-chain governance +- **🔁 Backend API Auto-Retries**: Resilient backend calls via `axios-retry` for transient failures - **🔑 Wallet Integration**: Built-in support for Freighter and Albedo wallets - **📊 Event Monitoring**: Real-time escrow state change tracking - **🛡️ Type Safety**: Full TypeScript support with Zod validation schemas @@ -122,10 +123,10 @@ Read more in [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md) The SDK is under active development. Here's what's coming: ### In Progress -- [ ] Tsup bundler configuration for ESM/CJS exports +- [x] Tsup bundler configuration for ESM/CJS exports - [ ] NPM publishing pipeline with provenance - [ ] Simulation wrappers for transaction cost estimation -- [ ] Auto-retry logic for RPC endpoints +- [x] Auto-retry logic for backend API endpoints (`axios-retry`) ### Planned Features - [x] Multi-signature support for corporate escrows diff --git a/docs/API.md b/docs/API.md index 0894ebc..0b269a5 100644 --- a/docs/API.md +++ b/docs/API.md @@ -4,6 +4,7 @@ - `createEscrow(params)` — create a new escrow - `releaseEscrow(id, signer)` — release funds to beneficiary - `getEscrow(id)` — read escrow state from contract +- `getGigs(params)` — fetch paginated gigs via backend API with automatic retries for transient failures (`429`, `5xx`, network) ## EscrowBuilder Fluent builder: `.setDepositor().setBeneficiary().setAmount().build()` @@ -13,9 +14,15 @@ Fluent builder: `.setDepositor().setBeneficiary().setAmount().build()` - `.startPolling(intervalMs, fetchFn)` — begin polling ## DisputeClient -- `.raiseDispute(params)` — raise a dispute -- `.getDispute(escrowId)` — get dispute status +- `.raiseDispute(params)` — raise a dispute (automatic retry on transient backend failures) +- `.getDispute(escrowId)` — get dispute status (automatic retry on transient backend failures) ## Auth -- `requestChallenge(apiUrl, address)` — get signing challenge -- `verifyAndGetToken(apiUrl, address, signature)` — exchange signature for JWT +- `requestChallenge(apiUrl, address, options?)` — get signing challenge with retry-aware backend transport +- `verifyAndGetToken(apiUrl, address, signature, options?)` — exchange signature for JWT with retry-aware backend transport + +## Backend API Retry Behavior +- Backend API endpoints now use a shared Axios transport configured with `axios-retry`. +- Default retry policy: 3 retries, exponential backoff (250ms base, 2000ms max cap). +- Retry conditions: network errors, HTTP `429`, and HTTP `5xx` responses. +- Non-transient `4xx` responses are returned without retry. diff --git a/package-lock.json b/package-lock.json index 183b641..cb72489 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,8 @@ "license": "MIT", "dependencies": { "@stellar/stellar-sdk": "^15.1.0", + "axios": "^1.18.1", + "axios-retry": "^4.5.0", "zod": "^3.22.0" }, "devDependencies": { @@ -546,6 +548,312 @@ "dev": true, "license": "MIT" }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/linux-x64": { "version": "0.27.7", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", @@ -563,6 +871,159 @@ "node": ">=18" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -1236,6 +1697,25 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, "node_modules/@noble/curves": { "version": "1.9.7", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", @@ -1287,6 +1767,277 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz", + "integrity": "sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.0.tgz", + "integrity": "sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.0.tgz", + "integrity": "sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.0.tgz", + "integrity": "sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.0.tgz", + "integrity": "sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.0.tgz", + "integrity": "sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.0.tgz", + "integrity": "sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.0.tgz", + "integrity": "sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.0.tgz", + "integrity": "sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.0.tgz", + "integrity": "sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.0.tgz", + "integrity": "sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.0.tgz", + "integrity": "sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.0.tgz", + "integrity": "sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.0.tgz", + "integrity": "sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.0.tgz", + "integrity": "sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.0.tgz", + "integrity": "sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.0.tgz", + "integrity": "sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.62.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.0.tgz", @@ -1301,6 +2052,107 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.0.tgz", + "integrity": "sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.0.tgz", + "integrity": "sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.0.tgz", + "integrity": "sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.0.tgz", + "integrity": "sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.0.tgz", + "integrity": "sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.0.tgz", + "integrity": "sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.0.tgz", + "integrity": "sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@sinclair/typebox": { "version": "0.34.49", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", @@ -1378,6 +2230,28 @@ "node": ">=20.0.0" } }, + "node_modules/@stellar/stellar-sdk/node_modules/axios": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", + "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1774,6 +2648,240 @@ "dev": true, "license": "ISC" }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", + "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz", + "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz", + "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz", + "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", + "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", + "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", + "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", + "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", + "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", + "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", + "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", + "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", + "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", + "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", + "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@unrs/resolver-binding-linux-x64-gnu": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", @@ -1802,6 +2910,81 @@ "linux" ] }, + "node_modules/@unrs/resolver-binding-openharmony-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", + "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", + "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", + "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", + "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", + "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/acorn": { "version": "8.17.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", @@ -1825,6 +3008,18 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", @@ -1953,16 +3148,41 @@ } }, "node_modules/axios": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", - "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.11", + "follow-redirects": "^1.16.0", "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, + "node_modules/axios-retry": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.5.0.tgz", + "integrity": "sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==", + "license": "Apache-2.0", + "dependencies": { + "is-retry-allowed": "^2.2.0" + }, + "peerDependencies": { + "axios": "0.x || 1.x" + } + }, + "node_modules/axios-retry/node_modules/is-retry-allowed": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", + "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/babel-jest": { "version": "30.4.1", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.4.1.tgz", @@ -2598,7 +3818,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3371,6 +4590,21 @@ "dev": true, "license": "ISC" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3650,6 +4884,19 @@ "dev": true, "license": "MIT" }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -4845,7 +6092,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/mz": { @@ -6138,6 +7384,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/tsup": { "version": "8.5.1", "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", diff --git a/package.json b/package.json index 022d464..4166c90 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,8 @@ "license": "MIT", "dependencies": { "@stellar/stellar-sdk": "^15.1.0", + "axios": "^1.18.1", + "axios-retry": "^4.5.0", "zod": "^3.22.0" }, "devDependencies": { diff --git a/src/auth/challenge.ts b/src/auth/challenge.ts index 858238e..020ca8b 100644 --- a/src/auth/challenge.ts +++ b/src/auth/challenge.ts @@ -1,31 +1,52 @@ +import { createApiHttpClient } from '../utils/http'; + export interface AuthChallenge { challenge: string; expiresAt: number; address: string; } -export async function requestChallenge(apiUrl: string, address: string): Promise { - const res = await fetch(`${apiUrl}/auth/challenge?address=${address}`); - if (!res.ok) { +export interface AuthRequestOptions { + timeoutMs?: number; +} + +/** + * Requests a signing challenge from the TrustFlow backend. + * + * Transient backend failures are automatically retried with exponential backoff. + */ +export async function requestChallenge( + apiUrl: string, + address: string, + options: AuthRequestOptions = {}, +): Promise { + const http = createApiHttpClient({ baseURL: apiUrl, timeoutMs: options.timeoutMs }); + try { + const response = await http.get<{ challenge: string }>('/auth/challenge', { + params: { address }, + }); + return { challenge: response.data.challenge, expiresAt: Date.now() + 60_000, address }; + } catch { throw new Error('Failed to get challenge'); } - const { challenge } = (await res.json()) as { challenge: string }; - return { challenge, expiresAt: Date.now() + 60_000, address }; } +/** + * Verifies a signature and exchanges it for a backend session token. + * + * Transient backend failures are automatically retried with exponential backoff. + */ export async function verifyAndGetToken( apiUrl: string, address: string, signature: string, + options: AuthRequestOptions = {}, ): Promise { - const res = await fetch(`${apiUrl}/auth/verify`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ address, signature }), - }); - if (!res.ok) { + const http = createApiHttpClient({ baseURL: apiUrl, timeoutMs: options.timeoutMs }); + try { + const response = await http.post<{ token: string }>('/auth/verify', { address, signature }); + return response.data.token; + } catch { throw new Error('Signature verification failed'); } - const { token } = (await res.json()) as { token: string }; - return token; } diff --git a/src/escrow/client.ts b/src/escrow/client.ts index fa116e5..fe9b833 100644 --- a/src/escrow/client.ts +++ b/src/escrow/client.ts @@ -1,6 +1,7 @@ import { ContractConfig } from '../types/contract'; import { EscrowParams, EscrowState, SDKResult, GetGigsParams, GigsPage } from '../types/index'; import { assertStellarAddress, xlmToStroops } from '../utils/validation'; +import { createApiHttpClient, toApiErrorMessage } from '../utils/http'; export class TrustFlowEscrowClient { protected readonly contractConfig: ContractConfig; @@ -42,12 +43,15 @@ export class TrustFlowEscrowClient { * you pass back as `cursor` on the next call to advance through results. * When `nextCursor` is `null` (or `hasMore` is `false`) you have reached * the last page. + * + * Network calls automatically retry transient backend failures (`429`, `5xx`, + * and short-lived network errors) using exponential backoff. * * @param params - Optional filter and pagination parameters * @param params.cursor - Opaque cursor from a previous response; omit to start from the first page * @param params.limit - Records per page (default 20, max 100) * @param params.status - Filter by escrow status - * @param params.depositor:2Filter by depositor address + * @param params.depositor - Filter by depositor address * @param params.beneficiary - Filter by beneficiary address * * @returns `{ ok: true, data: GigsPage }` on success, `{ ok: false, error }` on failure @@ -85,24 +89,18 @@ export class TrustFlowEscrowClient { query.set('beneficiary', params.beneficiary); } - const headers: Record = { 'Content-Type': 'application/json' }; - if (this.contractConfig.apiKey) { - headers['Authorization'] = `Bearer ${this.contractConfig.apiKey}`; - } + const http = createApiHttpClient({ + baseURL: this.contractConfig.apiBaseUrl, + apiKey: this.contractConfig.apiKey, + }); - let res: Response; try { - res = await fetch(`${this.contractConfig.apiBaseUrl}/gigs?${query}`, { headers }); + const response = await http.get('/gigs', { + params: Object.fromEntries(query.entries()), + }); + return { ok: true, data: response.data }; } catch (err: unknown) { - const message = err instanceof Error ? err.message : String(err); - return { ok: false, error: `Network error: ${message}` }; - } - - if (!res.ok) { - return { ok: false, error: `HTTP ${res.status}: ${res.statusText}` }; + return { ok: false, error: toApiErrorMessage(err) }; } - - const json = await res.json(); - return { ok: true, data: json as GigsPage }; } } diff --git a/src/escrow/dispute.ts b/src/escrow/dispute.ts index 065be84..67ebe31 100644 --- a/src/escrow/dispute.ts +++ b/src/escrow/dispute.ts @@ -1,39 +1,53 @@ import { DisputeParams, SDKResult } from '../types/index'; +import { createApiHttpClient, toApiErrorMessage } from '../utils/http'; + +export interface DisputeClientOptions { + timeoutMs?: number; +} export class DisputeClient { + private readonly http; + constructor( private apiUrl: string, private token: string, - ) {} + options: DisputeClientOptions = {}, + ) { + this.http = createApiHttpClient({ + baseURL: this.apiUrl, + timeoutMs: options.timeoutMs, + additionalHeaders: { + Authorization: `Bearer ${this.token}`, + }, + }); + } + /** + * Creates a dispute via the backend API. + * + * Transient backend failures are automatically retried before returning an error. + */ async raiseDispute(params: DisputeParams): Promise> { try { - const res = await fetch(`${this.apiUrl}/disputes`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` }, - body: JSON.stringify(params), - }); - if (!res.ok) { - return { ok: false, error: `HTTP ${res.status}` }; - } - const data = (await res.json()) as { id: string }; + const response = await this.http.post<{ id: string }>('/disputes', params); + const data = response.data; return { ok: true, data: { disputeId: data.id } }; } catch (e) { - return { ok: false, error: String(e) }; + return { ok: false, error: toApiErrorMessage(e) }; } } + /** + * Retrieves dispute details from the backend API. + * + * Transient backend failures are automatically retried before returning an error. + */ async getDispute(escrowId: string): Promise> { try { - const res = await fetch(`${this.apiUrl}/disputes/${escrowId}`, { - headers: { Authorization: `Bearer ${this.token}` }, - }); - if (!res.ok) { - return { ok: false, error: `HTTP ${res.status}` }; - } - return { ok: true, data: await res.json() }; + const response = await this.http.get(`/disputes/${escrowId}`); + return { ok: true, data: response.data }; } catch (e) { - return { ok: false, error: String(e) }; + return { ok: false, error: toApiErrorMessage(e) }; } } } diff --git a/src/utils/http.ts b/src/utils/http.ts new file mode 100644 index 0000000..c55b534 --- /dev/null +++ b/src/utils/http.ts @@ -0,0 +1,93 @@ +import axios, { AxiosError, AxiosInstance } from 'axios'; +import axiosRetry from 'axios-retry'; + +/** + * Retry tuning for backend API requests. + */ +export interface ApiRetryConfig { + /** Total retry attempts after the first request. Defaults to 3. */ + retries?: number; + /** Base retry delay in milliseconds. Defaults to 250ms. */ + retryDelayMs?: number; + /** Maximum retry delay in milliseconds. Defaults to 2000ms. */ + maxRetryDelayMs?: number; +} + +export interface ApiHttpClientOptions { + baseURL: string; + apiKey?: string; + timeoutMs?: number; + retry?: ApiRetryConfig; + additionalHeaders?: Record; +} + +const DEFAULT_RETRY_CONFIG: Required = { + retries: 3, + retryDelayMs: 250, + maxRetryDelayMs: 2000, +}; + +/** + * Creates an Axios instance configured with safe automatic retries for transient failures. + * + * Retries are applied to network failures, `429`, and `5xx` responses. + */ +export function createApiHttpClient(options: ApiHttpClientOptions): AxiosInstance { + const retryConfig = { + ...DEFAULT_RETRY_CONFIG, + ...options.retry, + }; + + const headers: Record = { + 'Content-Type': 'application/json', + ...options.additionalHeaders, + }; + + if (options.apiKey) { + headers['Authorization'] = `Bearer ${options.apiKey}`; + } + + const instance = axios.create({ + baseURL: options.baseURL, + timeout: options.timeoutMs ?? 10_000, + headers, + }); + + axiosRetry(instance, { + retries: retryConfig.retries, + retryCondition: (error) => { + const status = error.response?.status; + if (status === 429) { + return true; + } + if (typeof status === 'number' && status >= 500 && status < 600) { + return true; + } + return axiosRetry.isNetworkOrIdempotentRequestError(error); + }, + retryDelay: (retryCount) => { + const delay = retryConfig.retryDelayMs * 2 ** (retryCount - 1); + return Math.min(delay, retryConfig.maxRetryDelayMs); + }, + }); + + return instance; +} + +/** + * Maps unknown transport errors into stable SDK error strings. + */ +export function toApiErrorMessage(error: unknown): string { + if (error instanceof AxiosError) { + const status = error.response?.status; + const statusText = error.response?.statusText; + if (typeof status === 'number') { + return statusText ? `HTTP ${status}: ${statusText}` : `HTTP ${status}`; + } + return `Network error: ${error.message}`; + } + if (error instanceof Error) { + return `Network error: ${error.message}`; + } + return `Network error: ${String(error)}`; +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 706cd82..886605d 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -3,3 +3,4 @@ export * from './format'; export * from './retry'; export * from './error'; export * from './logger'; +export * from './http'; diff --git a/tests/auth-challenge.test.ts b/tests/auth-challenge.test.ts new file mode 100644 index 0000000..831187b --- /dev/null +++ b/tests/auth-challenge.test.ts @@ -0,0 +1,56 @@ +import { requestChallenge, verifyAndGetToken } from '../src/auth/challenge'; + +const mockHttpGet = jest.fn(); +const mockHttpPost = jest.fn(); + +jest.mock('../src/utils/http', () => ({ + createApiHttpClient: jest.fn(() => ({ + get: mockHttpGet, + post: mockHttpPost, + })), +})); + +describe('auth challenge API', () => { + beforeEach(() => { + mockHttpGet.mockReset(); + mockHttpPost.mockReset(); + }); + + it('returns a challenge payload', async () => { + mockHttpGet.mockResolvedValueOnce({ data: { challenge: 'nonce-123' } }); + + const result = await requestChallenge('https://api.trustflow.xyz', 'G' + 'A'.repeat(55)); + + expect(result.challenge).toBe('nonce-123'); + expect(result.address).toBe('G' + 'A'.repeat(55)); + expect(result.expiresAt).toBeGreaterThan(Date.now() - 1_000); + }); + + it('throws on challenge endpoint failure', async () => { + mockHttpGet.mockRejectedValueOnce(new Error('timeout')); + + await expect( + requestChallenge('https://api.trustflow.xyz', 'G' + 'A'.repeat(55)), + ).rejects.toThrow('Failed to get challenge'); + }); + + it('returns token after signature verification', async () => { + mockHttpPost.mockResolvedValueOnce({ data: { token: 'jwt-abc' } }); + + const token = await verifyAndGetToken( + 'https://api.trustflow.xyz', + 'G' + 'A'.repeat(55), + 'signed-payload', + ); + + expect(token).toBe('jwt-abc'); + }); + + it('throws on signature verification failure', async () => { + mockHttpPost.mockRejectedValueOnce(new Error('bad signature')); + + await expect( + verifyAndGetToken('https://api.trustflow.xyz', 'G' + 'A'.repeat(55), 'sig'), + ).rejects.toThrow('Signature verification failed'); + }); +}); diff --git a/tests/dispute.test.ts b/tests/dispute.test.ts index 1d7c466..d723014 100644 --- a/tests/dispute.test.ts +++ b/tests/dispute.test.ts @@ -1,17 +1,60 @@ import { DisputeClient } from '../src/escrow/dispute'; +const mockHttpPost = jest.fn(); +const mockHttpGet = jest.fn(); + +jest.mock('../src/utils/http', () => ({ + createApiHttpClient: jest.fn(() => ({ + post: mockHttpPost, + get: mockHttpGet, + })), + toApiErrorMessage: (error: unknown) => + error instanceof Error ? `Network error: ${error.message}` : `Network error: ${String(error)}`, +})); + describe('DisputeClient', () => { + beforeEach(() => { + mockHttpPost.mockReset(); + mockHttpGet.mockReset(); + }); + it('initialises with api url and token', () => { const client = new DisputeClient('http://api', 'tok'); expect(client).toBeDefined(); }); + it('returns success for raiseDispute when API responds with ID', async () => { + mockHttpPost.mockResolvedValueOnce({ data: { id: 'dsp-1' } }); + + const client = new DisputeClient('http://api', 'tok'); + const result = await client.raiseDispute({ escrowId: 'esc-1', reason: 'test' }); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.data.disputeId).toBe('dsp-1'); + } + }); + it('returns error result on network failure', async () => { - const client = new DisputeClient('http://invalid-host-xyz', 'tok'); + mockHttpPost.mockRejectedValueOnce(new Error('connection reset')); + + const client = new DisputeClient('http://api', 'tok'); const result = await client.raiseDispute({ escrowId: 'esc-1', reason: 'test' }); expect(result.ok).toBe(false); if (!result.ok) { - expect(result.error).toBeTruthy(); + expect(result.error).toMatch(/Network error/); + } + }); + + it('returns dispute payload for getDispute', async () => { + mockHttpGet.mockResolvedValueOnce({ data: { id: 'dsp-1', status: 'open' } }); + + const client = new DisputeClient('http://api', 'tok'); + const result = await client.getDispute('esc-1'); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.data).toEqual({ id: 'dsp-1', status: 'open' }); } }); }); diff --git a/tests/gigs.test.ts b/tests/gigs.test.ts index 7eaf11d..0ecea65 100644 --- a/tests/gigs.test.ts +++ b/tests/gigs.test.ts @@ -1,5 +1,19 @@ import { TrustFlowEscrowClient } from '../src/escrow/client'; import type { GigsPage } from '../src/types/index'; +import { createApiHttpClient } from '../src/utils/http'; + +const mockHttpGet = jest.fn(); +const mockToApiErrorMessage = jest.fn((error: unknown) => { + if (error instanceof Error) { + return `Network error: ${error.message}`; + } + return `Network error: ${String(error)}`; +}); + +jest.mock('../src/utils/http', () => ({ + createApiHttpClient: jest.fn(() => ({ get: mockHttpGet })), + toApiErrorMessage: (error: unknown) => mockToApiErrorMessage(error), +})); const BASE_CONTRACT_CONFIG = { contractId: 'C' + 'A'.repeat(55), @@ -19,14 +33,10 @@ const makePage = (overrides: Partial = {}): GigsPage => ({ }); describe('TrustFlowEscrowClient.getGigs', () => { - let fetchSpy: jest.SpyInstance; - beforeEach(() => { - fetchSpy = jest.spyOn(global, 'fetch'); - }); - - afterEach(() => { - fetchSpy.mockRestore(); + mockHttpGet.mockReset(); + mockToApiErrorMessage.mockClear(); + jest.mocked(createApiHttpClient).mockClear(); }); it('returns an error when apiBaseUrl is not configured', async () => { @@ -40,7 +50,7 @@ describe('TrustFlowEscrowClient.getGigs', () => { it('returns an empty first page when no gigs exist', async () => { const page = makePage(); - fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify(page), { status: 200 })); + mockHttpGet.mockResolvedValueOnce({ data: page }); const client = new TrustFlowEscrowClient({ ...BASE_CONTRACT_CONFIG, apiBaseUrl: API_BASE, apiKey: API_KEY }); const result = await client.getGigs({ limit: 20 }); @@ -55,7 +65,7 @@ describe('TrustFlowEscrowClient.getGigs', () => { it('sends cursor, limit, status, depositor and beneficiary as query params', async () => { const page = makePage(); - fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify(page), { status: 200 })); + mockHttpGet.mockResolvedValueOnce({ data: page }); const client = new TrustFlowEscrowClient({ ...BASE_CONTRACT_CONFIG, apiBaseUrl: API_BASE }); await client.getGigs({ @@ -66,32 +76,33 @@ describe('TrustFlowEscrowClient.getGigs', () => { beneficiary: 'G' + 'B'.repeat(55), }); - const calledUrl = new URL((fetchSpy.mock.calls[0][0] as string)); - expect(calledUrl.searchParams.get('cursor')).toBe('abc123'); - expect(calledUrl.searchParams.get('limit')).toBe('10'); - expect(calledUrl.searchParams.get('status')).toBe('active'); - expect(calledUrl.searchParams.get('depositor')).toBe('G' + 'A'.repeat(55)); - expect(calledUrl.searchParams.get('beneficiary')).toBe('G' + 'B'.repeat(55)); + const [_path, options] = mockHttpGet.mock.calls[0] as [string, { params: Record }]; + expect(options.params.cursor).toBe('abc123'); + expect(options.params.limit).toBe('10'); + expect(options.params.status).toBe('active'); + expect(options.params.depositor).toBe('G' + 'A'.repeat(55)); + expect(options.params.beneficiary).toBe('G' + 'B'.repeat(55)); }); it('caps limit at 100 regardless of what the caller passes', async () => { - fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify(makePage()), { status: 200 })); + mockHttpGet.mockResolvedValueOnce({ data: makePage() }); const client = new TrustFlowEscrowClient({ ...BASE_CONTRACT_CONFIG, apiBaseUrl: API_BASE }); await client.getGigs({ limit: 500 }); - const calledUrl = new URL((fetchSpy.mock.calls[0][0] as string)); - expect(calledUrl.searchParams.get('limit')).toBe('100'); + const [_path, options] = mockHttpGet.mock.calls[0] as [string, { params: Record }]; + expect(options.params.limit).toBe('100'); }); - it('sends Authorization header when apiKey is configured', async () => { - fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify(makePage()), { status: 200 })); + it('passes apiKey into shared API client creation', async () => { + mockHttpGet.mockResolvedValueOnce({ data: makePage() }); const client = new TrustFlowEscrowClient({ ...BASE_CONTRACT_CONFIG, apiBaseUrl: API_BASE, apiKey: API_KEY }); await client.getGigs(); - const headers = fetchSpy.mock.calls[0][1]?.headers as Record; - expect(headers['Authorization']).toBe(`Bearer ${API_KEY}`); + expect(jest.mocked(createApiHttpClient)).toHaveBeenCalledWith( + expect.objectContaining({ baseURL: API_BASE, apiKey: API_KEY }), + ); }); it('traverses multiple pages using nextCursor', async () => { @@ -107,9 +118,9 @@ describe('TrustFlowEscrowClient.getGigs', () => { hasMore: false, }; - fetchSpy - .mockResolvedValueOnce(new Response(JSON.stringify(page1), { status: 200 })) - .mockResolvedValueOnce(new Response(JSON.stringify(page2), { status: 200 })); + mockHttpGet + .mockResolvedValueOnce({ data: page1 }) + .mockResolvedValueOnce({ data: page2 }); const client = new TrustFlowEscrowClient({ ...BASE_CONTRACT_CONFIG, apiBaseUrl: API_BASE }); @@ -123,23 +134,23 @@ describe('TrustFlowEscrowClient.getGigs', () => { if (!result2.ok) return; expect(result2.data.nextCursor).toBeNull(); expect(result2.data.hasMore).toBe(false); - expect(fetchSpy).toHaveBeenCalledTimes(2); + expect(mockHttpGet).toHaveBeenCalledTimes(2); }); - it('returns an error on non-2xx HTTP response', async () => { - fetchSpy.mockResolvedValueOnce(new Response('Unauthorized', { status: 401, statusText: 'Unauthorized' })); + it('returns mapped HTTP errors from transport helper', async () => { + mockHttpGet.mockRejectedValueOnce(new Error('HTTP 401: Unauthorized')); const client = new TrustFlowEscrowClient({ ...BASE_CONTRACT_CONFIG, apiBaseUrl: API_BASE }); const result = await client.getGigs(); expect(result.ok).toBe(false); if (!result.ok) { - expect(result.error).toMatch(/401/); + expect(result.error).toMatch(/Network error/); } }); - it('returns a network error when fetch throws', async () => { - fetchSpy.mockRejectedValueOnce(new TypeError('Failed to fetch')); + it('returns a network error when transport throws', async () => { + mockHttpGet.mockRejectedValueOnce(new TypeError('Failed to fetch')); const client = new TrustFlowEscrowClient({ ...BASE_CONTRACT_CONFIG, apiBaseUrl: API_BASE }); const result = await client.getGigs(); @@ -149,4 +160,15 @@ describe('TrustFlowEscrowClient.getGigs', () => { expect(result.error).toMatch(/Network error/); } }); + + it('creates API client with endpoint base URL', async () => { + mockHttpGet.mockResolvedValueOnce({ data: makePage() }); + + const client = new TrustFlowEscrowClient({ ...BASE_CONTRACT_CONFIG, apiBaseUrl: API_BASE }); + await client.getGigs(); + + expect(jest.mocked(createApiHttpClient)).toHaveBeenCalledWith( + expect.objectContaining({ baseURL: API_BASE }), + ); + }); }); diff --git a/tests/http-retry.test.ts b/tests/http-retry.test.ts new file mode 100644 index 0000000..bf4298b --- /dev/null +++ b/tests/http-retry.test.ts @@ -0,0 +1,121 @@ +import axios, { AxiosError } from 'axios'; +import axiosRetry from 'axios-retry'; +import { createApiHttpClient, toApiErrorMessage } from '../src/utils/http'; + +jest.mock('axios', () => ({ + __esModule: true, + default: { + create: jest.fn(), + }, + AxiosError: class MockAxiosError extends Error { + response?: { status?: number; statusText?: string }; + + constructor(message: string, response?: { status?: number; statusText?: string }) { + super(message); + this.response = response; + } + }, +})); + +jest.mock('axios-retry', () => { + const retryFn = jest.fn(); + (retryFn as any).isNetworkOrIdempotentRequestError = jest.fn(); + return { + __esModule: true, + default: retryFn, + }; +}); + +describe('createApiHttpClient', () => { + const mockedAxios = axios as unknown as { create: jest.Mock }; + const mockedAxiosRetry = axiosRetry as unknown as jest.Mock & { + isNetworkOrIdempotentRequestError: jest.Mock; + }; + + beforeEach(() => { + mockedAxios.create.mockReset(); + mockedAxiosRetry.mockReset(); + mockedAxiosRetry.isNetworkOrIdempotentRequestError.mockReset(); + }); + + it('configures axios instance with default headers and timeout', () => { + const instance = { get: jest.fn() }; + mockedAxios.create.mockReturnValue(instance); + + const result = createApiHttpClient({ baseURL: 'https://api.trustflow.xyz', apiKey: 'token' }); + + expect(result).toBe(instance); + expect(mockedAxios.create).toHaveBeenCalledWith( + expect.objectContaining({ + baseURL: 'https://api.trustflow.xyz', + timeout: 10_000, + headers: expect.objectContaining({ + 'Content-Type': 'application/json', + Authorization: 'Bearer token', + }), + }), + ); + }); + + it('retries on 429 and 5xx responses', () => { + mockedAxios.create.mockReturnValue({}); + mockedAxiosRetry.isNetworkOrIdempotentRequestError.mockReturnValue(false); + + createApiHttpClient({ baseURL: 'https://api.trustflow.xyz' }); + + const retryOptions = mockedAxiosRetry.mock.calls[0][1] as { + retryCondition: (error: { response?: { status?: number } }) => boolean; + }; + + expect(retryOptions.retryCondition({ response: { status: 429 } })).toBe(true); + expect(retryOptions.retryCondition({ response: { status: 503 } })).toBe(true); + expect(retryOptions.retryCondition({ response: { status: 404 } })).toBe(false); + }); + + it('falls back to network/idempotent retry detection for non-http errors', () => { + mockedAxios.create.mockReturnValue({}); + mockedAxiosRetry.isNetworkOrIdempotentRequestError.mockReturnValue(true); + + createApiHttpClient({ baseURL: 'https://api.trustflow.xyz' }); + + const retryOptions = mockedAxiosRetry.mock.calls[0][1] as { + retryCondition: (error: Record) => boolean; + }; + + expect(retryOptions.retryCondition({ code: 'ECONNRESET' })).toBe(true); + expect(mockedAxiosRetry.isNetworkOrIdempotentRequestError).toHaveBeenCalled(); + }); + + it('uses exponential retry delay with max cap', () => { + mockedAxios.create.mockReturnValue({}); + + createApiHttpClient({ + baseURL: 'https://api.trustflow.xyz', + retry: { retryDelayMs: 100, maxRetryDelayMs: 250 }, + }); + + const retryOptions = mockedAxiosRetry.mock.calls[0][1] as { + retryDelay: (retryCount: number) => number; + }; + + expect(retryOptions.retryDelay(1)).toBe(100); + expect(retryOptions.retryDelay(2)).toBe(200); + expect(retryOptions.retryDelay(3)).toBe(250); + }); +}); + +describe('toApiErrorMessage', () => { + it('formats axios errors with status', () => { + const error = new (AxiosError as unknown as new ( + message: string, + response?: { status?: number; statusText?: string }, + ) => Error)('failed', { status: 503, statusText: 'Service Unavailable' }); + + expect(toApiErrorMessage(error)).toBe('HTTP 503: Service Unavailable'); + }); + + it('formats network errors and unknown values', () => { + expect(toApiErrorMessage(new Error('timeout'))).toBe('Network error: timeout'); + expect(toApiErrorMessage('boom')).toBe('Network error: boom'); + }); +}); diff --git a/tests/xdr-payloads.test.ts b/tests/xdr-payloads.test.ts new file mode 100644 index 0000000..7a1b41e --- /dev/null +++ b/tests/xdr-payloads.test.ts @@ -0,0 +1,39 @@ +import { Keypair, xdr } from '@stellar/stellar-sdk'; +import { buildCreateEscrowArgs, buildReleaseArgs, buildDisputeArgs } from '../src/contract/build'; + +const ADDR_A = Keypair.random().publicKey(); +const ADDR_B = Keypair.random().publicKey(); + +function expectValidScValPayload(value: unknown): void { + const scVal = value as xdr.ScVal; + const encoded = scVal.toXDR(); + expect(() => xdr.ScVal.fromXDR(encoded)).not.toThrow(); +} + +describe('contract argument XDR payloads', () => { + it('buildCreateEscrowArgs returns XDR-decodable ScVal values', () => { + const args = buildCreateEscrowArgs({ + sender: ADDR_A, + recipient: ADDR_B, + amountStroops: 1_000_000n, + durationBlocks: 42, + }); + + expect(args).toHaveLength(4); + args.forEach(expectValidScValPayload); + }); + + it('buildReleaseArgs returns XDR-decodable ScVal values', () => { + const args = buildReleaseArgs('escrow-1', ADDR_A); + + expect(args).toHaveLength(2); + args.forEach(expectValidScValPayload); + }); + + it('buildDisputeArgs returns XDR-decodable ScVal values', () => { + const args = buildDisputeArgs('escrow-1', 'work quality dispute'); + + expect(args).toHaveLength(2); + args.forEach(expectValidScValPayload); + }); +});