feat(bridge): humanize XRP engine_result errors for SDK + UI

broadcast_driver.go::humanizeBroadcastErr now recognizes the most
common XRP engine_result codes and renders them as one-line user-
facing messages, matching the EVM-side humanization pattern:

  tecINSUFFICIENT_FUNDS / tecUNFUNDED → "Insufficient funds in release
                                          address — fund the MPC address"
  tecNO_DST                           → "XRP destination account is
                                          not activated — recipient must
                                          hold ≥10 XRP reserve"
  tefPAST_SEQ                         → "XRP sequence already used —
                                          bridge will requery and retry"
  tem*                                → "XRP transaction was malformed
                                          — bridge will retry"
  telINSUF_FEE_P                      → "XRP network fee too low for
                                          current load — bridge will retry"

The SDK + UI surface these from Swap.LastError so users see the
actionable cause (fund the wallet, activate the destination, etc.)
instead of the raw rippled error string.

Test: TestHumanizeBroadcastErr_XRPEngineResults covers all five new
mappings plus regression for the existing EVM mappings.
This commit is contained in:
2026-05-27 18:15:03 -07:00
parent df4055fc25
commit 75c8c1bb78
2 changed files with 56 additions and 1 deletions
+11 -1
View File
@@ -291,12 +291,22 @@ func humanizeBroadcastErr(err error) string {
msg := err.Error()
low := strings.ToLower(msg)
switch {
case strings.Contains(low, "insufficient funds"):
case strings.Contains(low, "insufficient funds"),
strings.Contains(msg, "tecINSUFFICIENT_FUNDS"),
strings.Contains(msg, "tecUNFUNDED"):
return "Insufficient funds in release address — fund the MPC address with destination-chain gas tokens"
case strings.Contains(low, "nonce too low"):
return "Nonce stale — release address already has a tx with this nonce; bridge will catch up"
case strings.Contains(low, "invalid sender"):
return "Destination chain rejected the signature (invalid sender) — possible MPC pubkey mismatch"
case strings.Contains(msg, "tecNO_DST"):
return "XRP destination account is not activated — recipient must hold ≥10 XRP reserve"
case strings.Contains(msg, "tefPAST_SEQ"):
return "XRP sequence already used — bridge will requery account_info and retry"
case strings.Contains(msg, "temBAD_AMOUNT"), strings.Contains(msg, "tem"):
return "XRP transaction was malformed — bridge will retry with a fresh build"
case strings.Contains(msg, "telINSUF_FEE_P"):
return "XRP network fee too low for current load — bridge will retry with a higher fee"
case strings.Contains(low, "http 502"), strings.Contains(low, "http 503"), strings.Contains(low, "http 504"):
return "Destination RPC unreachable (gateway error) — retrying"
case strings.Contains(low, "context deadline exceeded"), strings.Contains(low, "timeout"):
+45
View File
@@ -530,3 +530,48 @@ func TestEndToEnd_AllDriversChained(t *testing.T) {
t.Errorf("DestTxHash = %q, want 0xfinal-tx-hash-e2e", got.DestTxHash)
}
}
// =============================================================================
// humanizeBroadcastErr — XRP engine_result mapping
// =============================================================================
func TestHumanizeBroadcastErr_XRPEngineResults(t *testing.T) {
cases := []struct {
in string
want string
}{
// XRP-specific mappings.
{"submit rpc 0: fatal engine_result=tecINSUFFICIENT_FUNDS: insufficient", "Insufficient funds in release address"},
{"submit rpc 0: fatal engine_result=tecNO_DST: destination not found", "destination account is not activated"},
{"submit rpc 0: fatal engine_result=tefPAST_SEQ: sequence already used", "sequence already used"},
{"submit rpc 0: fatal engine_result=temBAD_AMOUNT: bad amount", "malformed"},
{"submit rpc 0: retryable engine_result=telINSUF_FEE_P: fee too low", "fee too low for current load"},
// Existing EVM behaviour preserved.
{"insufficient funds for gas * price + value", "Insufficient funds in release address"},
{"nonce too low", "Nonce stale"},
{"HTTP 503: gateway", "Destination RPC unreachable"},
}
for _, c := range cases {
got := humanizeBroadcastErr(stringErr(c.in))
if !contains(got, c.want) {
t.Errorf("humanizeBroadcastErr(%q) = %q\n want substring %q", c.in, got, c.want)
}
}
}
func contains(haystack, needle string) bool {
return haystack != "" && (haystack == needle || (len(needle) > 0 && stringIndex(haystack, needle) >= 0))
}
func stringIndex(s, sub string) int {
for i := 0; i+len(sub) <= len(s); i++ {
if s[i:i+len(sub)] == sub {
return i
}
}
return -1
}
type stringErr string
func (s stringErr) Error() string { return string(s) }