# Reference Client

A complete, dependency-light TypeScript client covering fee selection, `actionData` encoding, EIP-712
signing, submission and polling. Copy it into your project and adapt — there is no npm package to
install.

Requires `viem`.

## `leverup-1ct.ts`

```ts
import {
  encodeAbiParameters,
  parseAbiParameter,
  keccak256,
  type Hex,
  type Address,
} from 'viem'
import { privateKeyToAccount } from 'viem/accounts'

// ───────────────────────── constants ─────────────────────────

export const OneClickAction = {
  MARKET_OPEN: 0,
  MARKET_CLOSE: 1,
  LIMIT_OPEN: 2,
  LIMIT_CANCEL: 3,
  LIMIT_UPDATE_TP_SL: 4,
  ADD_MARGIN: 5,
  REMOVE_MARGIN: 6,
  UPDATE_TP_SL: 7,
  BATCH_MARKET_CLOSE: 8,
  PARTIAL_CLOSE: 9,
  CANCEL_DECREASE_ORDER: 10,
  BATCH_CREATE_DECREASE_ORDERS: 11,
  BATCH_UPDATE_DECREASE_ORDERS: 12,
  CANCEL_ALL_DECREASE_ORDERS: 13,
} as const

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 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 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

const ZERO = '0x0000000000000000000000000000000000000000' as Address

// ───────────────────────── actionData ─────────────────────────

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

/** actionData = abi.encode(trader, ...params) with the leading trader word removed. */
export function buildActionData(action: number, trader: Address, values: unknown[]): Hex {
  if (action === OneClickAction.BATCH_CREATE_DECREASE_ORDERS) {
    return strip(encodeAbiParameters(
      [{ type: 'address' }, { type: 'bytes32' }, parseAbiParameter('(uint8,uint128,uint128,uint24)[]')],
      [trader, values[0] as Hex, values[1] as any],
    ))
  }

  if (action === OneClickAction.BATCH_UPDATE_DECREASE_ORDERS) {
    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,
  ))
}

// ───────────────────────── fee config ─────────────────────────

export type FeeConfig = {
  action: number
  actionName: string
  feeToken: string
  antiDdosFee: string
  enabled: boolean
  priority: number
}

let feeConfigCache: FeeConfig[] = []

export async function refreshFeeConfig(baseUrl: string) {
  const res = await fetch(`${baseUrl}/v2/trading/anti-ddos-config`)
  if (!res.ok) throw new Error(`anti-ddos config: HTTP ${res.status}`)
  feeConfigCache = await res.json()
}

/** Call once at startup. Keep the handle so you can clearInterval on teardown. */
export function startFeeConfigRefresh(baseUrl: string) {
  void refreshFeeConfig(baseUrl).catch(console.warn)
  return setInterval(() => void refreshFeeConfig(baseUrl).catch(console.warn), 60_000)
}

/**
 * Lowest-priority fee token the trader can actually pay.
 * `getAccountState` must read from a cache — do not issue RPC calls here.
 * `additionalSpends` must include what the trade itself spends, or the fee
 * transfer can fail onchain even though selection succeeded.
 */
export function selectFeeToken(
  action: number,
  getAccountState: (token: Address) => { balance: bigint; allowance: bigint } | undefined,
  additionalSpends: readonly { token: Address; amount: bigint }[] = [],
): { feeToken: Address; antiDdosFee: bigint } {
  const options = feeConfigCache
    .filter((c) => c.action === action && c.enabled)
    .sort((a, b) => a.priority - b.priority)

  if (options.length === 0) return { feeToken: ZERO, antiDdosFee: 0n }

  for (const opt of options) {
    const token = opt.feeToken as Address
    const state = getAccountState(token)
    if (!state) continue

    const extra = additionalSpends
      .filter((s) => s.token.toLowerCase() === token.toLowerCase())
      .reduce((sum, s) => sum + s.amount, 0n)
    const required = BigInt(opt.antiDdosFee) + extra

    if (state.balance >= required && state.allowance >= required) {
      return { feeToken: token, antiDdosFee: BigInt(opt.antiDdosFee) }
    }
  }

  throw new Error('No fee token with sufficient balance and allowance')
}

// ───────────────────────── nonce ─────────────────────────

let lastNonce = 0n

/** Strictly monotonic, so two intents in the same millisecond do not collide. */
export function nextNonce(): bigint {
  const now = BigInt(Date.now())
  lastNonce = now > lastNonce ? now : lastNonce + 1n
  return lastNonce
}

// ───────────────────────── sign + submit ─────────────────────────

export interface SubmitOpts {
  baseUrl: string
  chainId: number
  diamond: Address
  trader: Address
  signerPrivateKey: Hex
  action: number
  actionValues: unknown[]
  feeToken: Address
  antiDdosFee: bigint
  deadlineSeconds?: number
}

export async function signAndSubmitIntent(o: SubmitOpts): Promise<string> {
  const typeName = ACTION_TYPE_NAMES[o.action]
  if (!typeName) throw new Error(`Unknown action: ${o.action}`)

  const signer = privateKeyToAccount(o.signerPrivateKey)
  const nonce = nextNonce()
  const deadline = Math.floor(Date.now() / 1000) + (o.deadlineSeconds ?? 300)

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

  const signature = await signer.signTypedData({
    domain: {
      name: 'LeverupOneClickV2',
      version: '1',
      chainId: o.chainId,
      verifyingContract: o.diamond,
    },
    types: { [typeName]: COMMON_EIP712_FIELDS },
    primaryType: typeName,
    message: {
      trader: o.trader,
      action: o.action,
      nonce,
      deadline,
      feeToken: o.feeToken,
      antiDdosFee: o.antiDdosFee,
      actionDataHash,
    },
  } as any)

  const res = await fetch(`${o.baseUrl}/v2/trading/submit-intent?blockchain=MONAD`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      trader: o.trader,
      action: o.action,
      nonce: nonce.toString(),          // must be a string
      deadline,
      feeToken: o.feeToken,
      antiDdosFee: o.antiDdosFee.toString(),
      actionData,
      signature,
    }),
  })

  if (!res.ok) throw new Error(`submit-intent ${res.status}: ${await res.text()}`)

  return (await res.text()).replace(/^"|"$/g, '')
}

// ───────────────────────── polling ─────────────────────────

export interface IntentStatus {
  submitted: boolean
  executed: boolean
  success: boolean | null
  skipped: boolean
  skipReason: string | null
  txnHash: string
  reason: string
}

export async function pollIntentStatus(
  baseUrl: string,
  intentHash: string,
  { interval = 300, timeout = 300_000 } = {},
): Promise<IntentStatus> {
  const until = Date.now() + timeout

  while (Date.now() < until) {
    const res = await fetch(`${baseUrl}/v2/trading/${intentHash}/status`)
    if (res.ok) {
      const status: IntentStatus = await res.json()
      if (status.executed || status.skipped) return status
    }
    await new Promise((r) => setTimeout(r, interval))
  }

  throw new Error('Timed out waiting for intent execution')
}
```

## Usage

```ts
import { parseUnits } from 'viem'
import {
  OneClickAction,
  startFeeConfigRefresh,
  selectFeeToken,
  signAndSubmitIntent,
  pollIntentStatus,
} from './leverup-1ct'

const BASE_URL = 'https://oneclick-01-keeper.leverup.xyz'
const DIAMOND = '0xea1b8E4aB7f14F7dCA68c5B214303B13078FC5ec' as const
const CHAIN_ID = 143

startFeeConfigRefresh(BASE_URL)
```

### Market open

```ts
const amountIn = parseUnits('10', 6)
const BROKER_ID = 0          // your assigned id, see /introduction/brokers
const extraFee = 0n          // optional surcharge in tokenIn

const actionValues = [
  pairBase,
  true,                                          // isLong
  USDC,                                          // tokenIn
  LVUSD,                                         // lvToken
  amountIn,
  parseUnits((100 / markPrice).toFixed(10), 10), // qty — $100 notional
  parseUnits(String(markPrice * 1.01), 18),      // worst acceptable price
  0n,                                            // stopLoss
  0n,                                            // takeProfit
  BROKER_ID,                                     // 0 credits the default broker
  extraFee,                                      // optional surcharge, 0n if unused
]

const { feeToken, antiDdosFee } = selectFeeToken(
  OneClickAction.MARKET_OPEN,
  getCachedErc20State,
  // The trade's own spend — include extraFee, or the fee transfer can fail onchain.
  [{ token: USDC, amount: amountIn + extraFee }],
)

const intentHash = await signAndSubmitIntent({
  baseUrl: BASE_URL,
  chainId: CHAIN_ID,
  diamond: DIAMOND,
  trader: userAddress,
  signerPrivateKey: oneClickPrivateKey,
  action: OneClickAction.MARKET_OPEN,
  actionValues,
  feeToken,
  antiDdosFee,
})

const status = await pollIntentStatus(BASE_URL, intentHash)
console.log(status.success ? 'filled' : `failed: ${status.skipReason ?? status.reason}`)
```

### Close

```ts
const { feeToken, antiDdosFee } = selectFeeToken(OneClickAction.MARKET_CLOSE, getCachedErc20State)

await signAndSubmitIntent({
  baseUrl: BASE_URL, chainId: CHAIN_ID, diamond: DIAMOND,
  trader: userAddress,
  signerPrivateKey: oneClickPrivateKey,
  action: OneClickAction.MARKET_CLOSE,
  actionValues: [positionHash, BROKER_ID],   // [positionHash, broker]
  feeToken, antiDdosFee,
})
```

### Partial close

```ts
await signAndSubmitIntent({
  /* … */
  action: OneClickAction.PARTIAL_CLOSE,
  actionValues: [positionHash, position.qty / 2n, 0],
})
```

### TP/SL orders

```ts
await signAndSubmitIntent({
  /* … */
  action: OneClickAction.BATCH_CREATE_DECREASE_ORDERS,
  actionValues: [
    positionHash,
    [
      [0, parseUnits('110000', 18), position.qty / 2n, 0],  // TP, half
      [1, parseUnits('90000', 18), position.qty, 0],        // SL, all
    ],
  ],
})
```

## Before going live

- Fee config fetched at startup and refreshed every 60 seconds.
- `balanceOf` and `allowance(trader, diamond)` prefetched for every enabled fee token.
- Fee-token selection includes the trade's own spend, `extraFee` included.
- If you run a [broker integration](/introduction/brokers), your id is passed on opens, closes, and
  every TP/SL order — not just the open.
- Trader has approved the Diamond for both collateral and fee tokens.
- `nonce` transmitted as a string.
- `actionData` built with the `trader` prefix stripped.
- EIP-712 uses the shared seven-field struct with the correct `primaryType` per action.
- The authorization signing prompt states the domain, the connected address, and that the signature
  authorizes no transfer.
- Resetting 1CT revokes onchain and waits for the receipt before clearing local key material.
- Polling has a timeout, and `reason` is decoded into readable text.
