Skip to content

Commit 81be2e9

Browse files
committed
feat: add contract upgrade script and centralized Stellar error handling
1 parent b4753d6 commit 81be2e9

3 files changed

Lines changed: 230 additions & 2 deletions

File tree

contracts/rent-escrow/src/lib.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,8 @@ pub enum Error {
2121
DeadlineNotReached = 4,
2222
/// The sum of all roommate shares exceeds the total rent amount.
2323
ShareSumExceedsRent = 5,
24-
/// Roommate has no contributed balance to refund.
2524
/// No funds to refund for this roommate.
26-
NothingToRefund = 5,
25+
NothingToRefund = 6,
2726
}
2827

2928
/// Storage key definitions for persistent contract state.
@@ -392,6 +391,15 @@ impl RentEscrowContract {
392391

393392
Ok(())
394393
}
394+
395+
/// Upgrades the contract's WASM code. Only the landlord can call this.
396+
pub fn upgrade(env: Env, new_wasm_hash: soroban_sdk::BytesN<32>) -> Result<(), Error> {
397+
let landlord: Address = Self::get_landlord(env.clone());
398+
landlord.require_auth();
399+
400+
env.deployer().update_current_contract_wasm(new_wasm_hash);
401+
Ok(())
402+
}
395403
}
396404

397405
#[cfg(test)]

lib/stellar/errors.test.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { describe, it as test } from "node:test";
2+
import assert from "node:assert";
3+
import { translateStellarError, StellarErrorType } from "./errors.ts";
4+
5+
const expect = (actual) => ({
6+
toBe: (expected) => assert.strictEqual(actual, expected),
7+
toContain: (expected) => assert.ok(actual?.includes(expected) || actual?.message?.includes(expected)),
8+
});
9+
10+
describe("translateStellarError", () => {
11+
test("maps Soroban contract error code 1 to InvalidAmount", () => {
12+
const error = { code: 1 };
13+
const result = translateStellarError(error);
14+
expect(result.type).toBe(StellarErrorType.INVALID_AMOUNT);
15+
});
16+
17+
test("maps Soroban contract error code 2 to InsufficientFunding", () => {
18+
const error = { code: 2 };
19+
const result = translateStellarError(error);
20+
expect(result.type).toBe(StellarErrorType.INSUFFICIENT_FUNDING);
21+
});
22+
23+
test("maps Soroban contract error code 3 to Unauthorized", () => {
24+
const error = { code: 3 };
25+
const result = translateStellarError(error);
26+
expect(result.type).toBe(StellarErrorType.UNAUTHORIZED);
27+
});
28+
29+
test("maps Soroban contract error code 4 to DeadlineNotReached", () => {
30+
const error = { code: 4 };
31+
const result = translateStellarError(error);
32+
expect(result.type).toBe(StellarErrorType.DEADLINE_NOT_REACHED);
33+
});
34+
35+
test("maps Soroban contract error code 5 to ShareSumExceedsRent", () => {
36+
const error = { code: 5 };
37+
const result = translateStellarError(error);
38+
expect(result.type).toBe(StellarErrorType.SHARE_SUM_EXCEEDS_RENT);
39+
});
40+
41+
test("maps Soroban contract error code 6 to NothingToRefund", () => {
42+
const error = { code: 6 };
43+
const result = translateStellarError(error);
44+
expect(result.type).toBe(StellarErrorType.NOTHING_TO_REFUND);
45+
});
46+
47+
test("maps RPC timeout error message", () => {
48+
const error = { message: "Request Timeout" };
49+
const result = translateStellarError(error);
50+
expect(result.type).toBe(StellarErrorType.RPC_NETWORK_TIMEOUT);
51+
});
52+
53+
test("maps RPC connection error (node unavailable)", () => {
54+
const error = { message: "Failed to fetch node status" };
55+
const result = translateStellarError(error);
56+
expect(result.type).toBe(StellarErrorType.RPC_NODE_UNAVAILABLE);
57+
});
58+
59+
test("maps Freighter rejection message", () => {
60+
const error = "User declined transaction";
61+
const result = translateStellarError(error);
62+
expect(result.type).toBe(StellarErrorType.FREIGHTER_REJECTED);
63+
});
64+
65+
test("maps Freighter locked wallet message", () => {
66+
const error = { message: "Account is locked" };
67+
const result = translateStellarError(error);
68+
expect(result.type).toBe(StellarErrorType.FREIGHTER_LOCKED);
69+
});
70+
71+
test("handles unknown errors gracefully", () => {
72+
const error = { something: "went wrong" };
73+
const result = translateStellarError(error);
74+
expect(result.type).toBe(StellarErrorType.UNKNOWN);
75+
expect(result.message).toBe("An unexpected Stellar error occurred.");
76+
});
77+
78+
test("handles null error", () => {
79+
const result = translateStellarError(null);
80+
expect(result.type).toBe(StellarErrorType.UNKNOWN);
81+
});
82+
});

lib/stellar/errors.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/**
2+
* error: Stellar Error Handling & User Messaging
3+
* Centralized error handler that translates Stellar/Soroban errors into user-friendly messages.
4+
*/
5+
6+
export const StellarErrorType = {
7+
INVALID_AMOUNT: "InvalidAmount",
8+
INSUFFICIENT_FUNDING: "InsufficientFunding",
9+
UNAUTHORIZED: "Unauthorized",
10+
DEADLINE_NOT_REACHED: "DeadlineNotReached",
11+
SHARE_SUM_EXCEEDS_RENT: "ShareSumExceedsRent",
12+
NOTHING_TO_REFUND: "NothingToRefund",
13+
RPC_NETWORK_TIMEOUT: "RpcNetworkTimeout",
14+
RPC_NODE_UNAVAILABLE: "RpcNodeUnavailable",
15+
RPC_SIMULATION_FAILURE: "RpcSimulationFailure",
16+
FREIGHTER_REJECTED: "FreighterRejected",
17+
FREIGHTER_LOCKED: "FreighterLocked",
18+
FREIGHTER_NOT_INSTALLED: "FreighterNotInstalled",
19+
UNKNOWN: "Unknown",
20+
} as const;
21+
22+
export type StellarErrorType = (typeof StellarErrorType)[keyof typeof StellarErrorType];
23+
24+
export interface UserFriendlyError {
25+
type: StellarErrorType;
26+
message: string;
27+
guidance: string;
28+
}
29+
30+
const ERROR_MAPPINGS: Record<number | string, UserFriendlyError> = {
31+
// Soroban Contract Errors (matched by code or name)
32+
1: {
33+
type: StellarErrorType.INVALID_AMOUNT,
34+
message: "The provided amount is invalid.",
35+
guidance: "Please check the amount and try again. It must be a positive number and meet the minimum rent requirement.",
36+
},
37+
2: {
38+
type: StellarErrorType.INSUFFICIENT_FUNDING,
39+
message: "Insufficient funding in the escrow.",
40+
guidance: "The contract does not have enough tokens to complete this release. Ensure all roommates have contributed.",
41+
},
42+
3: {
43+
type: StellarErrorType.UNAUTHORIZED,
44+
message: "You are not authorized to perform this action.",
45+
guidance: "Ensure you are using the correct account (e.g., the landlord or a registered roommate).",
46+
},
47+
4: {
48+
type: StellarErrorType.DEADLINE_NOT_REACHED,
49+
message: "The deadline has not been reached yet.",
50+
guidance: "Refunds or certain actions are only available after the escrow deadline has passed.",
51+
},
52+
5: {
53+
type: StellarErrorType.SHARE_SUM_EXCEEDS_RENT,
54+
message: "Total roommate shares exceed the rent amount.",
55+
guidance: "The sum of all roommate obligations cannot be greater than the total rent. Please adjust the shares.",
56+
},
57+
6: {
58+
type: StellarErrorType.NOTHING_TO_REFUND,
59+
message: "No funds available to refund.",
60+
guidance: "This account has no contributed balance in the escrow to reclaim.",
61+
},
62+
63+
// RPC Errors
64+
"TIMEOUT": {
65+
type: StellarErrorType.RPC_NETWORK_TIMEOUT,
66+
message: "Network request timed out.",
67+
guidance: "The Stellar network is taking too long to respond. Please check your internet connection or try again in a few moments.",
68+
},
69+
"NODE_UNAVAILABLE": {
70+
type: StellarErrorType.RPC_NODE_UNAVAILABLE,
71+
message: "The Soroban RPC node is currently unavailable.",
72+
guidance: "We're having trouble connecting to the Stellar network. Please try again later.",
73+
},
74+
"SIMULATION_FAILURE": {
75+
type: StellarErrorType.RPC_SIMULATION_FAILURE,
76+
message: "Transaction simulation failed.",
77+
guidance: "The transaction would fail if submitted. This often happens if contract conditions aren't met.",
78+
},
79+
80+
// Freighter Errors
81+
"User declined": {
82+
type: StellarErrorType.FREIGHTER_REJECTED,
83+
message: "Transaction rejected in Freighter.",
84+
guidance: "You cancelled the transaction in your wallet. If this was a mistake, please try again and approve the request.",
85+
},
86+
"Wallet is locked": {
87+
type: StellarErrorType.FREIGHTER_LOCKED,
88+
message: "Freighter wallet is locked.",
89+
guidance: "Please unlock your Freighter extension and try again.",
90+
},
91+
"Freighter not found": {
92+
type: StellarErrorType.FREIGHTER_NOT_INSTALLED,
93+
message: "Freighter extension not found.",
94+
guidance: "Please install the Freighter wallet extension to interact with this application.",
95+
},
96+
};
97+
98+
/**
99+
* Translates a raw error from Stellar, Soroban, or Freighter into a user-friendly message.
100+
* @param error The raw error object or message string.
101+
*/
102+
export function translateStellarError(error: any): UserFriendlyError {
103+
let errorKey: string | number = "UNKNOWN";
104+
105+
if (typeof error === "string") {
106+
const msg = error.toLowerCase();
107+
if (msg.includes("declined") || msg.includes("reject")) errorKey = "User declined";
108+
else if (msg.includes("timeout")) errorKey = "TIMEOUT";
109+
else if (msg.includes("unavailable") || msg.includes("fetch")) errorKey = "NODE_UNAVAILABLE";
110+
else if (msg.includes("lock")) errorKey = "Wallet is locked";
111+
else errorKey = error;
112+
} else if (error && typeof error === "object") {
113+
// Handle Soroban contract error codes (e.g., from simulation or resultXdr)
114+
if (typeof error.code === "number") {
115+
errorKey = error.code;
116+
} else if (error.message) {
117+
// Handle known error message substrings
118+
const msg = error.message.toLowerCase();
119+
if (msg.includes("timeout")) errorKey = "TIMEOUT";
120+
else if (msg.includes("unavailable") || msg.includes("fetch")) errorKey = "NODE_UNAVAILABLE";
121+
else if (msg.includes("declined") || msg.includes("reject")) errorKey = "User declined";
122+
else if (msg.includes("lock")) errorKey = "Wallet is locked";
123+
else if (msg.includes("simulation")) errorKey = "SIMULATION_FAILURE";
124+
}
125+
}
126+
127+
const mapping = ERROR_MAPPINGS[errorKey];
128+
if (mapping) {
129+
return mapping;
130+
}
131+
132+
// Fallback for unmapped errors
133+
return {
134+
type: StellarErrorType.UNKNOWN,
135+
message: "An unexpected Stellar error occurred.",
136+
guidance: typeof error?.message === "string" ? error.message : "If the problem persists, please contact support with details of the action you were performing.",
137+
};
138+
}

0 commit comments

Comments
 (0)