# SVM (Solana)

This guide shows how to claim onchain token rewards from the Fuul protocol on Solana using the `@fuul/sdk-solana` package. The flow is: [get claim checks](https://docs.fuul.xyz/developer-guide/claiming-onchain-rewards/get-claim-checks) from the API, initialize the Solana SDK, build claim instructions, and submit the transaction.

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

## Prerequisites

| Requirement         | Version | Notes                                      |
| ------------------- | ------- | ------------------------------------------ |
| Node.js             | >= 18   | Required for native crypto support         |
| TypeScript          | >= 5.0  | Strict mode recommended                    |
| `@solana/web3.js`   | ^1.95.0 | Peer dependency                            |
| `@coral-xyz/anchor` | ^0.30.0 | Peer dependency                            |
| Wallet adapter      | Any     | `@solana/wallet-adapter-react` recommended |

```bash
npm install @fuul/sdk-solana @solana/web3.js @coral-xyz/anchor
```

## 1. Configure bundler (Next.js)

The SDK uses Node.js `Buffer` API. Configure webpack to provide it in the browser.

```typescript
// next.config.ts
import type { NextConfig } from 'next';
const webpack = require('webpack');

const nextConfig: NextConfig = {
  webpack: (config, { isServer }) => {
    if (!isServer) {
      config.resolve.fallback = {
        ...config.resolve.fallback,
        fs: false,
        net: false,
        tls: false,
        buffer: require.resolve('buffer/'),
      };
      config.plugins.push(
        new webpack.ProvidePlugin({
          Buffer: ['buffer', 'Buffer'],
        })
      );
    }
    return config;
  },
};

export default nextConfig;
```

{% hint style="info" %}
You also need to install the `buffer` package: `npm install buffer`.
{% endhint %}

## 2. Initialize the SDK

```typescript
'use client';

import { Connection } from '@solana/web3.js';
import { FuulSdk, Network } from '@fuul/sdk-solana';

const connection = new Connection('https://api.devnet.solana.com', {
  commitment: 'confirmed',
  confirmTransactionInitialTimeout: 60000,
});

const sdk = new FuulSdk(connection, Network.DEVNET);
```

{% hint style="warning" %}
The `Network` enum must match your RPC endpoint. Using `Network.DEVNET` with a mainnet RPC (or vice versa) will cause "Account does not exist" errors.
{% endhint %}

## 3. Fetch on-chain data

Before building a claim, fetch the project nonce and authorized signer from the chain:

```typescript
import * as anchor from '@coral-xyz/anchor';
import { PublicKey } from '@solana/web3.js';

async function fetchClaimRequirements(sdk: FuulSdk, projectAddress: PublicKey) {
  const program = sdk.getProgram();

  // Fetch project account to get nonce
  const projectAccount = await program.account.project.fetch(projectAddress);
  const projectNonce = projectAccount.nonce as anchor.BN;

  // Fetch global config to get authorized signer
  const globalConfig = await sdk.getGlobalConfig();
  if (!globalConfig) {
    throw new Error('Global config not found');
  }

  const signers = globalConfig.rolesMapping.roles
    .filter((r) => Object.keys(r.role)[0] === 'signer')
    .map((r) => r.account);

  if (signers.length === 0) {
    throw new Error('No authorized signers found');
  }

  return { projectNonce, signer: signers[0], program };
}
```

## 4. Build claim instructions

Construct the claim message from the API response and get transaction instructions from the SDK:

```typescript
import {
  ClaimMessage,
  ClaimMessageData,
  MessageDomain,
  TokenType,
  ClaimReason,
} from '@fuul/sdk-solana';

async function buildClaimInstructions(
  sdk: FuulSdk,
  walletPublicKey: PublicKey,
  claim: {
    projectAddress: PublicKey;
    recipient: PublicKey;
    tokenMint: PublicKey;
    amount: bigint;
    deadline: number;
    reasonCode: number;
    proof: Uint8Array;
    signature: Uint8Array;
  }
) {
  const { projectNonce, signer, program } = await fetchClaimRequirements(
    sdk,
    claim.projectAddress
  );

  // Build message domain
  const domain = new MessageDomain({
    programId: program.programId,
    version: 1,
    deadline: BigInt(claim.deadline),
  });

  // Build claim data
  const claimData = new ClaimMessageData({
    amount: claim.amount,
    project: claim.projectAddress,
    recipient: claim.recipient,
    tokenType: TokenType.FungibleSpl,
    tokenMint: claim.tokenMint,
    proof: Buffer.from(claim.proof),
    reason:
      claim.reasonCode === 0
        ? ClaimReason.AffiliatePayout
        : ClaimReason.EndUserPayout,
  });

  const claimMessage = new ClaimMessage({ data: claimData, domain });

  // SDK handles ATA creation and PDA derivation internally
  const instructions = await sdk.claim({
    authority: walletPublicKey,
    projectNonce,
    message: claimMessage,
    signatures: [
      {
        signature: claim.signature,
        signer: signer,
      },
    ],
  });

  return instructions;
}
```

{% hint style="info" %}
The SDK resolves Associated Token Accounts (ATAs), PDA addresses, compute budget, and account metas internally. **Do not** create ATAs manually — this causes `IllegalOwner` errors.
{% endhint %}

## 5. Execute the transaction

```typescript
import { Transaction, Connection, PublicKey } from '@solana/web3.js';

async function executeClaim(
  connection: Connection,
  instructions: TransactionInstruction[],
  walletPublicKey: PublicKey,
  signTransaction: (tx: Transaction) => Promise<Transaction>
): Promise<string> {
  const transaction = new Transaction();
  transaction.add(...instructions);

  const { blockhash, lastValidBlockHeight } =
    await connection.getLatestBlockhash('confirmed');

  transaction.recentBlockhash = blockhash;
  transaction.lastValidBlockHeight = lastValidBlockHeight;
  transaction.feePayer = walletPublicKey;

  // Simulate first (recommended for debugging)
  const simulation = await connection.simulateTransaction(transaction);
  if (simulation.value.err) {
    console.error('Simulation failed:', simulation.value.logs);
    throw new Error(`Simulation failed: ${JSON.stringify(simulation.value.err)}`);
  }

  // Sign and send
  const signedTx = await signTransaction(transaction);
  const signature = await connection.sendRawTransaction(signedTx.serialize(), {
    skipPreflight: false,
    preflightCommitment: 'confirmed',
  });

  // Confirm
  await connection.confirmTransaction(
    { signature, blockhash, lastValidBlockHeight },
    'confirmed'
  );

  return signature;
}
```

## 6. Complete Next.js example

```typescript
'use client';

import { useWallet } from '@solana/wallet-adapter-react';
import { Connection, PublicKey, Transaction } from '@solana/web3.js';
import { FuulSdk, Network } from '@fuul/sdk-solana';
import { useState } from 'react';

export function ClaimButton({ claimData }: { claimData: ValidatedClaimCheck }) {
  const { publicKey, signTransaction } = useWallet();
  const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');

  async function handleClaim() {
    if (!publicKey || !signTransaction) {
      alert('Please connect your wallet');
      return;
    }

    setStatus('loading');

    try {
      const connection = new Connection(
        process.env.NEXT_PUBLIC_SOLANA_RPC_URL || 'https://api.devnet.solana.com',
        'confirmed'
      );

      const sdk = new FuulSdk(connection, Network.DEVNET);

      const instructions = await buildClaimInstructions(sdk, publicKey, claimData);
      const signature = await executeClaim(
        connection,
        instructions,
        publicKey,
        signTransaction
      );

      console.log('Claim successful:', signature);
      setStatus('success');
    } catch (error) {
      console.error('Claim failed:', error);
      setStatus('error');
    }
  }

  return (
    <button onClick={handleClaim} disabled={status === 'loading'}>
      {status === 'loading' ? 'Processing...' : 'Claim Rewards'}
    </button>
  );
}
```

## Key types

```typescript
enum Network {
  FOGO_MAINNET = 'fogo-mainnet',
  FOGO_TESTNET = 'fogo-testnet',
  MAINNET = 'mainnet',
  DEVNET = 'devnet',
  TESTNET = 'testnet',
  LOCALHOST = 'localhost',
}

enum TokenType {
  Native = 'native',
  FungibleSpl = 'fungibleSpl',
  NonFungibleSpl = 'nonFungibleSpl',
}

enum ClaimReason {
  AffiliatePayout = 'affiliatePayout',
  EndUserPayout = 'endUserPayout',
}

type Signature = {
  signature: Uint8Array;
  signer: PublicKey;
};
```

## What the SDK handles internally

Do **not** pass these manually — the SDK derives them:

| What                      | How the SDK derives it                       |
| ------------------------- | -------------------------------------------- |
| Program ID                | From `Network` enum via internal IDL mapping |
| Associated Token Accounts | Created inside `sdk.claim()` if needed       |
| PDA addresses             | Derived from seeds (project, config, etc.)   |
| Global config address     | Derived from program ID                      |
| Compute budget            | Determined by SDK                            |
| Account metas             | Built from instruction requirements          |

## Troubleshooting

| Symptom                         | Cause                                     | Fix                                                                |
| ------------------------------- | ----------------------------------------- | ------------------------------------------------------------------ |
| `Buffer is not defined`         | Missing polyfill in browser               | Add webpack `ProvidePlugin` for Buffer                             |
| `IllegalOwner`                  | Manual ATA creation when SDK handles it   | Remove manual `createAssociatedTokenAccountInstruction`            |
| `Account does not exist`        | Network mismatch (devnet vs mainnet)      | Ensure `Network` enum matches your RPC endpoint                    |
| `Invalid program id`            | Hardcoded program ID                      | Use `sdk.getProgram().programId`                                   |
| `Signature verification failed` | Wrong signer or corrupted signature bytes | Use signer from `sdk.getGlobalConfig()`, verify signature encoding |
| `window is not defined`         | Importing SDK in server component         | Add `'use client'` directive or dynamic import with `ssr: false`   |
| `Blockhash not found`           | Wrong commitment level                    | Use `'confirmed'` for both connection and transaction              |
| `ConstraintSeeds`               | Wrong PDA derivation                      | Let SDK derive PDAs — don't calculate manually                     |

{% hint style="warning" %}
Always simulate the transaction before signing with `connection.simulateTransaction()`. Check `simulation.value.logs` for detailed error messages.
{% endhint %}
