> For the complete documentation index, see [llms.txt](https://docs-swap.crypto-chief.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs-swap.crypto-chief.com/guides/signing.md).

# Signing on Each Network

## Signing on Each Network

Every step of a [build](/api-reference/build.md) carries `transaction`: an unsigned transaction in the network's own format. You sign it with your own key and send it yourself — the API never asks for the key and never sends anything.

Execute steps in order, wait for `wait_for_confirmation`, and rebuild on `rebuild_after_previous` — the loop is in [Your First Swap](/guides/first-swap.md). This page is the signing code per network.

### EVM

The object for `eth_sendTransaction`: `from`, `to`, `data`, `value`, `chainId`, and `type: "0x2"` with `maxFeePerGas` / `maxPriorityFeePerGas` or `type: "0x0"` with `gasPrice`. Quantities are `0x` hex strings; `nonce` is absent — your wallet sets it; `gas` is present only when the API measured the step (otherwise the wallet estimates).

**ethers v6** — map the fields, quantities as `bigint`, `type` as a number, `gas` → `gasLimit`:

```javascript
import { Wallet } from "ethers";

function toEthers(tx) {
  const out = {
    to: tx.to,
    data: tx.data,
    value: BigInt(tx.value),
    chainId: BigInt(tx.chainId),
    type: Number(tx.type),
  };
  if (tx.gas !== undefined) out.gasLimit = BigInt(tx.gas);
  if (tx.maxFeePerGas !== undefined) {
    out.maxFeePerGas = BigInt(tx.maxFeePerGas);
    out.maxPriorityFeePerGas = BigInt(tx.maxPriorityFeePerGas);
  } else {
    out.gasPrice = BigInt(tx.gasPrice);
  }
  return out;
}

const sent = await wallet.sendTransaction(toEthers(step.transaction));
const receipt = await sent.wait(); // receipt.status === 1 means confirmed
```

**viem** — `value`, `gas` and the fee fields as `bigint` (`hexToBigInt` or `BigInt`). `walletClient` is created with `account` and `chain`; the chain comes from the client, so check it against `tx.chainId`:

```typescript
import { hexToBigInt, type Address, type Hex } from "viem";

const chainId = Number(tx.chainId);
if (chainId !== walletClient.chain?.id) throw new Error("wrong chain on the wallet client");

const hash = await walletClient.sendTransaction({
  to: tx.to as Address,
  data: tx.data as Hex,
  value: hexToBigInt(tx.value),
  ...(tx.gas !== undefined ? { gas: hexToBigInt(tx.gas) } : {}),
  ...(tx.maxFeePerGas !== undefined
    ? {
        maxFeePerGas: hexToBigInt(tx.maxFeePerGas),
        maxPriorityFeePerGas: hexToBigInt(tx.maxPriorityFeePerGas),
      }
    : { gasPrice: hexToBigInt(tx.gasPrice) }),
});
await publicClient.waitForTransactionReceipt({ hash });
```

**A browser wallet** takes the object unchanged:

```javascript
const hash = await window.ethereum.request({
  method: "eth_sendTransaction",
  params: [step.transaction],
});
```

{% hint style="warning" %}
In an [approve/transaction](/api-reference/approve-transaction.md) response without `owner`, `from` is absent — your wallet fills it from its own account. Never copy `from` between wallets: the allowance and the swap belong to the address in `from`.
{% endhint %}

### TRON

The body of `/wallet/triggersmartcontract`: `owner_address`, `contract_address`, `data` (full calldata, hex without `0x`), `call_value` and `fee_limit` in sun, `visible: true`. With TronWeb 5 or later (the `input` option):

```javascript
const tx = step.transaction;

const built = await tronWeb.transactionBuilder.triggerSmartContract(
  tx.contract_address,
  "",
  { input: tx.data, callValue: tx.call_value, feeLimit: tx.fee_limit },
  [],
  tx.owner_address,
);
const signed = await tronWeb.trx.sign(built.transaction);
const broadcast = await tronWeb.trx.sendRawTransaction(signed);
if (!broadcast.result) throw new Error(`broadcast refused: ${broadcast.code}`);
// then wait until tronWeb.trx.getTransactionInfo(broadcast.txid) shows the receipt
```

{% hint style="warning" %}
`fee_limit` is capped at 100 TRX (100,000,000 sun). A swap whose energy needs more than the cap buys fails with `OUT_OF_ENERGY` — the quote flags it with the risk factor `fee_limit_capped`. Renting energy for the wallet beforehand is the fix.
{% endhint %}

A Permit2 executor (`sun_universal_router`) produces two setup transactions — `approve` to the Permit2 contract, then `permit2_approve` — both signed exactly like this.

### Solana

`transaction` is `{encoding: "base64", transaction, version, last_valid_block_height}` — an unsigned transaction, `version: 0` or `"legacy"`. The fee payer and only signer is `from_address`:

```javascript
import { VersionedTransaction } from "@solana/web3.js";

const tx = step.transaction;
const transaction = VersionedTransaction.deserialize(Buffer.from(tx.transaction, "base64"));
transaction.sign([keypair]); // the keypair of from_address

const signature = await connection.sendRawTransaction(transaction.serialize());
await connection.confirmTransaction({
  signature,
  blockhash: transaction.message.recentBlockhash,
  lastValidBlockHeight: tx.last_valid_block_height,
});
```

{% hint style="warning" %}
The transaction already carries the compute-budget instructions (and, for `version: 0`, the route's lookup tables). **Do not add your own**: that makes it a different transaction than the one simulated. Sign or rebuild before `last_valid_block_height`: validity is about 60–90 seconds, and `deadline_seconds` does not apply on Solana.
{% endhint %}

### TON

`transaction` is a TON Connect request: `{validUntil, network ("-239" mainnet), from, messages: [{address, amount, payload}]}`. In a browser, pass it as is:

```javascript
await tonConnectUI.sendTransaction(step.transaction);
```

With a server wallet (`@ton/ton`), each message becomes an internal one — `address` passed as the string (so the bounce flag inside it is kept), `amount` as `bigint` nanoTON, `payload` as a base64 BoC cell, `validUntil` as the transfer timeout:

```typescript
import { Cell, SendMode, internal } from "@ton/ton";

const messages = step.transaction.messages.map((m) =>
  internal({
    to: m.address, // a string: internal() reads the bounce flag from it
    value: BigInt(m.amount),
    body: Cell.fromBase64(m.payload),
  }),
);
await wallet.sendTransfer({
  seqno: await wallet.getSeqno(),
  secretKey,
  messages,
  sendMode: SendMode.PAY_GAS_SEPARATELY | SendMode.IGNORE_ERRORS, // mode 3, as TON Connect wallets send
  timeout: step.transaction.validUntil,
});
```

{% hint style="warning" %}
`validUntil` is the swap deadline. TON settles asynchronously: the output arrives in **later messages**, not in this transaction. A rejected swap returns your input; a rejected second hop leaves the intermediate token — read the incoming messages to know the result. Keep `deadline_seconds` at most `300` ([why](/getting-started/how-a-swap-works.md#deadlines-per-network)).
{% endhint %}

### XRP Ledger

`transaction` is the transaction JSON itself — `TrustSet`, a `Payment` to yourself (`SendMax`, `DeliverMin`, `Flags: 131072` = `tfPartialPayment`), or a `Batch` when a [partner fee](/getting-started/partner-fees.md) is set. `Account`, `Fee`, `Sequence` and `LastLedgerSequence` are filled — sign as is with xrpl.js:

```typescript
const signed = wallet.sign(step.transaction); // wallet = xrpl.Wallet of Account
const res = await client.submitAndWait(signed.tx_blob);
```

To refresh `Fee`, `Sequence` or `LastLedgerSequence` (the transaction expired, or another transaction took the sequence), remove them — in a `Batch`, also the inner transactions' `Sequence` — and autofill again. xrpl.js autofill keeps fields that are already set, which is why removing them matters:

```typescript
const { Fee, Sequence, LastLedgerSequence, ...rest } = step.transaction;
if (rest.TransactionType === "Batch") {
  // In a Batch, the inner transactions' Sequence goes too.
  rest.RawTransactions = rest.RawTransactions.map(({ RawTransaction: { Sequence, ...t } }) => ({ RawTransaction: t }));
}
const refreshed = await client.autofill(rest);
```

The order of a first swap into a currency you have no trust line for: sign the `trust_set` step, wait for validation, then **rebuild** (the `swap` step carries `rebuild_after_previous: true`) and sign the `Payment` from the new build.

{% hint style="warning" %}
The result is not `Amount` — it is the `delivered_amount` of the validated transaction, anywhere between `DeliverMin` and `Amount`. A `Batch` applies both inner payments or none and its own result does not show them: check both inner transactions in the validated ledger. Signing a `Batch` needs a wallet library with XLS-56 (Batch) support.
{% endhint %}
