> For the complete documentation index, see [llms.txt](https://docs.fuul.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.fuul.xyz/developer-guide/claiming-onchain-rewards/stellar.md).

# Stellar

This guide shows how to claim onchain token rewards from the Fuul protocol on Stellar using the `@fuul/sdk-stellar` package. The flow is: [get claim checks](/developer-guide/claiming-onchain-rewards/get-claim-checks.md) from the API, initialize the Stellar SDK, build the claim with the authorization Fuul pre-signed, and have the user's wallet sign and submit the transaction.

{% hint style="info" %}
Before following this guide, make sure you've [fetched your claim checks](/developer-guide/claiming-onchain-rewards/get-claim-checks.md) using `@fuul/sdk` with `user_identifier_type: 'stellar_address'`.
{% endhint %}

## Prerequisites

| Requirement            | Version  | Notes                                                                         |
| ---------------------- | -------- | ----------------------------------------------------------------------------- |
| Node.js                | >= 22.12 | Required by `@fuul/sdk-stellar`                                               |
| `@fuul/sdk-stellar`    | 0.3.0    | ESM and CommonJS builds                                                       |
| `@stellar/stellar-sdk` | 17.2.1   | Use the same version the Fuul SDK is built on for transaction and XDR objects |
| Stellar wallet         | Any      | This guide uses Freighter (`@stellar/freighter-api`)                          |

```bash
npm install @fuul/sdk-stellar@0.3.0 @stellar/stellar-sdk@17.2.1 @stellar/freighter-api
```

## 1. Understand the Stellar claim check

Claim checks for a Stellar user come back in the same shape as on other chains, with three differences:

| Field           | Stellar value                                                     |
| --------------- | ----------------------------------------------------------------- |
| `currency_type` | `0` (Stellar asset)                                               |
| `signatures`    | Base64 XDR `SorobanAuthorizationEntry` values, pre-signed by Fuul |
| `signers`       | Extra array with the signer `G…` addresses, one per signature     |

`project_address`, `to`, and `currency` are Stellar StrKeys (`C…` for the project and the asset, `G…` for the recipient). They are case-sensitive: pass them exactly as returned.

```typescript
import type { ClaimResponse } from '@fuul/sdk';

// `signers` is only present on Stellar claim checks
type StellarClaimCheck = ClaimResponse & { signers: string[] };
```

## 2. Initialize the SDK

Use the Manager and Factory addresses for your network from [Stellar Contracts](/protocol-reference/stellar-contracts.md).

```typescript
import { FuulSdk } from '@fuul/sdk-stellar';
import { Networks } from '@stellar/stellar-sdk';

const sdk = new FuulSdk({
  rpcUrl: 'https://soroban-testnet.stellar.org', // your Soroban RPC endpoint
  networkPassphrase: Networks.TESTNET,           // Networks.PUBLIC on mainnet
  contracts: {
    factory: FUUL_FACTORY, // from Stellar Contracts
    manager: FUUL_MANAGER, // from Stellar Contracts
  },
  publicKey: walletAddress, // the connected G… address that submits the claim
});
```

{% hint style="warning" %}
The network passphrase must match both your RPC endpoint and the network the user's wallet is set to. A testnet wallet signing a mainnet transaction (or vice versa) fails.
{% endhint %}

## 3. Skip checks that were already claimed

Each claim check carries a unique `proof`. Read the project contract to skip checks that were already redeemed:

```typescript
import { readContract } from '@fuul/sdk-stellar';
import { Buffer } from 'buffer';

const proofBytes = (check: StellarClaimCheck) =>
  Buffer.from(check.proof.replace(/^0x/, ''), 'hex');

async function isClaimed(sdk: FuulSdk, check: StellarClaimCheck): Promise<boolean> {
  return readContract(
    sdk.project(check.project_address).claimed_proofs({ proof: proofBytes(check) })
  );
}
```

## 4. Build the claim transaction

Build the claim check, simulate the Manager's `claim`, then swap in the authorization entries Fuul signed:

```typescript
import { claimReason, createClaimCheck, currencyType } from '@fuul/sdk-stellar';
import { Address, Operation, rpc, TransactionBuilder, xdr } from '@stellar/stellar-sdk';

// Signer address of an authorization entry (covers both address credential types)
function authSigner(entry: xdr.SorobanAuthorizationEntry): string | null {
  const { credentials } = entry;
  if (credentials.type === 'sorobanCredentialsAddress') {
    return Address.fromScAddress(credentials.address.address).toString();
  }
  if (credentials.type === 'sorobanCredentialsAddressV2') {
    return Address.fromScAddress(credentials.addressV2.address).toString();
  }
  return null;
}

async function buildClaimTransaction(sdk: FuulSdk, walletAddress: string, check: StellarClaimCheck) {
  const claimCheck = createClaimCheck({
    project: check.project_address,
    to: check.to,
    currency: check.currency,
    currencyType: currencyType.stellarAsset,
    amount: BigInt(check.amount),
    reason: check.reason === 0 ? claimReason.affiliatePayout : claimReason.endUserPayout,
    tokenId: BigInt(check.token_id ?? 0),
    deadline: BigInt(check.deadline),
    proof: proofBytes(check),
    signers: check.signers,
  });

  // Simulate manager.claim with the connected wallet as caller
  const assembled = await sdk.manager.claim({ caller: walletAddress, checks: [claimCheck] });
  if (!assembled.simulation || !assembled.built) throw new Error('Simulation returned no transaction');
  if (rpc.Api.isSimulationError(assembled.simulation)) throw new Error(assembled.simulation.error);

  // Replace the simulated signer entries with the ones Fuul pre-signed
  const presigned = new Map(
    check.signatures.map((signature) => {
      const entry = xdr.SorobanAuthorizationEntry.fromXdr(signature, 'base64');
      return [authSigner(entry), entry] as const;
    })
  );
  const operation = assembled.built.operations[0];
  if (operation?.type !== 'invokeHostFunction') throw new Error('Unexpected claim transaction');
  const auth = (operation.auth ?? []).map((entry) => presigned.get(authSigner(entry)) ?? entry);

  const withAuth = TransactionBuilder.cloneFrom(assembled.built)
    .clearOperations()
    .addOperation(Operation.invokeHostFunction({ func: operation.func, auth }))
    .build();

  // Re-simulate in enforcement mode so the signed entries are checked before the wallet signs
  const enforced = await sdk.rpc.simulateTransaction(withAuth, undefined, 'enforce');
  if (rpc.Api.isSimulationError(enforced)) throw new Error(enforced.error);

  return rpc.assembleTransaction(withAuth, enforced).build();
}
```

{% hint style="info" %}
Never build the authorization entries yourself or copy them from a simulation response. Only the entries returned by the Fuul API carry Fuul's signature.
{% endhint %}

## 5. Sign and submit

The user's wallet signs the transaction and pays the network fee plus the claim fee:

```typescript
import { signTransaction } from '@stellar/freighter-api';
import { Transaction } from '@stellar/stellar-sdk';

async function submitClaim(sdk: FuulSdk, prepared: Transaction, walletAddress: string, networkPassphrase: string) {
  const signed = await signTransaction(prepared.toXdr(), { networkPassphrase, address: walletAddress });
  if (signed.error) throw new Error(`Wallet: ${signed.error.message ?? signed.error}`);

  const sent = await sdk.rpc.sendTransaction(new Transaction(signed.signedTxXdr, networkPassphrase));
  if (sent.status === 'ERROR') throw new Error(`Send failed: ${JSON.stringify(sent.errorResult)}`);

  // Poll until the transaction is confirmed
  for (let attempt = 0; attempt < 30; attempt++) {
    const result = await sdk.rpc.getTransaction(sent.hash);
    if (result.status === 'SUCCESS') return sent.hash;
    if (result.status === 'FAILED') throw new Error(`Transaction failed: ${sent.hash}`);
    await new Promise((resolve) => setTimeout(resolve, 2000));
  }
  throw new Error(`Timed out waiting for ${sent.hash}`);
}
```

{% hint style="warning" %}
If a submission times out, look up its hash before retrying. Rebuilding and resending a claim whose first transaction already landed fails with `ProofAlreadyClaimed`.
{% endhint %}

## 6. Complete example

```typescript
async function claimAll(walletAddress: string, checks: StellarClaimCheck[]) {
  const sdk = new FuulSdk({
    rpcUrl: RPC_URL,
    networkPassphrase: NETWORK_PASSPHRASE,
    contracts: { factory: FUUL_FACTORY, manager: FUUL_MANAGER },
    publicKey: walletAddress,
  });

  for (const check of checks) {
    if (await isClaimed(sdk, check)) continue;

    const prepared = await buildClaimTransaction(sdk, walletAddress, check);
    const hash = await submitClaim(sdk, prepared, walletAddress, NETWORK_PASSPHRASE);
    console.log('Claim successful:', hash);
  }
}
```

## Good to know

| Topic      | Detail                                                                                                                      |
| ---------- | --------------------------------------------------------------------------------------------------------------------------- |
| Claim fee  | A small native fee per claim, always paid in **XLM** by the wallet submitting the transaction, whatever the reward currency |
| Amounts    | `amount` is a string in base units. Stellar assets use **7 decimals**                                                       |
| Trustlines | The recipient needs a trustline for classic assets such as USDC before claiming. XLM needs none                             |
| Deadline   | Each check carries a `deadline` (Unix seconds). Claim before it passes                                                      |
| Networks   | Stellar Mainnet and Stellar Testnet are both supported. Use testnet to try the full flow before launch                      |

## Troubleshooting

| Symptom                                                         | Cause                                                           | Fix                                                             |
| --------------------------------------------------------------- | --------------------------------------------------------------- | --------------------------------------------------------------- |
| `ProofAlreadyClaimed` (contract error `6102`)                   | The check was already redeemed                                  | Check `claimed_proofs` before submitting (step 3)               |
| Wallet refuses to sign, or the transaction fails on the network | Wallet set to a different network than the SDK                  | Match the wallet network, RPC endpoint, and `networkPassphrase` |
| Simulation fails for a USDC claim                               | The recipient has no USDC trustline                             | Add the trustline from the wallet, then retry                   |
| Claim check has no `signers`                                    | Claim checks fetched for a non-Stellar identifier               | Fetch with `user_identifier_type: 'stellar_address'`            |
| Signature or authorization errors at simulation                 | Authorization entries built locally or copied from a simulation | Use the `signatures` returned by the Fuul API (step 4)          |
| Claim rejected after the deadline                               | The check expired                                               | Fetch fresh claim checks                                        |

{% hint style="warning" %}
Always simulate before asking the user to sign. The enforcement-mode simulation in step 4 catches authorization and resource errors before any fee is spent.
{% endhint %}
