Title
[Gasless] Standardize fallback pattern for sponsored→user_pays transaction failures
Body
## Description
The Starkzap SDK supports gas sponsorship via `feeMode: "sponsored"` but provides no standard
error detection or fallback pattern when sponsorship fails. Each developer must reinvent error
handling, leading to inconsistent UX across applications.
## Problem
When a transaction with `feeMode: "sponsored"` fails (e.g., custom token not whitelisted on testnet,
or policy limit exceeded), the SDK throws a generic error. Developers must:
1. Parse error messages as strings to detect sponsorship failures
2. Manually retry with `feeMode: "user_pays"`
3. Communicate to users whether they must pay gas or not
### Impact
- **DX:** Developers waste time on boilerplate error handling
- **UX:** Inconsistent user messaging across apps ("Fee required" vs. "Transaction failed")
- **Reliability:** String-based error parsing is fragile and breaks with SDK updates
## Expected Behavior
The SDK should provide:
1. **Structured error types:**
```typescript
class SponsorshipNotAvailableError extends Error {
reason: "not_whitelisted" | "policy_exceeded" | "network_unavailable";
fallbackFeeRequired: boolean; // true if user must pay gas
}
-
Automatic fallback option:
const tx = await wallet.transfer(token, [...], {
feeMode: "sponsored",
fallbackTo: "user_pays", // Auto-retry if sponsorship fails
onFallback: (error) => console.log("User pays gas:", error.reason),
});
-
Clear documentation with examples for each use case (custom tokens, multiple networks, etc.)
Reproduction Steps
- Deploy a custom ERC20 token on Sepolia testnet
- Create a Cartridge policy allowing transfer of that token
- Attempt transfer with
feeMode: "sponsored"
- Observe: Generic error, no structured way to detect sponsorship failure
- Must retry manually with
feeMode: "user_pays" and parse error string
Code Example
// Current (fragile, error-prone)
try {
const tx = await wallet.transfer(customToken, [...], { feeMode: "sponsored" });
} catch (err) {
if (err.message.includes("not in policies") || err.message.includes("sponsored")) {
// Hope this pattern is stable...
return wallet.transfer(customToken, [...], { feeMode: "user_pays" });
}
throw err;
}
// Desired (clean, maintainable)
try {
const tx = await wallet.transfer(customToken, [...], {
feeMode: "sponsored",
fallbackTo: "user_pays",
});
} catch (err) {
if (err instanceof SponsorshipNotAvailableError) {
showUserMessage(`Fee will be deducted (reason: ${err.reason})`);
}
throw err;
}
Environment
- Starkzap SDK Version: Latest
- Network: Sepolia testnet (primary issue), Mainnet (secondary)
- Use case: StarkSplit bill-splitting app with custom token transfers
Real-World Impact
StarkSplit uses this pattern in lib/starkzap.ts::transferCustomToken(). Any app supporting
custom tokens on testnet or non-whitelisted networks will hit this.
Suggested Solution
- Add
SponsorshipNotAvailableError with reason and fallbackFeeRequired fields
- Add optional
fallbackTo parameter to transfer options
- Document with examples for testnet + custom tokens scenario
- (Optional) Consider wallet provider standardization (WalletConnect, ethers.js patterns)
Title
Body
Automatic fallback option:
Clear documentation with examples for each use case (custom tokens, multiple networks, etc.)
Reproduction Steps
feeMode: "sponsored"feeMode: "user_pays"and parse error stringCode Example
Environment
Real-World Impact
StarkSplit uses this pattern in
lib/starkzap.ts::transferCustomToken(). Any app supportingcustom tokens on testnet or non-whitelisted networks will hit this.
Suggested Solution
SponsorshipNotAvailableErrorwithreasonandfallbackFeeRequiredfieldsfallbackToparameter to transfer options