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.
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.1and16.2.0, identical code.Reproduction
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:
The thrown error is a bare
AxiosErrorreadingRequest failed with status code 400, so even the loud failure gives no hint about the cause.Root cause
src/rpc/server.ts:1645:The outer
status === 400check passes anddetailis present, so only the substring match fails.createAccountAlreadyExistis the XDRCreateAccountResultCodeenum name; friendbot returns a human-readabledetailinstead.Suggested fix
Match the real response, or widen it:
For reference, the
stellarCLI already handles this correctly.stellar keys fund --very-verboseprintsalready funded error ignored because account is funded, so two official tools disagree on the same condition and one of them is wrong.