diff --git a/package.json b/package.json index aa6112b..8a7e6ca 100644 --- a/package.json +++ b/package.json @@ -48,8 +48,6 @@ }, "devDependencies": { "@stellar/stellar-sdk": "^13.3.0", - "@typescript-eslint/eslint-plugin": "^7.0.0", - "@typescript-eslint/parser": "^7.0.0", "@types/node": "^20.0.0", "@typescript-eslint/parser": "^8.65.0", "eslint": "^8.57.0", diff --git a/src/account/didAssociation.ts b/src/account/didAssociation.ts index 0e298bc..6b968bc 100644 --- a/src/account/didAssociation.ts +++ b/src/account/didAssociation.ts @@ -205,7 +205,7 @@ export async function verifyDidOwnership( } if (!document) { - return { verified: false, reason: `DID could not be resolved: "${did}".`, document: undefined }; + return { verified: false, reason: `DID could not be resolved: "${did}".` }; } if (isExpired(document)) { diff --git a/src/account/getAccount.ts b/src/account/getAccount.ts index 597c04b..604eae3 100644 --- a/src/account/getAccount.ts +++ b/src/account/getAccount.ts @@ -9,11 +9,9 @@ import { CircuitBreakerRegistry } from "../network/circuitBreaker"; // Shared circuit breaker registry for Horizon operations const horizonCircuitBreaker = new CircuitBreakerRegistry({ - requestWindow: 10, - failureRateThreshold: 0.5, + failureThreshold: 5, recoveryWindowMs: 30_000, }); -import { CircuitBreakerRegistry } from "../network/circuitBreaker"; /** * Fetch full account details including all balances from Horizon. @@ -51,10 +49,6 @@ export function getAccount( }); }); - if (account.status === "error") { - return account; - } - const balances: AssetBalance[] = account.balances.map((b) => { // Note: parseFloat is used here for convenience/backward compatibility. // IEEE-754 doubles can represent integers up to ~9e15 exactly, and diff --git a/src/network/circuitBreaker.ts b/src/network/circuitBreaker.ts index 61f2758..67bcf12 100644 --- a/src/network/circuitBreaker.ts +++ b/src/network/circuitBreaker.ts @@ -5,8 +5,8 @@ * called immediately (fail-fast) instead of retrying for 30+ seconds. * * State machine: - * CLOSED ──(50% failure rate across 10 requests)──▶ OPEN - * OPEN ──(30 s recovery window)───▶ HALF_OPEN + * CLOSED ──(5 consecutive transient failures)──▶ OPEN + * OPEN ──(60 s recovery window)───▶ HALF_OPEN * HALF_OPEN ──(probe succeeds)─────▶ CLOSED * HALF_OPEN ──(probe fails)────────▶ OPEN * @@ -15,8 +15,6 @@ */ import { isTransientError } from "../shared/errors"; -import { err, SorokitErrorCode } from "../shared/response"; -import type { SorokitResult } from "../shared/response"; // ─── Public types ───────────────────────────────────────────────────────────── @@ -24,18 +22,13 @@ export type CircuitState = "CLOSED" | "OPEN" | "HALF_OPEN"; export interface CircuitBreakerConfig { /** - * Number of requests to track for failure rate calculation. - * @default 10 + * Number of consecutive transient failures required to trip the circuit. + * @default 5 */ - requestWindow?: number; - /** - * Failure rate threshold (0-1) that trips the circuit. - * @default 0.5 (50%) - */ - failureRateThreshold?: number; + failureThreshold?: number; /** * Milliseconds to wait in OPEN state before transitioning to HALF_OPEN. - * @default 30_000 + * @default 60_000 */ recoveryWindowMs?: number; /** @@ -58,10 +51,6 @@ export interface CircuitBreakerMetrics { consecutiveFailures: number; totalFailures: number; totalSuccesses: number; - /** Number of requests in the current window. */ - requestCount: number; - /** Current failure rate in the window (0-1). */ - failureRate: number; /** Epoch ms when the circuit was last opened, or null if never opened. */ lastOpenedAt: number | null; /** Epoch ms when the circuit last transitioned to any state. */ @@ -86,9 +75,8 @@ export class CircuitOpenError extends Error { // ─── Defaults ───────────────────────────────────────────────────────────────── -const DEFAULT_REQUEST_WINDOW = 10; -const DEFAULT_FAILURE_RATE_THRESHOLD = 0.5; -const DEFAULT_RECOVERY_WINDOW_MS = 30_000; +const DEFAULT_FAILURE_THRESHOLD = 5; +const DEFAULT_RECOVERY_WINDOW_MS = 60_000; // ─── Implementation ─────────────────────────────────────────────────────────── @@ -106,12 +94,10 @@ export class CircuitBreaker { private consecutiveFailures = 0; private totalFailures = 0; private totalSuccesses = 0; - private requestHistory: boolean[] = []; // true = success, false = failure private lastOpenedAt: number | null = null; private lastTransitionAt: number = Date.now(); - private readonly requestWindow: number; - private readonly failureRateThreshold: number; + private readonly failureThreshold: number; private readonly recoveryWindowMs: number; private readonly onStateChange: | ((event: CircuitStateChangeEvent) => void) @@ -121,10 +107,8 @@ export class CircuitBreaker { readonly endpoint: string, config: CircuitBreakerConfig = {}, ) { - this.requestWindow = - config.requestWindow ?? DEFAULT_REQUEST_WINDOW; - this.failureRateThreshold = - config.failureRateThreshold ?? DEFAULT_FAILURE_RATE_THRESHOLD; + this.failureThreshold = + config.failureThreshold ?? DEFAULT_FAILURE_THRESHOLD; this.recoveryWindowMs = config.recoveryWindowMs ?? DEFAULT_RECOVERY_WINDOW_MS; this.onStateChange = config.onStateChange ?? undefined; @@ -138,14 +122,11 @@ export class CircuitBreaker { } getMetrics(): CircuitBreakerMetrics { - const failureRate = this.calculateFailureRate(); return { state: this.currentState, consecutiveFailures: this.consecutiveFailures, totalFailures: this.totalFailures, totalSuccesses: this.totalSuccesses, - requestCount: this.requestHistory.length, - failureRate, lastOpenedAt: this.lastOpenedAt, lastTransitionAt: this.lastTransitionAt, }; @@ -157,24 +138,20 @@ export class CircuitBreaker { * Execute `fn` through the circuit breaker. * * - CLOSED: execute normally; track failures and successes. - * - OPEN: return SERVICE_UNAVAILABLE error immediately without calling `fn`. + * - OPEN: throw `CircuitOpenError` immediately without calling `fn`. * - HALF_OPEN: execute one probe; close on success, reopen on failure. */ - async call(fn: () => Promise): Promise> { + async call(fn: () => Promise): Promise { this.checkRecovery(); if (this.state === "OPEN") { - return err( - SorokitErrorCode.SERVICE_UNAVAILABLE, - `Circuit breaker OPEN for "${this.endpoint}" — failing fast. ` + - `Opened at ${new Date(this.lastOpenedAt!).toISOString()}.`, - ); + throw new CircuitOpenError(this.endpoint, this.lastOpenedAt!); } try { const result = await fn(); this.onSuccess(); - return { status: "ok", data: result }; + return result; } catch (error) { // Only transient errors (5xx, timeout, network) count as circuit failures. // Permanent errors (bad params, 404) pass through without tripping the circuit. @@ -191,31 +168,10 @@ export class CircuitBreaker { reset(): void { this.transition("CLOSED"); this.consecutiveFailures = 0; - this.requestHistory = []; } // ─── Private helpers ──────────────────────────────────────────────────────── - /** - * Calculate the current failure rate based on the request history window. - */ - private calculateFailureRate(): number { - if (this.requestHistory.length === 0) return 0; - const failures = this.requestHistory.filter((success) => !success).length; - return failures / this.requestHistory.length; - } - - /** - * Record a request result in the sliding window. - */ - private recordRequest(success: boolean): void { - this.requestHistory.push(success); - // Keep only the most recent requests within the window - if (this.requestHistory.length > this.requestWindow) { - this.requestHistory.shift(); - } - } - /** * If the circuit is OPEN and the recovery window has elapsed, * transition to HALF_OPEN to allow a probe request. @@ -232,12 +188,10 @@ export class CircuitBreaker { private onSuccess(): void { this.totalSuccesses += 1; - this.recordRequest(true); if (this.state === "HALF_OPEN") { // Probe succeeded — close the circuit this.consecutiveFailures = 0; - this.requestHistory = []; this.transition("CLOSED"); return; } @@ -249,7 +203,6 @@ export class CircuitBreaker { private onFailure(): void { this.totalFailures += 1; this.consecutiveFailures += 1; - this.recordRequest(false); if (this.state === "HALF_OPEN") { // Probe failed — reopen the circuit and restart the recovery clock @@ -260,8 +213,7 @@ export class CircuitBreaker { if ( this.state === "CLOSED" && - this.calculateFailureRate() >= this.failureRateThreshold && - this.requestHistory.length >= this.requestWindow + this.consecutiveFailures >= this.failureThreshold ) { this.lastOpenedAt = Date.now(); this.transition("OPEN"); @@ -317,7 +269,7 @@ export class CircuitBreakerRegistry { } /** Convenience wrapper: call `fn` through the breaker for `endpoint`. */ - async call(endpoint: string, fn: () => Promise): Promise> { + async call(endpoint: string, fn: () => Promise): Promise { return this.getBreakerFor(endpoint).call(fn); } diff --git a/src/network/fallback.ts b/src/network/fallback.ts index bf13271..f3d125f 100644 --- a/src/network/fallback.ts +++ b/src/network/fallback.ts @@ -1,4 +1,4 @@ -import { err, isErr, ok } from "../shared/response"; +import { err, isErr, ok, SorokitErrorCode } from "../shared/response"; import type { RecoveryAttempt, SorokitResult } from "../shared/response"; import { isTransientError, toMessage } from "../shared/errors"; @@ -72,20 +72,25 @@ export class EndpointFallbackManager { if (!isEligibleForFallback) { if (this._allowDegradedMode) { return { - ...result, + status: "error", + data: null, error: { - ...result.error, + code: result.error.code, + message: result.error.message, + cause: result.error.cause, recoveryAttempts, degradedMode: true, }, }; } return { - ...result, + status: "error", + data: null, error: { - ...result.error, - recoveryAttempts, + code: result.error.code, + message: result.error.message, cause: result.error.cause ?? lastCause, + recoveryAttempts, }, }; } @@ -107,15 +112,20 @@ export class EndpointFallbackManager { : "Network operation failed with no available endpoints."; const errorResult = err( - "NETWORK_ERROR" as any, + SorokitErrorCode.NETWORK_ERROR, finalErrorMessage, lastCause, ); + const errObj = isErr(errorResult) ? errorResult.error : { code: SorokitErrorCode.NETWORK_ERROR, message: finalErrorMessage, cause: lastCause }; + return { - ...errorResult, + status: "error", + data: null, error: { - ...errorResult.error, + code: errObj.code, + message: errObj.message, + cause: errObj.cause, recoveryAttempts, degradedMode: this._allowDegradedMode, }, diff --git a/src/soroban/callOptimization.ts b/src/soroban/callOptimization.ts index bf516d3..4ef0dec 100644 --- a/src/soroban/callOptimization.ts +++ b/src/soroban/callOptimization.ts @@ -221,15 +221,22 @@ export function analyzeCallOptimization( const priorityOrder: Record = { high: 0, medium: 1, low: 2 }; suggestions.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]); + const observedMetrics: { + feeStroops?: number; + cpuInstructions?: number; + memoryBytes?: number; + ledgerReads?: number; + ledgerWrites?: number; + } = {}; + if (feeStroops !== undefined) observedMetrics.feeStroops = feeStroops; + if (cpuInstructions !== undefined) observedMetrics.cpuInstructions = cpuInstructions; + if (memoryBytes !== undefined) observedMetrics.memoryBytes = memoryBytes; + if (ledgerReads !== undefined) observedMetrics.ledgerReads = ledgerReads; + if (ledgerWrites !== undefined) observedMetrics.ledgerWrites = ledgerWrites; + return { suggestions, dataAvailable: true, - observedMetrics: { - feeStroops, - cpuInstructions, - memoryBytes, - ledgerReads, - ledgerWrites, - }, + observedMetrics, }; } diff --git a/src/soroban/contractMetadata.ts b/src/soroban/contractMetadata.ts index 1da9c68..773b6bc 100644 --- a/src/soroban/contractMetadata.ts +++ b/src/soroban/contractMetadata.ts @@ -304,6 +304,13 @@ export async function fetchContractWasm( } } +export const contractMetadataInternals = { + parseContractMethodsFromWasm, + readContractSpecSection, + readWasmCustomSections, + fetchContractWasm, +}; + function specTypeToString(typeDef: unknown): string { const kind = xdrName(call(typeDef, "switch")); const normalized = kind @@ -464,7 +471,7 @@ export async function getContractMethods( return inFlightRequests.get(cacheKey)!; } - const promise = (async () => { + const promise = (async (): Promise> => { try { const wasmResult = await contractMetadataInternals.fetchContractWasm(rpcUrl, contractId); if (wasmResult.status === "error") return wasmResult; @@ -561,36 +568,64 @@ export function validateContractArgs( method: ContractMethod, args: xdr.ScVal[], errorCode: SorokitErrorCode, +): SorokitResult; +export function validateContractArgs( + schema: ContractSchema, + method: string, + argCount: number, +): SorokitResult; +export function validateContractArgs( + methodOrSchema: ContractMethod | ContractSchema, + argsOrMethod: xdr.ScVal[] | string, + errorCodeOrArgCount?: SorokitErrorCode | number, ): SorokitResult { - for (let i = 0; i < args.length; i++) { - const input = method.inputs[i]; - const arg = args[i]; - if (!input || !arg) continue; - - const scvName: string = arg.switch().name; - const actualType = SCV_TO_ABI_TYPE[scvName] ?? scvName; - const expectedType = input.type; - - // Allow vec/map/option/result/tuple as prefix matches (e.g. "vec
") - const expectedBase = expectedType.split("<")[0]; - if (actualType !== expectedBase && actualType !== expectedType) { - return err( - errorCode, - `Argument "${input.name}" (position ${i}): expected type "${expectedType}", got "${actualType}"`, - ); + if ("inputs" in methodOrSchema && Array.isArray(argsOrMethod)) { + const method = methodOrSchema; + const args = argsOrMethod; + const errorCode = errorCodeOrArgCount as SorokitErrorCode; + for (let i = 0; i < args.length; i++) { + const input = method.inputs[i]; + const arg = args[i]; + if (!input || !arg) continue; + + const scvName: string = arg.switch().name; + const actualType = SCV_TO_ABI_TYPE[scvName] ?? scvName; + const expectedType = input.type; + + // Allow vec/map/option/result/tuple as prefix matches (e.g. "vec
") + const expectedBase = expectedType.split("<")[0]; + if (actualType !== expectedBase && actualType !== expectedType) { + return err( + errorCode, + `Argument "${input.name}" (position ${i}): expected type "${expectedType}", got "${actualType}"`, + ); + } } + + return ok(undefined); + } + + const schema = methodOrSchema as ContractSchema; + const method = argsOrMethod as string; + const argCount = errorCodeOrArgCount as number; + const methodSchema = schema.methods.find((m) => m.name === method); + if (!methodSchema) { + return err( + SorokitErrorCode.CONTRACT_PREPARE_FAILED, + `Method "${method}" not found in schema for contract ${schema.contractId}. Available: ${schema.methods.map((m) => m.name).join(", ")}`, + ); + } + + if (methodSchema.params.length !== argCount) { + return err( + SorokitErrorCode.CONTRACT_PREPARE_FAILED, + `Method "${method}" expects ${methodSchema.params.length} argument(s) [${methodSchema.params.map((p) => `${p.name}: ${p.type}`).join(", ")}], but ${argCount} were provided.`, + ); } return ok(undefined); } -export const contractMetadataInternals = { - parseContractMethodsFromWasm, - readContractSpecSection, - readWasmCustomSections, - fetchContractWasm, -}; - /** * Fetch, parse, and cache the typed ABI schema for a contract. * @@ -652,42 +687,3 @@ function isContractSchema(value: unknown): value is ContractSchema { const s = value as Partial; return typeof s.contractId === "string" && Array.isArray(s.methods); } - -/** - * Validate user-supplied arguments against a parsed `ContractMethodSchema`. - * - * Checks: - * - the method exists in the schema - * - the number of provided ScVal arguments matches the expected param count - * - * Returns `ok(void)` when valid, or a `CONTRACT_PREPARE_FAILED` error - * describing the mismatch. - * - * @param schema - Schema returned by `parseContractSchema`. - * @param method - Name of the method to validate against. - * @param argCount - Number of arguments the caller intends to pass. - * - * (issue #206) - */ -export function validateContractArgs( - schema: ContractSchema, - method: string, - argCount: number, -): SorokitResult { - const methodSchema = schema.methods.find((m) => m.name === method); - if (!methodSchema) { - return err( - SorokitErrorCode.CONTRACT_PREPARE_FAILED, - `Method "${method}" not found in schema for contract ${schema.contractId}. Available: ${schema.methods.map((m) => m.name).join(", ")}`, - ); - } - - if (methodSchema.params.length !== argCount) { - return err( - SorokitErrorCode.CONTRACT_PREPARE_FAILED, - `Method "${method}" expects ${methodSchema.params.length} argument(s) [${methodSchema.params.map((p) => `${p.name}: ${p.type}`).join(", ")}], but ${argCount} were provided.`, - ); - } - - return ok(undefined); -} diff --git a/src/soroban/eventIndex.ts b/src/soroban/eventIndex.ts index 3187997..33f4cb7 100644 --- a/src/soroban/eventIndex.ts +++ b/src/soroban/eventIndex.ts @@ -136,8 +136,11 @@ export class InMemoryEventIndex { const slice = all.slice(startIdx, startIdx + limit); const nextCursor = startIdx + limit < total ? slice[slice.length - 1]?.id : undefined; - - return { events: slice, nextCursor, total }; + const result: IndexedEventQueryResult = { events: slice, total }; + if (nextCursor !== undefined) { + result.nextCursor = nextCursor; + } + return result; } /** Total number of events currently held in the index. */ diff --git a/src/soroban/prepareCall.ts b/src/soroban/prepareCall.ts index c221b17..654d65a 100644 --- a/src/soroban/prepareCall.ts +++ b/src/soroban/prepareCall.ts @@ -25,8 +25,7 @@ import { CircuitBreakerRegistry } from "../network/circuitBreaker"; // Shared circuit breaker registry for RPC operations const rpcCircuitBreaker = new CircuitBreakerRegistry({ - requestWindow: 10, - failureRateThreshold: 0.5, + failureThreshold: 5, recoveryWindowMs: 30_000, }); diff --git a/src/tests/integration/streaming.test.ts b/src/tests/integration/streaming.test.ts index 8095d62..f983bc2 100644 --- a/src/tests/integration/streaming.test.ts +++ b/src/tests/integration/streaming.test.ts @@ -73,7 +73,12 @@ describe("Integration: Account Streaming", () => { expect(events[1].balances[0].balance).toBe("5.0"); expect(onBalanceChangeSpy).toHaveBeenCalledOnce(); - expect(onBalanceChangeSpy).toHaveBeenCalledWith("XLM", "0.0", "5.0"); + expect(onBalanceChangeSpy).toHaveBeenCalledWith( + expect.objectContaining({ code: "XLM" }), + "0.0", + "5.0", + "5", + ); expect(onAlertSpy).toHaveBeenCalledOnce(); expect(onAlertSpy).toHaveBeenCalledWith( diff --git a/src/tests/transaction.test.ts b/src/tests/transaction.test.ts index 1b3dffb..04e2d62 100644 --- a/src/tests/transaction.test.ts +++ b/src/tests/transaction.test.ts @@ -7,7 +7,7 @@ import { afterEach, type SpyInstance, } from "vitest"; -import { Asset, Horizon, Account, Keypair, Networks, StrKey, FeeBumpTransaction, Operation } from "@stellar/stellar-sdk"; +import { Asset, Horizon, Account, Keypair, Networks, StrKey, FeeBumpTransaction, Operation, TransactionBuilder } from "@stellar/stellar-sdk"; import * as serverFactory from "../shared/serverFactory"; import { createHash } from "crypto"; import { @@ -131,8 +131,10 @@ vi.mock("@stellar/stellar-sdk", async (importOriginal) => { class MockTransactionBuilder { static fromXDR = mocks.fromXDR; memo?: unknown; + sourceAccount?: any; - constructor(_sourceAccount: unknown, _options: unknown) { + constructor(sourceAccount: unknown, _options: unknown) { + this.sourceAccount = sourceAccount; transactionBuilderInstances.push(this); } @@ -155,7 +157,14 @@ vi.mock("@stellar/stellar-sdk", async (importOriginal) => { build(...args: any[]) { const customBuild = mockBuild(...args); if (customBuild) return customBuild; - return { toXDR: () => MOCK_XDR }; + const source = typeof this.sourceAccount === "string" ? this.sourceAccount : (this.sourceAccount as any)?.accountId?.() ?? (this.sourceAccount as any)?.publicKey; + return { + source, + toXDR: () => MOCK_XDR, + sign: vi.fn(), + hash: () => Buffer.alloc(32), + signatures: [], + }; } } @@ -399,26 +408,17 @@ describe("memo builders (#114)", () => { describe("muxed account network passphrase detection (#381)", () => { it("detects network mismatch for regular G-address accounts", async () => { - const sourcePublicKey = "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWNA"; - const keypair = Keypair.fromSecret( - "SAAPQAMBGM7T4KLLH6EJIFRFSLEOTBGYHSCIG47ETBAMKBRF42C2J7OZ", - ); + const keypair = Keypair.random(); + const sourcePublicKey = keypair.publicKey(); - // Create a transaction signed for testnet - const testnetTx = new TransactionBuilder( - new Account(sourcePublicKey, "1"), - { fee: "100", networkPassphrase: Networks.TESTNET }, - ) - .addOperation(Operation.payment({ - destination: "GABBZAB7XBYRSX2NH6RQ5ZFAK3LWOO4SR7WKC6ANM5WFCZJDH6VTLTT", - asset: Asset.native(), - amount: "10", - })) - .setTimeout(30) - .build(); + const mockTx = { + source: sourcePublicKey, + hash: () => Buffer.alloc(32), + signatures: [{ hint: () => Buffer.from(keypair.rawPublicKey().slice(-4)), signature: () => Buffer.alloc(64) }], + }; + mocks.fromXDR.mockReturnValueOnce(mockTx); - testnetTx.sign(keypair); - const signedXdr = testnetTx.toXDR(); + const signedXdr = "AAAAAQAAAAA="; // Try to submit with mainnet passphrase const result = await submitTransaction( @@ -438,7 +438,8 @@ describe("muxed account network passphrase detection (#381)", () => { it("handles muxed M-address accounts by extracting inner G-address", async () => { // Test that the implementation can handle muxed addresses without crashing // by mocking a transaction with a muxed source - const sourcePublicKey = "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWNA"; + const keypair = Keypair.random(); + const sourcePublicKey = keypair.publicKey(); // Create a mock transaction with muxed source const mockTx = { @@ -468,17 +469,16 @@ describe("muxed account network passphrase detection (#381)", () => { }); it("allows transactions with correct network passphrase for regular accounts", async () => { - const sourcePublicKey = "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWNA"; - const keypair = Keypair.fromSecret( - "SAAPQAMBGM7T4KLLH6EJIFRFSLEOTBGYHSCIG47ETBAMKBRF42C2J7OZ", - ); + const keypair = Keypair.random(); + const sourcePublicKey = keypair.publicKey(); + const destPublicKey = Keypair.random().publicKey(); const testnetTx = new TransactionBuilder( new Account(sourcePublicKey, "1"), { fee: "100", networkPassphrase: Networks.TESTNET }, ) .addOperation(Operation.payment({ - destination: "GABBZAB7XBYRSX2NH6RQ5ZFAK3LWOO4SR7WKC6ANM5WFCZJDH6VTLTT", + destination: destPublicKey, asset: Asset.native(), amount: "10", })) @@ -505,17 +505,16 @@ describe("muxed account network passphrase detection (#381)", () => { }); it("handles invalid muxed addresses gracefully", async () => { - const sourcePublicKey = "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWNA"; - const keypair = Keypair.fromSecret( - "SAAPQAMBGM7T4KLLH6EJIFRFSLEOTBGYHSCIG47ETBAMKBRF42C2J7OZ", - ); + const keypair = Keypair.random(); + const sourcePublicKey = keypair.publicKey(); + const destPublicKey = Keypair.random().publicKey(); const testnetTx = new TransactionBuilder( new Account(sourcePublicKey, "1"), { fee: "100", networkPassphrase: Networks.TESTNET }, ) .addOperation(Operation.payment({ - destination: "GABBZAB7XBYRSX2NH6RQ5ZFAK3LWOO4SR7WKC6ANM5WFCZJDH6VTLTT", + destination: destPublicKey, asset: Asset.native(), amount: "10", })) diff --git a/src/transaction/submitTransaction.ts b/src/transaction/submitTransaction.ts index 2092f51..d44ceff 100644 --- a/src/transaction/submitTransaction.ts +++ b/src/transaction/submitTransaction.ts @@ -17,8 +17,7 @@ import { CircuitBreakerRegistry } from "../network/circuitBreaker"; // Shared circuit breaker registry for Horizon operations const horizonCircuitBreaker = new CircuitBreakerRegistry({ - requestWindow: 10, - failureRateThreshold: 0.5, + failureThreshold: 5, recoveryWindowMs: 30_000, }); @@ -53,7 +52,9 @@ function detectNetworkPassphraseMismatch( let sourceAccountId = source; if (source.startsWith("M")) { try { - sourceAccountId = StrKey.decodeEd25519PublicKey(source); + sourceAccountId = StrKey.encodeEd25519PublicKey( + StrKey.decodeMed25519PublicKey(source).subarray(0, 32), + ); } catch { // If muxed account decoding fails, fall back to Horizon validation return false; diff --git a/src/wallet/capabilities.ts b/src/wallet/capabilities.ts index cae98c6..2e51e20 100644 --- a/src/wallet/capabilities.ts +++ b/src/wallet/capabilities.ts @@ -54,19 +54,25 @@ function normalizeCapabilities( const byId = new Map(); for (const id of STANDARD_CAPABILITIES) { - byId.set(id, { + const cap: WalletCapability = { id, supported: false, source: "fallback", - description: CAPABILITY_DESCRIPTIONS[id], - }); + }; + const desc = CAPABILITY_DESCRIPTIONS[id]; + if (desc !== undefined) cap.description = desc; + byId.set(id, cap); } for (const capability of capabilities) { - byId.set(capability.id, { - ...capability, - description: capability.description ?? CAPABILITY_DESCRIPTIONS[capability.id], - }); + const desc = capability.description ?? CAPABILITY_DESCRIPTIONS[capability.id]; + const cap: WalletCapability = { + id: capability.id, + supported: capability.supported, + source: capability.source, + }; + if (desc !== undefined) cap.description = desc; + byId.set(capability.id, cap); } const normalized = Array.from(byId.values()); @@ -99,12 +105,16 @@ function fallbackCapabilities(adapter: WalletAdapter): WalletCapability[] { } } - return Array.from(supported).map((id) => ({ - id, - supported: true, - source: "fallback", - description: CAPABILITY_DESCRIPTIONS[id], - })); + return Array.from(supported).map((id) => { + const cap: WalletCapability = { + id, + supported: true, + source: "fallback", + }; + const desc = CAPABILITY_DESCRIPTIONS[id]; + if (desc !== undefined) cap.description = desc; + return cap; + }); } export function getWalletCapabilities(adapter: WalletAdapter): WalletCapabilities { diff --git a/src/wallet/index.ts b/src/wallet/index.ts index a46878a..f6fe4a7 100644 --- a/src/wallet/index.ts +++ b/src/wallet/index.ts @@ -24,10 +24,6 @@ export type { WalletCapabilityId, WalletCapabilitySource, WalletCapabilities, - WalletCapability, - WalletCapabilityId, - WalletCapabilitySource, - WalletCapabilities, } from "./types"; export { getSigningHistory, @@ -65,10 +61,6 @@ import type { WalletCapabilityId, WalletCapabilitySource, WalletCapabilities, - WalletCapability, - WalletCapabilityId, - WalletCapabilitySource, - WalletCapabilities, } from "./types"; import { WalletType } from "./types";