-
Paying gas for users
Apps/wallets often sponsor fees so UX stays smooth. But sponsorship:- increases attack surface,
- forces the app to constantly monitor & top up its XLM balance.
-
No common framework
Every project re-implements a bespoke solution from scratch. -
Soroban is trickier than Stellar Classic
Classic sponsorship is “relatively” simple. In Soroban, a transaction with a contract invocation can only contain one such operation, so composing multiple actions requires an indirection.
Gasolina is a tiny SDK that lets apps (wallets, DEXes, etc.) prepay gas and get refunded immediately in the same flow.
- The user signs once (no extra prompts) and sends the signed authorization entry to the backend (relayer/payer).
- The backend submits the transaction on-chain and receives a refund (e.g., in USDC) via a bundled call.
Because Soroban allows only one invoke op per transaction, we use RouterV1 to bundle calls:
- RouterV1 repo: https://github.com/Creit-Tech/Stellar-Router-Contract/tree/main/contracts/router-v1/src
In this hackathon demo we use two inner calls:
- Soroswap
swap_tokens_for_exact_tokensto buy the required XLM for gas using USDC and pay it to the relayer (refund). - USDC token
transferto send user-intended funds to a recipient.
The same pattern generalizes: users can interact with contracts while paying fees in USDC instead of XLM.
- Frontend builds a RouterV1 op that wraps two calls:
[ swap_usdc_to_xlm_and_pay_back_backend, transfer ], then sends it to the backend. - Backend simulates the draft transaction to discover
required_auth, and returns the user’s unsigned auth entry. - Frontend has the user sign the auth entry (not the transaction) and sends it back.
- Backend rebuilds the op with signed auth, assembles resource footprint/fees, signs the transaction as payer, and submits.
UX note: The user signs only once — it feels like a normal single-sign flow.
You can use pnpm or npm. No environment variables needed — demo input parameters are hardcoded to make the script easy to run.
Using pnpm:
pnpm install
pnpm exec ts-node scripts/sdk.tsUsing npm (equivalent):
npm install
npx ts-node scripts/sdk.ts-
scripts/— SDK methods plus a runnable demo entry.scripts/sdk.ts— core library (documented) with amain()demo that builds the RouterV1 call, simulates, has the user sign the auth entry, and submits via the relayer.scripts/only_once_trustline.ts— helper to sponsor & create a USDC trustline for a user from the payer/relayer account. Useful when your app creates a fresh user address and wants to ensure the USDC trustline exists only once.
-
src/— not our code; contains the Stellar RouterV1 contract sources placed here purely so you can deploy them alongside this repo. -
showcase/— minimal example app demonstrating gas abstraction end-to-end:- the user sends USDC to a recipient without paying XLM gas directly;
- the relayer prepays gas and is refunded within the same RouterV1 execution (via Soroswap swap USDC→XLM);
- the user experience is identical to a normal send (single signature), as if they had paid gas in XLM themselves;
- includes wiring for the four-step flow (build → simulate → sign auth → submit) and simple UI/console outputs so you can trace each stage. Demo Video
Try the showcase app live on Vercel: Open the Showcase
You can also watch Demo Video
export interface AuthEntrySigner {
getPublicKey(): string | Promise<string>;
signAuthEntry(
unsigned: xdr.SorobanAuthorizationEntry,
validUntilLedger: number,
networkPassphrase: string
): Promise<xdr.SorobanAuthorizationEntry>;
}
export interface TxSigner {
getPublicKey(): string | Promise<string>;
signTxXDR(b64Xdr: string, networkPassphrase: string): Promise<string>;
}Why? These let you plug in either raw Keypair (Node) or Freighter (browser) without changing the rest of the flow.
export const keypairAuthSigner = (kp: Keypair): AuthEntrySigner => ({
getPublicKey: () => kp.publicKey(),
signAuthEntry: (u, v, p) => authorizeEntry(u, kp, v, p),
});
export const keypairTxSigner = (kp: Keypair): TxSigner => ({
getPublicKey: () => kp.publicKey(),
signTxXDR: async (b64, pass) => {
const tx = TransactionBuilder.fromXDR(b64, pass);
if (tx instanceof FeeBumpTransaction) tx.innerTransaction.sign(kp);
else (tx as Transaction).sign(kp);
return tx.toXDR();
},
});constructRouterContractOp(...)– Backend-only logic. Builds a RouterV1invokeContractFunctionop without auth and returns{ opWithoutAuth, routerArgs }.simulateAndGetUnsignedCaller(...)– Backend simulates a draft transaction to retrieverequired_auth. Returns{ unsignedCaller, sim }.makeOpWithAuth(...)– Frontend asksAuthEntrySigner(user) to sign the SorobanAuthorizationEntry, then returns the same op withauthpopulated.submitOpWithAuthToRelayer(...)– Backend assembles resource footprint and fees usingsim, asksTxSigner(payer) to sign the transaction XDR, then sends and polls.
estimateGasXlm()– demo gas heuristic (2 * BASE_FEE). Replace with a real estimator.estimateMaxUsdcForGas({ margin, estimatedGasXlm, rateXlmToUsdc })– converts the gas target to a max USDC in for the swap.- Invocation builders:
buildInvocation_UsdcTransfer(from, to, amount)buildInvocation_UsdcSwapXlm(amount_out, amount_in_max, caller, deadline)(USDC → XLM path)
If you don’t want to bundle Node Keypairs in the browser, implement the signer interfaces with Freighter:
import * as freighter from '@stellar/freighter-api';
import { xdr } from '@stellar/stellar-sdk';
const freighterUserSigner: AuthEntrySigner = {
getPublicKey: () => freighter.getPublicKey(),
signAuthEntry: async (unsigned, validUntil, passphrase) => {
// Freighter generally returns base64; convert back to XDR object
const signedB64 = await freighter.signAuthEntry(unsigned.toXDR('base64'), {
networkPassphrase: passphrase,
validUntilLedger: validUntil,
});
return xdr.SorobanAuthorizationEntry.fromXDR(signedB64, 'base64');
},
};
const freighterTxSigner: TxSigner = {
getPublicKey: () => freighter.getPublicKey(),
signTxXDR: (b64, passphrase) => freighter.signTransaction(b64, { networkPassphrase: passphrase }),
};- Contracts are pinned via IDs in
scripts/sdk.ts(RouterV1, USDC, XLM wrapper, Soroswap router). Change as needed. - Decimals: the demo uses a scale of
1e7. Adjust to your tokens’ decimals. - Swap safety: set real
amount_in_maxmargins & short deadlines (slippage control). - Prereqs: the user should have USDC balance; relevant trustlines/approvals must exist per your token/router configs.
- Security: never hardcode secrets in production; use env or wallet signers. Validate inputs server-side.
Soroban only allows one contract invocation op per transaction. RouterV1 batches multiple contract invocations into a single op so the gas purchase (refund) and user action happen atomically.
MIT


