# Send transaction

Solana wallets use [`@solana/kit`](https://www.npmjs.com/package/@solana/kit) to build, sign, and submit transactions. Openfort's embedded wallet signs transaction message bytes directly with Ed25519, so the flow is always the same: wrap the wallet's `signMessage` in a `@solana/kit` `TransactionSigner`, build a transaction message, sign it with that signer, then broadcast and confirm.

:::tip[Built-in Send UI]
If you render [`<OpenfortButton />`](/docs/products/embedded-wallet/react/ui), your users already get a **Send** action for native SOL in the wallet modal — with an optional **gasless** toggle that sponsors the fee through the [Solana Paymaster](/docs/products/infrastructure/paymaster/solana). On an empty wallet, Send routes to **Add funds** instead. The steps below are for building a custom send flow.
:::

## How signing works

Two details are specific to Solana:

* **Ed25519, not keccak256.** Pass `{ hashMessage: false }` to `signMessage` so the wallet signs the raw transaction bytes. The default hashing is only correct for EVM signatures.
* **A signer adapter.** `@solana/kit` builds and signs transactions through a `TransactionSigner`. You bridge it to the embedded wallet with a small adapter that calls `signMessage` and trims the recovery byte (Ed25519 signatures are 64 bytes).

## 1. Create the signer adapter

`createOpenfortSigner` turns the wallet's `signMessage` into a `@solana/kit` `TransactionSigner` you can reuse for every transaction:

```tsx [createOpenfortSigner.ts]
import {
  type Address,
  type SignatureBytes,
  type SignatureDictionary,
  type TransactionSigner,
} from '@solana/kit'
import { Base58 } from 'ox'

export function createOpenfortSigner(
  signerAddress: Address,
  signMessage: (message: Uint8Array) => Promise<string>,
): TransactionSigner {
  return {
    address: signerAddress,
    signTransactions: async (transactions): Promise<readonly SignatureDictionary[]> => {
      return Promise.all(
        transactions.map(async (transaction) => {
          const signatureBase58 = await signMessage(
            new Uint8Array(transaction.messageBytes),
          )
          let signatureBytes = Base58.toBytes(signatureBase58)

          // Trim recovery byte if present (65 → 64 bytes for Ed25519)
          if (signatureBytes.length === 65) {
            signatureBytes = signatureBytes.slice(0, 64)
          }

          return Object.freeze({
            [signerAddress]: signatureBytes as SignatureBytes,
          })
        }),
      )
    },
  }
}
```

## 2. Build, sign, and send a transfer

Build the transfer with the signer as the source, sign it with `signTransactionMessageWithSigners`, then broadcast and confirm with `sendAndConfirmTransactionFactory`:

```tsx [useSendSol.ts]
import { useMemo } from 'react'
import { useSolanaEmbeddedWallet } from '@openfort/react/solana'
import {
  type Address,
  address,
  createSolanaRpc,
  createSolanaRpcSubscriptions,
  createTransactionMessage,
  lamports,
  pipe,
  sendAndConfirmTransactionFactory,
  setTransactionMessageFeePayer,
  setTransactionMessageLifetimeUsingBlockhash,
  signTransactionMessageWithSigners,
  appendTransactionMessageInstruction,
  assertIsTransactionWithBlockhashLifetime,
} from '@solana/kit'
import { getTransferSolInstruction } from '@solana-program/system'
import { createOpenfortSigner } from './createOpenfortSigner'

const rpc = createSolanaRpc('https://api.devnet.solana.com')
const rpcSubscriptions = createSolanaRpcSubscriptions('wss://api.devnet.solana.com')

export function useSendSol() {
  const solana = useSolanaEmbeddedWallet()

  return useMemo(() => {
    if (solana.status !== 'connected') return null

    return async (to: string, amountInSol: number) => {
      const from = solana.activeWallet.address as Address
      const openfortSigner = createOpenfortSigner(from, (message) =>
        solana.provider.signMessage(message, { hashMessage: false }),
      )

      const { value: latestBlockhash } = await rpc.getLatestBlockhash().send()

      const transactionMessage = pipe(
        createTransactionMessage({ version: 0 }),
        (tx) => setTransactionMessageFeePayer(from, tx),
        (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
        (tx) =>
          appendTransactionMessageInstruction(
            getTransferSolInstruction({
              source: openfortSigner,
              destination: address(to),
              amount: lamports(BigInt(Math.floor(amountInSol * 1_000_000_000))),
            }),
            tx,
          ),
      )

      const signedTransaction = await signTransactionMessageWithSigners(transactionMessage)
      assertIsTransactionWithBlockhashLifetime(signedTransaction)

      const sendAndConfirm = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })
      await sendAndConfirm(signedTransaction, { commitment: 'confirmed' })

      return Object.keys(signedTransaction.signatures)[0]
    }
  }, [solana])
}
```

:::tip
To sign arbitrary messages on Solana, see [Sign message (Solana)](/docs/products/embedded-wallet/react/wallet/actions/sign-message-solana).
:::

## Gasless Solana transactions

Keep transactions SOL-free for users by sponsoring fees through the [Openfort Solana Paymaster](/docs/products/infrastructure/paymaster/solana), which integrates with [Kora](https://www.npmjs.com/package/@solana/kora). Openfort acts as the transaction fee payer, so users never need to hold SOL.

1. In [**Dashboard → Gas sponsorship**](https://dashboard.openfort.io/policies), add a gas sponsorship with a `sponsorSolTransaction` rule, then pick a **Sponsorship mode**:
   * **App pays** — your project covers the SOL fees so users transact for free.
   * **User pays** — users pay fees in a supported SPL token; Kora computes the exchange rate at request time.
2. Point a `KoraClient` at the Openfort RPC for your cluster (`https://api.openfort.io/rpc/solana/{cluster}`), authenticated with your publishable key.

:::info
Solana gas sponsorship is **project-scoped** — matching policies apply automatically, with no per-request `policyId` to pass (unlike Ethereum).
:::

Build the transfer with the standard `@solana-program/token` library, set Kora as the fee payer, sign the user's part with the same `createOpenfortSigner` adapter from above (Ed25519, `{ hashMessage: false }`), then submit through Kora for co-signing and broadcast:

```tsx [useGaslessSol.ts]
import { KoraClient } from '@solana/kora'
import { useSolanaEmbeddedWallet } from '@openfort/react/solana'
import {
  address,
  createNoopSigner,
  getBase64EncodedWireTransaction,
  partiallySignTransactionMessageWithSigners,
  pipe,
  createTransactionMessage,
  setTransactionMessageFeePayerSigner,
  setTransactionMessageLifetimeUsingBlockhash,
  appendTransactionMessageInstruction,
  type Blockhash,
} from '@solana/kit'
import {
  findAssociatedTokenPda,
  getTransferInstruction,
  TOKEN_PROGRAM_ADDRESS,
} from '@solana-program/token'
import { createOpenfortSigner } from './createOpenfortSigner'

const client = new KoraClient({
  rpcUrl: 'https://api.openfort.io/rpc/solana/devnet',
  apiKey: 'Bearer YOUR_OPENFORT_PUBLISHABLE_KEY',
})

export function useGaslessSol() {
  const solana = useSolanaEmbeddedWallet()

  return async (destination: string) => {
    if (solana.status !== 'connected') throw new Error('Wallet not connected')

    const from = address(solana.activeWallet.address)
    const userSigner = createOpenfortSigner(from, (message) =>
      solana.provider.signMessage(message, { hashMessage: false }),
    )

    // 1. Kora's fee-payer signer address
    const { signer_address } = await client.getPayerSigner()
    const feePayer = createNoopSigner(address(signer_address))

    // 2. The SPL token to transfer
    const { allowed_spl_paid_tokens } = (await client.getConfig()).validation_config
    const mint = address(allowed_spl_paid_tokens[0])

    // 3. Build the transfer with the standard SPL Token program
    const [source] = await findAssociatedTokenPda({ owner: from, tokenProgram: TOKEN_PROGRAM_ADDRESS, mint })
    const [dest] = await findAssociatedTokenPda({ owner: address(destination), tokenProgram: TOKEN_PROGRAM_ADDRESS, mint })

    const { blockhash } = await client.getBlockhash()

    const transactionMessage = pipe(
      createTransactionMessage({ version: 0 }),
      (tx) => setTransactionMessageFeePayerSigner(feePayer, tx),
      (tx) =>
        setTransactionMessageLifetimeUsingBlockhash(
          { blockhash: blockhash as Blockhash, lastValidBlockHeight: 0n },
          tx,
        ),
      (tx) =>
        appendTransactionMessageInstruction(
          getTransferInstruction({
            source,
            destination: dest,
            authority: userSigner,
            amount: 100_000n, // 0.10 USDC (6 decimals)
          }),
          tx,
        ),
    )

    // 4. Sign the user's part, then submit for Kora co-signing + broadcast
    const userSigned = await partiallySignTransactionMessageWithSigners(transactionMessage)
    const { signature } = await client.signAndSendTransaction({
      transaction: getBase64EncodedWireTransaction(userSigned),
      signer_key: signer_address,
    })

    return signature
  }
}
```

:::tip
For the complete copy-pasteable flow (compute-budget instructions, payment instruction, and confirmation), see the [Solana Paymaster](/docs/products/infrastructure/paymaster/solana) docs and the [Solana sample app](https://github.com/openfort-xyz/openfort-js/tree/main/examples/apps/solana-sample).
:::
