npm package: @rockerone/xprnkit
Description: > **⚠️ Warning:** Version 0.4.0 introduces breaking changes. Make sure to save your project before migrate.
Latest version: 0.5.1

# @rockerone/xprnkit

> **⚠️ Warning:** Version 0.4.0 introduces breaking changes. Make sure to save your project before migrate.

React SDK for building dApps on XPR Network. Provides wallet connection, identity proof with pluggable transports, multi-account management, token swap UI, and transaction helpers.

## Installation

```bash
npm install @rockerone/xprnkit
```

---

# XPRNProvider

Wrap your application with `XPRNProvider` to enable wallet and blockchain interactions.

```tsx
import { XPRNProvider } from '@rockerone/xprnkit';

const config = {
  chainId: "your-chain-id",
  endpoints: ["https://your-endpoint.com"],
  dAppName: "Your dApp Name",
  requesterAccount: "your-account",
  apiMode: "mainnet",
};

function App() {
  return (
    <XPRNProvider config={config}>
      {/* Your app */}
    </XPRNProvider>
  );
}
```

### XPRProviderConfig

| Property | Type | Required | Description |
|---|---|---|---|
| `chainId` | `string` | Yes | Blockchain chain ID |
| `endpoints` | `string[]` | Yes | RPC endpoints |
| `dAppName` | `string` | Yes | Display name for your dApp |
| `requesterAccount` | `string` | Yes | Your dApp's account name |
| `requesterLogo` | `string` | No | Logo URL for your dApp |
| `apiMode` | `"testnet" \| "mainnet"` | Yes | Network environment |
| `restoreSession` | `boolean` | No | Restore last active session on page refresh |
| `identityProof` | `XPRNKitIdentityProofConfig` | No | Identity proof configuration |

---

# useXPRN

Hook to access the provider context.

```tsx
import { useXPRN } from '@rockerone/xprnkit';

function MyComponent() {
  const {
    config,
    session,
    link,
    profile,
    identityProof,
    identityProofStatus,
    identityProofError,
    connect,
    disconnect,
    pushTransaction,
    switchToSession,
    requestIdentityProof,
  } = useXPRN();
}
```

### State

| Property | Type | Description |
|---|---|---|
| `config` | `XPRProviderConfig \| null` | Current provider configuration |
| `session` | `LinkSession \| null` | Active session |
| `link` | `ProtonWebLink \| Link \| null` | Proton link object |
| `profile` | `XPRNKitProfile \| null` | Profile for the active session |
| `identityProof` | `XPRNKitIdentityProof \| null` | Identity proof for the active session |
| `identityProofStatus` | `XPRNKitIdentityProofStatus` | Status of the identity proof process |
| `identityProofError` | `Error \| null` | Error from the last failed identity proof attempt |

### Methods

| Method | Description |
|---|---|
| `connect(restore?: boolean)` | Connect to wallet. Pass `true` to restore a previous session silently. |
| `disconnect()` | Disconnect the current session |
| `pushTransaction(actions: any[])` | Push a transaction with the active session |
| `switchToSession(actor, permission)` | Switch to a different stored session |
| `requestIdentityProof()` | Trigger the identity proof flow manually. Can retry after error. |

### Transaction Example

```tsx
const { session, pushTransaction } = useXPRN();

await pushTransaction([
  {
    account: "eosio.token",
    name: "transfer",
    authorization: [session.auth],
    data: {
      from: session.auth.actor.toString(),
      to: "recipient",
      quantity: "1.0000 XPR",
      memo: "Hello!",
    },
  },
]);
```

---

# Identity Proof

Verify wallet ownership by having users sign a message. Supports HTTP, WebSocket, and custom transports.

### Flow

1. When `required` is `true`, identity proof is triggered automatically on wallet connect.
2. The provider first checks localStorage for an existing proof and validates it.
3. If no valid proof exists, the user is prompted to sign a message.
4. The signed message is sent to your backend for verification.
5. The backend returns a JWT token, stored in localStorage and exposed via `useXPRN().identityProof`.
6. On failure, `identityProofStatus` is set to `"error"`. Call `requestIdentityProof()` to retry.

### XPRNKitIdentityProofConfig

| Property | Type | Required | Description |
|---|---|---|---|
| `required` | `boolean` | Yes | Auto-trigger on connect |
| `transport` | `TransportConfig` | No | Transport configuration (defaults to HTTP) |
| `createUrl` | `string` | No | HTTP endpoint for signature verification |
| `validationUrl` | `string` | No | HTTP endpoint for token validation |
| `validationBuffer` | `number` | No | Seconds before expiry to trigger revalidation |
| `headers` | `Record<string, string>` | No | Custom headers for requests |
| `timeout` | `number` | No | Request timeout in milliseconds |
| `createIdentityResponseTransformer` | `(response: any) => any` | No | Transform create response |
| `validationIdentityResponseTransformer` | `(response: any) => any` | No | Transform validation response |

## HTTP Transport (Default)

```typescript
const config = {
  identityProof: {
    required: true,
    createUrl: "/api/auth/authorize",
    validationUrl: "/api/auth/validate",
  },
};
```

## WebSocket Transport

```typescript
const config = {
  identityProof: {
    required: true,
    transport: {
      type: 'websocket',
      options: {
        url: 'wss://api.example.com/identity',
        timeout: 15000,
        reconnect: true,
        reconnectInterval: 5000,
        maxReconnectAttempts: 5,
        maxReconnectDelay: 30000,
      },
    },
  },
};
```

### WebSocket Message Format

```typescript
// Client -> Server
{ id: string, action: 'createIdentityProof' | 'validateIdentityProof', data: any }

// Server -> Client
{ id: string, action: string, data: any, error?: string }
```

## Custom Transport

```typescript
import { IdentityProofTransport, TransportFactory } from '@rockerone/xprnkit';

class GraphQLTransport implements IdentityProofTransport {
  name = 'graphql';

  async createIdentityProof(payload, config, signal) {
    const response = await fetch('/graphql', {
      method: 'POST',
      body: JSON.stringify({
        query: CREATE_IDENTITY_PROOF_MUTATION,
        variables: { input: payload },
      }),
      signal,
    });
    return response.json();
  }

  async validateIdentityProof(proof, config, signal) {
    return true;
  }

  dispose() {}
}

TransportFactory.register('graphql', new GraphQLTransport());

const config = {
  identityProof: {
    required: true,
    transport: { type: 'custom', options: { name: 'graphql' } },
  },
};
```

## Backend Endpoints

### Authorization (`createUrl`)

```typescript
// Request body
{
  signer: { actor: string, permission: string },
  transaction: any,
  signatures: string[],
  chainId: string,
}

// Response
{
  success: true,
  validated: true,
  actor: string,
  permission: string,
  token: string,
  timestamp: string,
}
```

### Validation (`validationUrl`)

```typescript
// Request: Authorization header with "Bearer <token>" or body { token: string }

// Response
{ valid: boolean, actor?: string, expiresAt?: string, error?: string }
```

## XPRNIdentityProofGate

Conditionally render content based on identity proof status.

```tsx
import { XPRNIdentityProofGate, useXPRN } from '@rockerone/xprnkit';

function ProtectedContent() {
  const { identityProofError, requestIdentityProof } = useXPRN();

  return (
    <XPRNIdentityProofGate
      fallback={<p>Please verify your identity.</p>}
      loading={<p>Verifying...</p>}
      error={
        <div>
          <p>Failed: {identityProofError?.message}</p>
          <button onClick={requestIdentityProof}>Retry</button>
        </div>
      }
      notConnected={<p>Please connect your wallet first.</p>}
    >
      <p>Verified content.</p>
    </XPRNIdentityProofGate>
  );
}
```

| Prop | Type | Description |
|---|---|---|
| `children` | `ReactNode` | Content when identity proof is obtained |
| `fallback` | `ReactNode` | Content when identity proof is not obtained |
| `loading` | `ReactNode` | Content while in progress |
| `error` | `ReactNode` | Content on failure (falls back to `fallback`) |
| `notConnected` | `ReactNode` | Content when no session (falls back to `fallback`) |

## useIdentityProofGate

Hook version for programmatic access.

```tsx
import { useIdentityProofGate } from '@rockerone/xprnkit';

const {
  isGateActive,
  isConnected,
  isInProgress,
  hasError,
  isVerified,
  shouldRenderChildren,
  needsIdentityProof,
  identityProofStatus,
} = useIdentityProofGate();
```

---

# Multi-Account Support

Connect multiple wallet accounts. The provider maintains one active session at a time. Use `switchToSession(actor, permission)` to switch between stored accounts.

```tsx
const { session, switchToSession } = useXPRN();

await switchToSession("otheraccount", "active");
```

---

# Components

## XPRNConnectButton

```tsx
import { XPRNConnectButton } from '@rockerone/xprnkit';

<XPRNConnectButton
  onSession={(session, link) => {}}
  onProfile={(profile) => {}}
  onIdentityProof={(proof) => {}}
>
  Connect Wallet
</XPRNConnectButton>
```

Shows "Connect" when disconnected, "Log out (actor)" when connected. Accepts standard button props, `variant`, `size`, `asChild`, and `className`.

## XPRNIdentity

Dropdown showing user identity with avatar and session info.

```tsx
import { XPRNIdentity } from '@rockerone/xprnkit';

<XPRNIdentity showLogout avatarClassName="custom-avatar">
  <div>Dropdown content</div>
</XPRNIdentity>
```

| Prop | Type | Description |
|---|---|---|
| `showLogout` | `boolean` | Show logout link |
| `activeSessionClassName` | `string` | CSS for active session container |
| `dropdownClassName` | `string` | CSS for dropdown |
| `avatarClassName` | `string` | CSS for avatar |
| `matchDropdownWidth` | `boolean` | Match dropdown width to trigger (default: `true`) |
| `closeOnSelect` | `boolean` | Close dropdown on click (default: `true`) |
| `onSession` | `(session, link) => void` | Session callback |
| `onProfile` | `(profile) => void` | Profile callback |
| `onIdentityProof` | `(proof) => void` | Identity proof callback |

Sub-components: `XPRNAvatar`, `XPRNSessionActor`, `XPRNSessionName`.

## XPRNAccountList

Lists stored accounts with switch, add, and remove functionality.

```tsx
import { XPRNIdentity, XPRNAccountList } from '@rockerone/xprnkit';

<XPRNIdentity showLogout>
  <XPRNAccountList
    showAddAccount
    showRemove
    onSelect={(entry) => {}}
  />
</XPRNIdentity>
```

| Prop | Type | Description |
|---|---|---|
| `showRemove` | `boolean` | Show remove button per account |
| `showAddAccount` | `boolean` | Show "Add Account" button |
| `addAccountLabel` | `string` | Custom add account label |
| `renderItem` | `(props: AccountItemRenderProps) => ReactNode` | Custom item renderer |
| `onSelect` | `(entry: XPRNKitProfileStorageEntry) => void` | Selection callback |
| `onRemove` | `(entry: XPRNKitProfileStorageEntry) => void` | Removal callback |
| `onAddAccount` | `() => void` | Add account callback |

## XRPNContainer / XPRNLogged / XPRNUnlogged

Conditional rendering based on session state.

```tsx
import { XRPNContainer, XPRNLogged, XPRNUnlogged } from '@rockerone/xprnkit';

<XRPNContainer noSessionState={<p>Please log in.</p>}>
  <p>Welcome!</p>
</XRPNContainer>

<XPRNLogged>
  <p>Visible when connected.</p>
</XPRNLogged>

<XPRNUnlogged>
  <p>Visible when disconnected.</p>
</XPRNUnlogged>
```

## XPRNTransaction

Button that pushes a blockchain transaction.

```tsx
import { XPRNTransaction } from '@rockerone/xprnkit';

<XPRNTransaction
  actions={actions}
  onTransactionSuccess={(res) => {}}
  onTransactionFail={(err) => {}}
>
  Swap
</XPRNTransaction>
```

Shows "Connect" when no session. Accepts `actions`, `onTransactionStart`, `onTransactionSuccess`, `onTransactionFail`, `variant`, `size`, `asChild`, and `className`.

---

# Swap Components

## XPRNSwap

Complete swap interface out of the box.

```tsx
import { XPRNSwap } from '@rockerone/xprnkit';

<XPRNSwap />

<XPRNSwap
  filters={{ quoteSymbol: "XUSDC" }}
  sides={['buy', 'sell']}
/>
```

## Custom Swap Layout

```tsx
import {
  XPRNSwapProvider,
  XPRNPairsSelector,
  XPRNSwapFieldsGroup,
  XPRNSwapField,
  XPRNSwapSideButton,
  XPRNTransaction,
} from '@rockerone/xprnkit';

<XPRNSwapProvider config={{ filters: { quoteSymbol: "SNIPS", baseSymbol: "XUSDC" } }}>
  <XPRNPairsSelector />
  <XPRNSwapFieldsGroup horizontal>
    <XPRNSwapField />
    <XPRNSwapSideButton horizontal>Swap</XPRNSwapSideButton>
    <XPRNSwapField />
  </XPRNSwapFieldsGroup>
  <XPRNTransaction>Swap</XPRNTransaction>
</XPRNSwapProvider>
```

### useXPRNSwap

| Property/Method | Type | Description |
|---|---|---|
| `marketProviders` | `XPRNMarketProvider[]` | Available market providers |
| `currentMarketProvider` | `XPRNMarketProvider \| null` | Selected market provider |
| `setCurrentMarketProvider` | `(mp: XPRNMarketProvider) => void` | Set market provider |
| `refreshMarketPairs` | `() => void` | Refresh pairs |
| `currentMarketPairs` | `XPRNMarketProviderResult[]` | Current pairs |
| `refreshStatus` | `ServiceStatus` | Refresh status |
| `currentSwapPair` | `XPRNMarketProviderResult \| null` | Selected pair |
| `setCurrentSwapPair` | `(pair: XPRNMarketProviderResult) => void` | Set pair |
| `swapSide` | `XPRNSwapSide` | Current side (`"buy"` or `"sell"`) |
| `setSwapSide` | `(side: XPRNSwapSide) => void` | Set side |
| `swapVolume` | `number` | Current volume |
| `setSwapVolume` | `(volume: number) => void` | Set volume |
| `swapValues` | `XPRNSwapValues` | Current swap values |
| `updateSwapValues` | `(base, quote, lastMutated) => void` | Update values |
| `swapTransaction` | `any[]` | Current transaction actions |

### Sub-components

| Component | Description |
|---|---|
| `XPRNSwapProvider` | Context provider for swap state. Props: `config?: XPRNSwapProviderConfig` |
| `XPRNSwapFieldsGroup` | Groups swap fields, flips on side change. Props: `horizontal?: boolean` |
| `XPRNSwapField` | Token amount input. Props: `type?: "base" \| "quote"` |
| `XPRNSwapSideButton` | Toggles buy/sell side. Props: `horizontal?: boolean` |
| `XPRNSwapMarketsSelector` | Market provider dropdown (renders only if >1 provider) |
| `XPRNPairsSelector` | Trading pair dropdown. Props: `contentClassName`, `itemsClassName` |

---

# Types Reference

```typescript
type XPRProviderConfig = {
  chainId: string;
  endpoints: string[];
  dAppName: string;
  requesterAccount: string;
  requesterLogo?: string;
  apiMode: "testnet" | "mainnet";
  restoreSession?: boolean;
  identityProof?: XPRNKitIdentityProofConfig;
};

type XPRNKitIdentityProofConfig = {
  required: boolean;
  transport?: TransportConfig;
  createUrl?: string;
  validationUrl?: string;
  createIdentityResponseTransformer?: (response: any) => any;
  validationIdentityResponseTransformer?: (response: any) => any;
  validationBuffer?: number;
  headers?: Record<string, string>;
  timeout?: number;
};

type TransportConfig = {
  type: 'http' | 'websocket' | 'custom';
  transport?: IdentityProofTransport;
  options?: Record<string, any>;
};

type WebSocketOptions = {
  url: string;
  timeout?: number;
  reconnect?: boolean;
  reconnectInterval?: number;
  maxReconnectAttempts?: number;
  maxReconnectDelay?: number;
};

type XPRNKitIdentityProof = {
  auth: { actor: string; permission: string };
  token: string;
  metadata: { [key: string]: any };
};

type XPRNKitIdentityProofPayload = {
  signer: { actor: string; permission: string };
  transaction: any;
  signatures: string[];
  chainId: string;
};

type XPRNKitIdentityProofStatus =
  | "idle" | "signing" | "verifying"
  | "validating" | "success" | "expired" | "error";

type XPRNKitProfile = {
  displayName: string;
  avatar?: string;
  isKyc: boolean;
};

type XPRNKitProfileStorageEntry = {
  auth: { actor: string; permission: string };
  chainId: string;
  profile: XPRNKitProfile;
};

type ServiceStatus = 'idle' | 'pending' | 'success' | 'fail';
```

---

# Utilities

## toPrecision

Format a number to a given precision.

```typescript
function toPrecision(
  value: number,
  precision: number,
  mode?: "ceil" | "floor" | "round" | "none",
  forceDecimal?: boolean
): string;
```

```typescript
toPrecision(3.14159, 2)              // "3.15"
toPrecision(3.14159, 2, "floor")     // "3.14"
toPrecision(3, 2, "none", true)      // "3.00"
toPrecision(3, 2, "ceil", false)     // "3"
```