Skip to content

Commit 27f2c9e

Browse files
committed
fix(rpc): handle contract addresses in requestAirdrop()
requestAirdrop() now properly supports C... (contract) addresses by detecting them via StrKey.isValidContract() and returning a ContractFundingResult instead of attempting to find a created account. Fixes funding contract addresses which previously threw 'No account created in transaction' error.
1 parent 8b2d969 commit 27f2c9e

File tree

3 files changed

+110
-11
lines changed

3 files changed

+110
-11
lines changed

src/rpc/api.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ export namespace Api {
4848
protocolVersion: string;
4949
}
5050

51+
/**
52+
* Result returned when funding a contract address via friendbot.
53+
*/
54+
export interface ContractFundingResult {
55+
contractId: string;
56+
hash: string;
57+
}
58+
5159
/** @see https://developers.stellar.org/docs/data/rpc/api-reference/methods/getLatestLedger */
5260
export interface GetLatestLedgerResponse {
5361
id: string;

src/rpc/server.ts

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1194,37 +1194,52 @@ export class RpcServer {
11941194
}
11951195

11961196
/**
1197-
* Fund a new account using the network's Friendbot faucet, if any.
1197+
* Fund a new account or contract using the network's Friendbot faucet, if any.
11981198
*
11991199
* @param {string | Account} address The address or account instance that we
1200-
* want to create and fund with Friendbot
1200+
* want to create and fund with Friendbot. Supports both G... (account) and
1201+
* C... (contract) addresses.
12011202
* @param {string} [friendbotUrl] Optionally, an explicit address for
12021203
* friendbot (by default: this calls the Soroban RPC
12031204
* {@link module:rpc.Server#getNetwork | getNetwork} method to try to
12041205
* discover this network's Friendbot url).
1205-
* @returns {Promise<Account>} An {@link Account} object for the created
1206-
* account, or the existing account if it's already funded with the
1207-
* populated sequence number (note that the account will not be "topped
1208-
* off" if it already exists)
1206+
* @returns {Promise<Account | Api.ContractFundingResult>} An {@link Account}
1207+
* object for the created account (G... addresses), or a
1208+
* {@link Api.ContractFundingResult} for contract addresses (C...). For
1209+
* existing accounts, returns the account with populated sequence number
1210+
* (note that accounts will not be "topped off" if they already exist).
12091211
* @throws {Error} If Friendbot is not configured on this network or request failure
12101212
*
12111213
* @see {@link https://developers.stellar.org/docs/learn/fundamentals/networks#friendbot | Friendbot docs}
12121214
* @see {@link module:Friendbot.Api.Response}
12131215
*
12141216
* @example
1217+
* // Funding an account (G... address)
12151218
* server
12161219
* .requestAirdrop("GBZC6Y2Y7Q3ZQ2Y4QZJ2XZ3Z5YXZ6Z7Z2Y4QZJ2XZ3Z5YXZ6Z7Z2Y4")
1217-
* .then((accountCreated) => {
1218-
* console.log("accountCreated:", accountCreated);
1219-
* }).catch((error) => {
1220-
* console.error("error:", error);
1220+
* .then((result) => {
1221+
* if (result instanceof Account) {
1222+
* console.log("accountCreated:", result.accountId());
1223+
* }
1224+
* });
1225+
*
1226+
* @example
1227+
* // Funding a contract (C... address)
1228+
* server
1229+
* .requestAirdrop("CBZC6Y2Y7Q3ZQ2Y4QZJ2XZ3Z5YXZ6Z7Z2Y4QZJ2XZ3Z5YXZ6Z7Z2Y4")
1230+
* .then((result) => {
1231+
* if ('contractId' in result) {
1232+
* console.log("contractFunded:", result.contractId, "hash:", result.hash);
1233+
* }
12211234
* });
12221235
*/
12231236
public async requestAirdrop(
12241237
address: string | Pick<Account, "accountId">,
12251238
friendbotUrl?: string,
1226-
): Promise<Account> {
1239+
): Promise<Account | Api.ContractFundingResult> {
12271240
const account = typeof address === "string" ? address : address.accountId();
1241+
const isContract = StrKey.isValidContract(account);
1242+
12281243
friendbotUrl = friendbotUrl || (await this.getNetwork()).friendbotUrl;
12291244
if (!friendbotUrl) {
12301245
throw new Error("No friendbot URL configured for current network");
@@ -1235,6 +1250,14 @@ export class RpcServer {
12351250
`${friendbotUrl}?addr=${encodeURIComponent(account)}`,
12361251
);
12371252

1253+
// Contract addresses don't create accounts - they receive funds via SAC transfer
1254+
if (isContract) {
1255+
return {
1256+
contractId: account,
1257+
hash: response.data.hash,
1258+
};
1259+
}
1260+
12381261
let meta: xdr.TransactionMeta;
12391262
if (!response.data.result_meta_xdr) {
12401263
const txMeta = await this.getTransaction(response.data.hash);

test/unit/server/soroban/request_airdrop.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,4 +369,72 @@ describe("Server#requestAirdrop", () => {
369369
});
370370
expect(mockPost).toHaveBeenCalledTimes(3);
371371
});
372+
373+
it("returns ContractFundingResult for contract addresses (C...)", async () => {
374+
const friendbotUrl = "https://friendbot.stellar.org";
375+
// Valid contract address (C...)
376+
const contractId = StellarSdk.StrKey.encodeContract(
377+
Buffer.from("0".repeat(64), "hex"),
378+
);
379+
const hash =
380+
"ae9f315c048d87a5f853bc15bf284a2c3c89eb0e1cb38c10409b77a877b830a8";
381+
382+
const networkResult = {
383+
friendbotUrl,
384+
passphrase: Networks.FUTURENET,
385+
protocolVersion: 20,
386+
};
387+
const networkResponse = { data: { result: networkResult } };
388+
389+
// Mock the friendbot call - contract funding returns hash but no account creation
390+
const friendbotResponse = {
391+
status: 200,
392+
data: {
393+
hash,
394+
result_meta_xdr: "someXdr", // Doesn't matter - should not be parsed for contracts
395+
},
396+
};
397+
398+
mockPost
399+
.mockResolvedValueOnce(networkResponse) // getNetwork call
400+
.mockResolvedValueOnce(friendbotResponse); // friendbot call
401+
402+
const result = await server.requestAirdrop(contractId);
403+
404+
// Should return ContractFundingResult, not Account
405+
expect(result).not.toBeInstanceOf(StellarSdk.Account);
406+
expect(result).toHaveProperty("contractId", contractId);
407+
expect(result).toHaveProperty("hash", hash);
408+
expect(mockPost).toHaveBeenCalledWith(serverUrl, {
409+
jsonrpc: "2.0",
410+
id: 1,
411+
method: "getNetwork",
412+
params: null,
413+
});
414+
expect(mockPost).toHaveBeenCalledWith(`${friendbotUrl}?addr=${contractId}`);
415+
expect(mockPost).toHaveBeenCalledTimes(2);
416+
});
417+
418+
it("does not throw 'No account created' for contract addresses", async () => {
419+
const customFriendbotUrl = "https://custom-friendbot.example.com";
420+
const contractId = StellarSdk.StrKey.encodeContract(
421+
Buffer.from("1".repeat(64), "hex"),
422+
);
423+
const hash = "somehash123";
424+
425+
const friendbotResponse = {
426+
status: 200,
427+
data: { hash },
428+
};
429+
430+
mockPost.mockResolvedValueOnce(friendbotResponse);
431+
432+
// Should NOT throw "No account created in transaction"
433+
const result = await server.requestAirdrop(contractId, customFriendbotUrl);
434+
435+
expect(result).toEqual({
436+
contractId,
437+
hash,
438+
});
439+
});
372440
});

0 commit comments

Comments
 (0)