# Signing Intents

An intent is seven fields, signed with EIP-712. The trade's business parameters are not in the
signed struct directly — they are ABI-encoded into `actionData` and included as a hash.

Two rules account for almost every signature failure:

1. `actionData` is encoded **with `trader` prepended, then the first 32 bytes stripped**.
2. All actions share **one** field list; the action is identified by the `primaryType` name.

## Domain

```ts
const domain = {
  name: 'LeverupOneClickV2',
  version: '1',
  chainId: 143,
  verifyingContract: '0xea1b8E4aB7f14F7dCA68c5B214303B13078FC5ec', // the Diamond
} as const
```

::: warning
`verifyingContract` is the **Diamond**. There is no separate agent contract in this version.
:::

## Message type

```ts
const COMMON_EIP712_FIELDS = [
  { name: 'trader',         type: 'address' },
  { name: 'action',         type: 'uint8'   },
  { name: 'nonce',          type: 'uint64'  },
  { name: 'deadline',       type: 'uint48'  },
  { name: 'feeToken',       type: 'address' },
  { name: 'antiDdosFee',    type: 'uint96'  },
  { name: 'actionDataHash', type: 'bytes32' },
] as const
```

Identical for all fourteen actions. The type *string* is:

```
OneClickXxx(address trader,uint8 action,uint64 nonce,uint48 deadline,address feeToken,uint96 antiDdosFee,bytes32 actionDataHash)
```

where `OneClickXxx` is the action's [type name](/gasless/actions#eip-712-type-names). Two intents
with identical fields but different `primaryType` produce different signatures — that is what binds a
signature to an action.

## Building `actionData`

```
actionData = abi.encode(trader, ...actionParams).slice(32 bytes)
```

Encode `trader` as the first parameter, then drop the first 32-byte word.

**Why.** The contract reconstructs the calldata by concatenating the function selector, the encoded
`trader`, and your `actionData`:

```solidity
bytes memory cd = bytes.concat(selector, abi.encode(intent.trader), intent.actionData);
address(this).call(cd);
```

For actions containing dynamic types — arrays and structs — the ABI offset pointers must be computed
against a layout that *includes* the `trader` word. Encoding the parameters alone produces offsets
that are one word too small, and the contract decodes garbage.

For purely static actions (0–7, 9, 10, 13) the result is byte-identical either way. Use the formula
uniformly and you will never hit the edge case.

### Implementation

```ts
import { encodeAbiParameters, parseAbiParameter, type Hex, type Address } from 'viem'

const ACTION_DATA_ABI_TYPES: Record<number, readonly string[]> = {
  0:  ['address','bool','address','address','uint96','uint128','uint128','uint128','uint128','uint24','uint96'],
  1:  ['bytes32','uint24'],
  2:  ['address','bool','address','address','uint96','uint128','uint128','uint128','uint128','uint24','uint96'],
  3:  ['bytes32'],
  4:  ['bytes32','uint128','uint128'],
  5:  ['bytes32','address','uint96'],
  6:  ['bytes32','uint96'],
  7:  ['bytes32','uint128','uint128'],
  8:  ['bytes32[]','uint24'],
  9:  ['bytes32','uint128','uint24'],
  10: ['bytes32'],
  11: ['bytes32', '(uint8,uint128,uint128,uint24)[]'],
  12: ['(bytes32,uint128,uint128,uint24)[]'],
  13: ['bytes32'],
}

const strip = (hex: Hex): Hex => `0x${hex.slice(2 + 64)}` as Hex

export function buildActionData(action: number, trader: Address, values: unknown[]): Hex {
  // Tuple arrays need parseAbiParameter — viem does not accept them as plain type strings.
  if (action === 11) {
    return strip(encodeAbiParameters(
      [{ type: 'address' }, { type: 'bytes32' }, parseAbiParameter('(uint8,uint128,uint128,uint24)[]')],
      [trader, values[0] as Hex, values[1] as any],
    ))
  }
  if (action === 12) {
    return strip(encodeAbiParameters(
      [{ type: 'address' }, parseAbiParameter('(bytes32,uint128,uint128,uint24)[]')],
      [trader, values[0] as any],
    ))
  }

  const types = ACTION_DATA_ABI_TYPES[action]
  if (!types) throw new Error(`Unknown action: ${action}`)

  return strip(encodeAbiParameters(
    ['address', ...types].map((t) => ({ type: t })),
    [trader, ...values] as any,
  ))
}
```

With ethers v6:

```ts
import { AbiCoder } from 'ethers'

const coder = AbiCoder.defaultAbiCoder()
const full = coder.encode(['address', ...abiTypes], [trader, ...values])
const actionData = '0x' + full.slice(2 + 64)
```

### Self-check

If you are debugging a dynamic action, compare against these known-good offsets:

| Action | Offset value in the encoding | Array starts at byte |
| :--- | :--- | :--- |
| `BATCH_MARKET_CLOSE` (8) | `0x60` | `0x40` |
| `BATCH_CREATE_DECREASE_ORDERS` (11) | `0x60` | `0x40` |
| `BATCH_UPDATE_DECREASE_ORDERS` (12) | `0x40` | `0x20` |

An offset one word smaller than the table means the `trader` prefix was omitted.

## Nonce and deadline

```ts
const nonce = BigInt(Date.now())                       // milliseconds, uint64
const deadline = Math.floor(Date.now() / 1000) + 300   // seconds, uint48
```

Constraints and the high-frequency caveat are covered in
[Security Model → Nonce and deadline](/gasless/security#nonce-and-deadline).

## Signing

```ts
import { keccak256 } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'

const ACTION_TYPE_NAMES: Record<number, string> = {
  0: 'OneClickMarketOpen',   1: 'OneClickMarketClose',
  2: 'OneClickLimitOpen',    3: 'OneClickLimitCancel',
  4: 'OneClickLimitUpdateTpSl', 5: 'OneClickAddMargin',
  6: 'OneClickRemoveMargin', 7: 'OneClickUpdateTpSl',
  8: 'OneClickBatchMarketClose', 9: 'OneClickPartialClose',
  10: 'OneClickCancelDecreaseOrder',
  11: 'OneClickBatchCreateDecreaseOrders',
  12: 'OneClickBatchUpdateDecreaseOrders',
  13: 'OneClickCancelAllDecreaseOrders',
}

const signer = privateKeyToAccount(signingKey)
const typeName = ACTION_TYPE_NAMES[action]

const actionData = buildActionData(action, trader, actionValues)
const actionDataHash = keccak256(actionData)

const signature = await signer.signTypedData({
  domain,
  types: { [typeName]: COMMON_EIP712_FIELDS },
  primaryType: typeName,
  message: {
    trader,
    action,
    nonce,
    deadline,
    feeToken,
    antiDdosFee,
    actionDataHash,
  },
})
```

`signTypedData` on a local account is silent — no popup. That is the whole point of the browser-key
and hosted-agent modes. [Self-signing](/gasless/overview#integration-modes) through a browser wallet
works too, but prompts the user each time.

## Worked example — market open

```ts
import { parseUnits, keccak256 } from 'viem'

const actionValues = [
  pairBase,                            // pairBase
  true,                                // isLong
  USDC,                                // tokenIn
  LVUSD,                               // lvToken
  parseUnits('10', 6),                 // amountIn
  parseUnits((100 / 100_000).toFixed(10), 10), // qty — $100 of BTC at $100k
  parseUnits('101000', 18),            // price — worst acceptable
  0n,                                  // stopLoss
  0n,                                  // takeProfit
  0,                                   // broker — 0 credits the default broker
  0n,                                  // extraFee — optional surcharge, see /introduction/brokers
]

const actionData = buildActionData(0, trader, actionValues)
const actionDataHash = keccak256(actionData)
```

Then sign as above and [submit](/gasless/submit).

## Worked example — TP/SL orders

```ts
// TP at $110k for half the position, SL at $90k for all of it.
const actionValues = [
  positionHash,
  [
    [0, parseUnits('110000', 18), position.qty / 2n, 0],  // kind 0 = TP
    [1, parseUnits('90000', 18), position.qty, 0],        // kind 1 = SL
  ],
]

const actionData = buildActionData(11, trader, actionValues)
```

Note the tuple array is passed as arrays of positional values, not objects.

## Debugging checklist

Persistent `skipReason = INVALID` with a signature that looks correct usually means one of:

- The EIP-712 struct expands the business fields instead of using `actionDataHash`.
- `primaryType` does not match the action.
- `verifyingContract` is not the Diamond address.
- The agent is not authorized, or lacks the permission bit for this action.

Batch actions (8, 11, 12) that decode as garbage onchain mean the `trader` prefix was left out of
`actionData`.

Next: [Execution Fee](/gasless/fees).
