-
Notifications
You must be signed in to change notification settings - Fork 42
Map CleverVault contract error codes to typed errors in the vault client #116
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Bosun-Josh121
merged 2 commits into
clevercon-protocol:main
from
Tijesunimi004:feat/issue-89-vault-error-mapping
Aug 21, 2026
Merged
Changes from 1 commit
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,187 @@ | ||
| import { describe, it, expect } from 'vitest'; | ||
| import { readFileSync } from 'node:fs'; | ||
| import { fileURLToPath } from 'node:url'; | ||
| import path from 'node:path'; | ||
| import { xdr, Address, Keypair } from '@stellar/stellar-sdk'; | ||
| import { | ||
| VaultErrorCode, | ||
| VaultContractError, | ||
| extractContractErrorCode, | ||
| errorFromSimulation, | ||
| errorFromSendResponse, | ||
| errorFromFailedTransaction, | ||
| } from './vault-errors.js'; | ||
|
|
||
| const __dirname = path.dirname(fileURLToPath(import.meta.url)); | ||
| const CONTRACT_LIB_RS = path.join( | ||
| __dirname, | ||
| '..', | ||
| '..', | ||
| '..', | ||
| 'contracts', | ||
| 'agent-vault', | ||
| 'src', | ||
| 'lib.rs', | ||
| ); | ||
|
|
||
| function diagnosticEventWithContractCode(code: number): xdr.DiagnosticEvent { | ||
| const errorScVal = xdr.ScVal.scvError(xdr.ScError.sceContract(code)); | ||
| const body = new xdr.ContractEventBody( | ||
| 0, | ||
| new xdr.ContractEventV0({ topics: [], data: errorScVal }), | ||
| ); | ||
| const event = new xdr.ContractEvent({ | ||
| ext: new xdr.ExtensionPoint(0), | ||
| contractId: null, | ||
| type: xdr.ContractEventType.diagnostic(), | ||
| body, | ||
| }); | ||
| return new xdr.DiagnosticEvent({ inSuccessfulContractCall: false, event }); | ||
| } | ||
|
|
||
| describe('VaultErrorCode mirrors the Rust VaultError enum', () => { | ||
| it('matches every variant and discriminant in contracts/agent-vault/src/lib.rs exactly', () => { | ||
| const rustSrc = readFileSync(CONTRACT_LIB_RS, 'utf-8'); | ||
| const enumMatch = rustSrc.match(/pub enum VaultError\s*\{([\s\S]*?)\n\}/); | ||
| expect(enumMatch, 'could not find `pub enum VaultError { ... }` in lib.rs').not.toBeNull(); | ||
|
|
||
| const body = enumMatch![1]; | ||
| const variantPattern = /(\w+)\s*=\s*(\d+),/g; | ||
| const rustVariants: Record<string, number> = {}; | ||
| let match: RegExpExecArray | null; | ||
| while ((match = variantPattern.exec(body)) !== null) { | ||
| rustVariants[match[1]] = Number(match[2]); | ||
| } | ||
| expect(Object.keys(rustVariants).length).toBeGreaterThan(0); | ||
|
Comment on lines
+49
to
+55
|
||
|
|
||
| const tsVariants: Record<string, number> = {}; | ||
| for (const key of Object.keys(VaultErrorCode)) { | ||
| const value = VaultErrorCode[key as keyof typeof VaultErrorCode]; | ||
| if (typeof value === 'number') { | ||
| tsVariants[key] = value; | ||
| } | ||
| } | ||
|
|
||
| expect(tsVariants).toEqual(rustVariants); | ||
| }); | ||
| }); | ||
|
|
||
| describe('extractContractErrorCode', () => { | ||
| it('extracts the code from a diagnostic event carrying a scvError(sceContract)', () => { | ||
| const code = extractContractErrorCode({ | ||
| diagnosticEvents: [diagnosticEventWithContractCode(6)], | ||
| }); | ||
| expect(code).toBe(6); | ||
| }); | ||
|
|
||
| it('extracts the code from a simulation HostError message', () => { | ||
| const message = | ||
| 'HostError: Error(Contract, #9)\n\nEvent log (newest first):\n 0: [Diagnostic Event] ...'; | ||
| expect(extractContractErrorCode({ message })).toBe(9); | ||
| }); | ||
|
|
||
| it('returns null for a diagnostic event that is not a contract error', () => { | ||
| const nonErrorScVal = new Address(Keypair.random().publicKey()).toScVal(); | ||
| const body = new xdr.ContractEventBody( | ||
| 0, | ||
| new xdr.ContractEventV0({ topics: [], data: nonErrorScVal }), | ||
| ); | ||
| const event = new xdr.ContractEvent({ | ||
| ext: new xdr.ExtensionPoint(0), | ||
| contractId: null, | ||
| type: xdr.ContractEventType.contract(), | ||
| body, | ||
| }); | ||
| const diag = new xdr.DiagnosticEvent({ inSuccessfulContractCall: true, event }); | ||
| expect(extractContractErrorCode({ diagnosticEvents: [diag] })).toBeNull(); | ||
| }); | ||
|
|
||
| it('returns null for a non-contract failure (network/auth error text)', () => { | ||
| expect( | ||
| extractContractErrorCode({ message: 'HostError: Error(Auth, InvalidAction)' }), | ||
| ).toBeNull(); | ||
| expect(extractContractErrorCode({ message: 'fetch failed: ECONNREFUSED' })).toBeNull(); | ||
| expect(extractContractErrorCode({})).toBeNull(); | ||
| }); | ||
|
|
||
| it('prefers diagnostic events over the message when both are present', () => { | ||
| const code = extractContractErrorCode({ | ||
| message: 'HostError: Error(Contract, #1)', | ||
| diagnosticEvents: [diagnosticEventWithContractCode(2)], | ||
| }); | ||
| expect(code).toBe(2); | ||
| }); | ||
| }); | ||
|
|
||
| describe('VaultContractError', () => { | ||
| it('marks a known code with its variant name', () => { | ||
| const err = new VaultContractError(6, { some: 'raw' }); | ||
| expect(err.code).toBe(6); | ||
| expect(err.codeName).toBe('InsufficientAvailable'); | ||
| expect(err.known).toBe(true); | ||
| expect(err.raw).toEqual({ some: 'raw' }); | ||
| expect(err.message).toContain('InsufficientAvailable'); | ||
| }); | ||
|
|
||
| it('preserves and flags an unmapped/unknown code rather than swallowing it', () => { | ||
| const err = new VaultContractError(999, 'raw-value'); | ||
| expect(err.code).toBe(999); | ||
| expect(err.codeName).toBeUndefined(); | ||
| expect(err.known).toBe(false); | ||
| expect(err.raw).toBe('raw-value'); | ||
| expect(err.message).toContain('999'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('error builders', () => { | ||
| it('errorFromSimulation returns a VaultContractError for a contract revert', () => { | ||
| const sim = { error: 'HostError: Error(Contract, #18)', events: [] } as any; | ||
| const err = errorFromSimulation(sim); | ||
| expect(err).toBeInstanceOf(VaultContractError); | ||
| expect((err as VaultContractError).code).toBe(18); | ||
| expect((err as VaultContractError).codeName).toBe('TooManyActiveTasks'); | ||
| }); | ||
|
|
||
| it('errorFromSimulation returns a plain Error for a non-contract simulation failure', () => { | ||
| const sim = { error: 'HostError: Error(Auth, InvalidAction)', events: [] } as any; | ||
| const err = errorFromSimulation(sim); | ||
| expect(err).not.toBeInstanceOf(VaultContractError); | ||
| expect(err.message).toContain('Simulation failed'); | ||
| }); | ||
|
|
||
| it('errorFromSendResponse returns a VaultContractError when diagnostic events carry the code', () => { | ||
| const response = { | ||
| status: 'ERROR', | ||
| diagnosticEvents: [diagnosticEventWithContractCode(9)], | ||
| } as any; | ||
| const err = errorFromSendResponse(response); | ||
| expect(err).toBeInstanceOf(VaultContractError); | ||
| expect((err as VaultContractError).code).toBe(9); | ||
| expect((err as VaultContractError).codeName).toBe('TaskAlreadyCompleted'); | ||
| }); | ||
|
|
||
| it('errorFromSendResponse returns a plain Error when there is no contract code', () => { | ||
| const response = { status: 'ERROR', errorResult: { foo: 'bar' } } as any; | ||
| const err = errorFromSendResponse(response); | ||
| expect(err).not.toBeInstanceOf(VaultContractError); | ||
| expect(err.message).toContain('Send failed'); | ||
| }); | ||
|
|
||
| it('errorFromFailedTransaction returns a VaultContractError when diagnostic events carry the code', () => { | ||
| const result = { | ||
| status: 'FAILED', | ||
| diagnosticEventsXdr: [diagnosticEventWithContractCode(24)], | ||
| } as any; | ||
| const err = errorFromFailedTransaction('deadbeef', result); | ||
| expect(err).toBeInstanceOf(VaultContractError); | ||
| expect((err as VaultContractError).code).toBe(24); | ||
| expect((err as VaultContractError).codeName).toBe('ReleaseConflict'); | ||
| }); | ||
|
|
||
| it('errorFromFailedTransaction returns a plain Error when no contract code is present', () => { | ||
| const result = { status: 'FAILED' } as any; | ||
| const err = errorFromFailedTransaction('deadbeef', result); | ||
| expect(err).not.toBeInstanceOf(VaultContractError); | ||
| expect(err.message).toContain('deadbeef'); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Propagate typed contract errors from public vault methods.
signAndSubmitandcreateTasknow createVaultContractErrorinstances. The surrounding catches increateTask,releasePayment, andcompleteTasklog onlyerr.messageand returnnullorvoid. Callers cannot branch oncode,known, or inspectraw.Re-throw
VaultContractError, or return a discriminated failure result that preserves it. Keep the null fallback only for non-contract failures if that behavior is required.Also applies to: 236-243
🤖 Prompt for AI Agents