Skip to content

requestAirdrop cannot detect an already-funded account: the guard matches a string friendbot never emits #1645

Description

@oceans404

Summary

Server.requestAirdrop() has a branch to return the existing account when friendbot reports it is already funded. That branch matches on "createAccountAlreadyExist", which friendbot does not return. The path has never been reachable, so the method throws instead.

It fires on the single most-travelled path in Stellar development, re-running setup, and the code that exists to handle it reads as "this case was handled" to anyone auditing the source.

Affected: 17.0.0-rc.1 and 16.2.0, identical code.

Reproduction

import { Server } from "@stellar/stellar-sdk/rpc";

const server = new Server("https://soroban-testnet.stellar.org");
// must hold MORE than the 10,000 XLM starting balance
const FUNDED = "GCMUNHA3NWUQBYRDL6ODYLDSCW3JSPX3PX2H6ONQAOOPAMI3JPQJ6VQQ";

try {
  const acct = await server.requestAirdrop(FUNDED);
  console.log("returned Account:", acct.accountId());
} catch (e: any) {
  console.log("THREW:", e.constructor.name, e.message);
  console.log("  detail:", JSON.stringify(e.response?.data?.detail));
}

The account must hold more than 10,000 XLM. Friendbot tops up an account below the starting balance rather than refusing, so a freshly friendbotted account will not reproduce this. That is why it is easy to miss.

Expected vs actual

Expected, per the code's own intent: return the existing Account.

Actual:

friendbot HTTP 400
{
  "type": "https://stellar.org/friendbot-errors/bad_request",
  "title": "Bad Request",
  "status": 400,
  "detail": "account already funded to starting balance"
}

body contains "createAccountAlreadyExist": false

--- server.requestAirdrop(alreadyFundedAccount) ---
THREW: AxiosError: Request failed with status code 400
  error.response?.data?.detail = "account already funded to starting balance"

The thrown error is a bare AxiosError reading Request failed with status code 400, so even the loud failure gives no hint about the cause.

Root cause

src/rpc/server.ts:1645:

if (error.response?.status === 400) {
  if (error.response.data?.detail?.includes("createAccountAlreadyExist")) {
    return this.getAccount(account);
  }
}
throw error;

The outer status === 400 check passes and detail is present, so only the substring match fails. createAccountAlreadyExist is the XDR CreateAccountResultCode enum name; friendbot returns a human-readable detail instead.

Suggested fix

Match the real response, or widen it:

if (error.response?.status === 400 &&
    /already (funded|exist)/i.test(error.response.data?.detail ?? "")) {
  return this.getAccount(account);
}

For reference, the stellar CLI already handles this correctly. stellar keys fund --very-verbose prints already funded error ignored because account is funded, so two official tools disagree on the same condition and one of them is wrong.


Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    Backlog (Not Ready)

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions