{"_id":"@arcanahq/sdk","name":"@arcanahq/sdk","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@arcanahq/sdk","version":"0.1.0","description":"General-purpose TypeScript SDK for building frontends that interact with deployed Arcana contracts","license":"MIT","type":"module","publishConfig":{"access":"public"},"main":"./src/index.ts","types":"./src/index.ts","exports":{".":{"import":"./src/index.ts","types":"./src/index.ts","default":"./src/index.ts"},"./subscriptions":{"import":"./src/subscriptions/index.ts","types":"./src/subscriptions/index.ts","default":"./src/subscriptions/index.ts"},"./devtools":{"import":"./src/devtools/index.tsx","types":"./src/devtools/index.tsx","default":"./src/devtools/index.tsx"}},"scripts":{"build":"tsc","dev":"tsc --watch","test":"vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage","prepublishOnly":"npm run build"},"dependencies":{"@msgpack/msgpack":"^3.1.3","@noble/ed25519":"^2.3.0","@noble/hashes":"^1.4.0","axios":"^1.6.0","cbor-x":"^1.6.0"},"devDependencies":{"@tanstack/react-query":"^5.0.0","@types/node":"^20.10.0","@types/react":"^18.3.27","tsx":"^4.21.0","typescript":"^5.3.0","viem":"^2.0.0","vitest":"^4.1.7"},"peerDependencies":{"@tanstack/react-query":"^5.0.0","react":"^18.0.0","viem":"^2.0.0"},"gitHead":"c9e8a602e25807fbcc843c2419f89a89a93bdc25","_id":"@arcanahq/sdk@0.1.0","_nodeVersion":"25.2.1","_npmVersion":"11.6.2","dist":{"integrity":"sha512-uw149zzlJ/2qFEm24X2lAmq0v6Wd/99n02+4Rp128KDdfqgu4BPSmpLcILxaqqqbJ6w5K25OQ4oguGzIYGBFJw==","shasum":"ff0c1567dd34aef73f1dad59693a98095f62ca6a","tarball":"https://registry.npmjs.org/@arcanahq/sdk/-/sdk-0.1.0.tgz","fileCount":80,"unpackedSize":602997,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCQ042HE2c96GWDY/xXwlqOxmVG6YqvFlkYnIcKG1HdEwIgHLoMik/UNVBOWsgkg/vFhT+yC6ISVf6HEr8RoHEMVaw="}]},"_npmUser":{"name":"tapone","email":"johnny@empyrealsdk.com"},"directories":{},"maintainers":[{"name":"tapone","email":"johnny@empyrealsdk.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/sdk_0.1.0_1782368328491_0.39636995022534105"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-25T06:18:48.336Z","0.1.0":"2026-06-25T06:18:48.649Z","modified":"2026-06-25T06:18:48.868Z"},"maintainers":[{"name":"tapone","email":"johnny@empyrealsdk.com"}],"description":"General-purpose TypeScript SDK for building frontends that interact with deployed Arcana contracts","license":"MIT","readme":"# Arcana Frontend SDK\n\nA general-purpose TypeScript SDK for building frontends that interact with deployed Arcana contracts.\n\n## Installation\n\n```bash\nnpm install @arcanahq/sdk viem\n```\n\n**Note**: `viem` is a peer dependency and must be installed separately.\n\n## Testing\n\nSee [TESTING.md](./TESTING.md) for detailed testing instructions, including how to run E2E tests with Docker.\n\n## Quick Start\n\n```typescript\nimport { ArcanaClient } from '@arcanahq/sdk';\nimport { createWalletClient, custom } from 'viem';\n\n// Initialize client\nconst client = new ArcanaClient({\n  apiUrl: 'http://localhost:3003',\n  getToken: () => localStorage.getItem('auth_token'),\n  setToken: (token) => localStorage.setItem('auth_token', token),\n  // Optional: Custom headers for server-side API token auth\n  // customHeaders: { 'x-api-token': 'your-token' },\n});\n\n// Authenticate with wallet\nconst walletClient = createWalletClient({\n  transport: custom(window.ethereum),\n});\n\nconst [account] = await walletClient.getAddresses();\nawait client.auth.signIn(walletClient, account);\n\n// Call contract action\nconst result = await client.contracts.executeAction(\n  'contract-id',\n  'play',\n  { move: 'rock' }\n);\n\n// Wait for transaction\nawait client.transactions.wait(result.transaction_id);\n\n// Get contract view\nconst view = await client.contracts.view('contract-id');\n```\n\n## Authentication\n\nThe SDK supports two authentication methods:\n\n### Device Authentication (Recommended for Web Apps)\n\nDevice auth provides the best user experience with minimal wallet prompts:\n\n```typescript\nimport { ArcanaProvider, useArcana } from '@arcanahq/sdk';\n\n// Wrap your app with the provider\n<ArcanaProvider apiUrl=\"http://localhost:3003\">\n  <App />\n</ArcanaProvider>\n\n// In your components\nfunction GameComponent() {\n  const { isAuthenticated, userAddress, connect, client } = useArcana();\n  \n  if (!isAuthenticated) {\n    return <button onClick={connect}>Connect Wallet</button>;\n  }\n  \n  // Use client for API calls\n  const view = await client.contracts.view('contract-id');\n}\n```\n\n### Legacy Session Authentication\n\nFor simpler use cases or backend integrations:\n\n```typescript\nconst client = new ArcanaClient({\n  apiUrl: 'http://localhost:3003',\n  getToken: () => localStorage.getItem('auth_token'),\n  setToken: (token) => localStorage.setItem('auth_token', token),\n});\n\n// Sign in with wallet\nconst walletClient = createWalletClient({ transport: custom(window.ethereum) });\nconst [account] = await walletClient.getAddresses();\nawait client.auth.signIn(walletClient, account);\n```\n\n### How It Works\n\n1. **Device Registration**: First-time users sign once to register their device\n2. **Automatic Tokens**: SDK manages refresh tokens automatically\n3. **Per-Request Signing**: Mutating requests are signed with the device key\n4. **Wallet Prompts**: Only required for device registration or high-value actions\n\nThe SDK signs requests using EIP-712 typed data and an Ed25519 device key; see\nthe source in [`src/auth/`](./src/auth) for the full flow.\n\n## Architecture\n\nThe SDK is organized into focused modules:\n\n- **Auth**: EIP-712 authentication with viem\n- **Contracts**: Execute actions, view state, get events\n- **History**: Transaction history and events\n- **Tables**: Game table management\n- **Transactions**: Transaction status and waiting\n- **Bank**: Balance management, withdrawals, transfers\n- **Config**: Server configuration\n- **Chain**: On-chain data queries\n\n## API Reference\n\n### ArcanaClient\n\nMain client class that provides access to all modules.\n\n```typescript\nconst client = new ArcanaClient({\n  apiUrl?: string;           // Default: 'http://localhost:3003'\n  getToken?: () => string | null;\n  setToken?: (token: string) => void;\n});\n```\n\n### Auth Module\n\n#### `signIn(walletClient, address, chainId?, verifyingContract?)`\n\nSign in with wallet using EIP-712.\n\n```typescript\nconst walletClient = createWalletClient({\n  transport: custom(window.ethereum),\n});\nconst [account] = await walletClient.getAddresses();\n\nawait client.auth.signIn(walletClient, account);\n```\n\n#### `signOut()`\n\nSign out and clear token.\n\n```typescript\nawait client.auth.signOut();\n```\n\n#### `getUserInfo()`\n\nGet current user information.\n\n```typescript\nconst userInfo = await client.auth.getUserInfo();\n```\n\n#### `isAuthenticated()`\n\nCheck if user is authenticated.\n\n```typescript\nif (client.auth.isAuthenticated()) {\n  // User is signed in\n}\n```\n\n### Contracts Module\n\n#### `executeAction(contractId, entrypoint, args, options?)`\n\nExecute an action/entrypoint on a contract.\n\nLow-level action args are encoded exactly by shape: arrays become positional\nMessagePack arrays and objects become MessagePack maps. AssemblyScript args\nclasses generated with `decodeArgsArray` expect positional arrays, so call\n`executeAction(instanceId, 'submit_guess', ['cider'])` for a single positional\nfield. Prefer the generated client from `arcana generate sdk` when you want to\npass named objects; those helpers convert named fields into the program's tuple\norder.\n\n**Note**: The response does not include `new_state`. If you need the updated state after an action, fetch it separately using `view()`:\n\n```typescript\nconst result = await client.contracts.executeAction(\n  'contract-id',\n  'play',\n  ['rock'],\n  {\n    idempotency_key: 'optional-idempotency-key',\n  }\n);\n\n// Fetch updated state after action\nconst updatedState = await client.contracts.view('contract-id');\n```\n\n#### `view(contractId)`\n\nGet personalized view of contract state (read-only).\n\n**Note**: The lower-level `client.contracts.view()` / `client.programs.view()`\npath returns the decoded MessagePack value exactly as the program emitted it.\nView classes that encode positional MessagePack arrays therefore come back as\narrays unless that entrypoint has a built-in SDK normalizer. The generated\nclient from `arcana generate sdk` includes view-shape normalizers and is the\nrecommended path when you want named object fields. If you call the lower-level\nSDK directly, normalize positional arrays in your app code using the view field\norder from the program source.\n\nThe response structure may also vary by program. The state may be nested (e.g.,\n`result.state` or directly in `result`). Handle both cases:\n\n```typescript\nconst viewResult = await client.contracts.view('contract-id');\n// Handle nested structure - try result.state first, then fallback to result\nconst state = viewResult?.state || viewResult || {};\n```\n\n#### `getState(contractId)`\n\nGet raw contract state (no caller_id filtering).\n\n```typescript\nconst state = await client.contracts.getState('contract-id');\n```\n\n#### `getEvents(contractId, options?)`\n\nGet contract events.\n\n```typescript\nconst events = await client.contracts.getEvents('contract-id', {\n  limit: 50,\n  offset: 0,\n  event_type: 'GameStarted',\n});\n```\n\n#### `getEventsPage(contractId, options?)`\n\nGet cursor-paginated instance events.\n\n```typescript\nconst page = await client.contracts.getEventsPage('contract-id', {\n  page_size: 50,\n  cursor: 'optional-cursor',\n  event_type: 'GameStarted',\n});\n```\n\n#### `events.queryPage(options?)`\n\nQuery event history across scopes, programs, or instances.\n\n```typescript\nconst page = await client.events.queryPage({\n  scope_id: 'my-game:app',\n  program_type: 'coinflip',\n  event_type: 'coinflip.resolved',\n  page_size: 50,\n});\n```\n\n### Subscriptions Module\n\nThe subscriptions module is available from `client.subscriptions` or from the\nsubpath export:\n\n```typescript\nimport { SubscriptionsModule } from '@arcanahq/sdk/subscriptions';\n```\n\n#### `subscribeInstance(scopeId, instanceId, options)`\n\nSubscribe to public spectator-safe state updates for one instance over SSE.\n\n```typescript\nconst sub = client.subscriptions.subscribeInstance('my-scope', 'instance-id', {\n  initialStateVersion: currentStateVersion,\n  onView: (view, event) => {\n    // `view` is the public/spectator view emitted by the server after commit.\n    // Use it to update cache, then keep private/player-only state refreshed\n    // through normal authenticated `view()` calls where needed.\n  },\n  refetch: () => client.contracts.view('instance-id'),\n  onRefetch: (view) => {\n    // Re-hydrate authoritative client cache after reconnects or missed events.\n  },\n});\n\n// Later:\nsub.close();\n```\n\nThe SDK automatically reconnects with backoff, ignores duplicate/stale events\nusing `sequence` and `state_version`, and can run slow fallback polling through\n`refetch` while disconnected.\n\n##### Spectator-view contract\n\nPushed `view_json` payloads are rendered through the program's **public\nsubscription view** with `caller_id = \"spectator\"`. They never contain\nhand-private or otherwise scoped data (hole cards, hidden ships, private\nbalances, etc.).\n\nThe subscription path renders the entrypoint named `view`. REST view handlers\nalso allow custom named view entrypoints, but pushed spectator updates do not\nguess which custom view should be public. Use bare `@view` for the\nspectator-safe default shape, and explicit names such as `@view(\"canvas\")` for\nadditional projections.\n\nFor player-private state, fall back to authenticated REST views:\n\n```typescript\nconst sub = client.subscriptions.subscribeInstance(scopeId, instanceId, {\n  onView: (publicView) => updateSpectatorCache(publicView),\n  refetch: async () => client.contracts.view(instanceId), // authenticated, full view\n  onRefetch: (privateView) => updatePlayerCache(privateView),\n});\n```\n\nA common pattern is: render off `onView` for shared state, then re-fetch the\nplayer's private view via `refetch` after each event (or on a coarser cadence)\nwhen private state actually matters.\n\n##### Reliability features\n\n- **`Last-Event-ID` replay.** Each event carries a per-topic monotonic\n  `sequence`, sent in the SSE `id:` field. The SDK echoes the last id on every\n  reconnect, and the server replays buffered events past that cursor before\n  resuming the live stream. Buffer size defaults to 64 events per topic\n  (override at the server with `ARCANA_SUBSCRIPTION_RING_CAPACITY`).\n\n- **Resync sentinel.** If the server detects a lagged subscriber or a publish\n  queue overflow, it emits an `event_type: \"resync\"` sentinel. The SDK\n  intercepts it, fires `onStatusChange('resync')` and calls `refetch()` so the\n  client re-hydrates from the authoritative view. Sentinels never reach\n  `onEvent`/`onView`.\n\n- **Keep-alive watchdog.** The server pings every 15s. The SDK aborts and\n  reconnects if no chunk (event or ping) arrives within\n  `keepAliveTimeoutMs` (default 45000ms).\n\n- **Online + visibility wakeups.** When the browser fires `window.online` or\n  the page becomes visible, the SDK skips the current backoff wait and\n  reconnects immediately. Disable with `reconnectOnOnline: false` /\n  `reconnectOnVisible: false`.\n\n- **Auth refresh.** On 401/403, the SDK calls the configured `refreshTokens`\n  hook and retries the connect. `ensureAccessToken` is also called at the top\n  of every reconnect attempt so long-running streams pick up rotated tokens.\n\n##### Server feature flags\n\nThe server reads two env vars at startup; both default to enabled.\n\n- **`ARCANA_SUBSCRIPTIONS_ENABLED`** — global kill switch. When `false`,\n  `/subscriptions` is not mounted (returns 404) and the publisher is not\n  wired. Use to disable the feature without redeploying:\n  `fly secrets set ARCANA_SUBSCRIPTIONS_ENABLED=false -a <app>`.\n- **`ARCANA_SUBSCRIPTION_RESYNC_SENTINEL`** — compatibility switch. When\n  `false`, lagged subscribers' streams end silently and publish-queue\n  overflow no longer emits a resync sentinel. Set this if older SDK\n  clients cannot interpret `event_type: \"resync\"` events.\n\nSizing knobs (also env vars):\n`ARCANA_SUBSCRIPTION_RING_CAPACITY` (default 64),\n`ARCANA_SUBSCRIPTION_BROADCAST_CAPACITY` (default 256).\n\n##### Single-node assumption\n\nThe default `InMemoryBackend` is single-process. In a multi-node deployment\neach node has its own ring buffer and broadcast channel, so a publisher on\nnode A is invisible to subscribers on node B. Multi-node fan-out requires a\nRedis/NATS backend (the `SubscriptionBackend` trait is in place but no\ndistributed implementation ships yet).\n\n#### `subscribeScope(scopeId, options)`\n\nSubscribe to public spectator-safe updates for all instances in a scope.\n\n```typescript\nconst sub = client.subscriptions.subscribeScope('my-scope', {\n  onEvent: (event) => {\n    console.log(event.instance_id, event.state_version, event.view_json);\n  },\n});\n```\n\n#### `create(contractType, args?, contractId?)`\n\nCreate a new contract instance.\n\n```typescript\nconst contract = await client.contracts.create(\n  'battleship',\n  { min_players: 2 },\n  'optional-contract-id'\n);\n```\n\n#### `getUserContracts(userId?)`\n\nGet user's contracts.\n\n```typescript\nconst contracts = await client.contracts.getUserContracts();\n```\n\n### History Module\n\n#### `listContracts()`\n\nList contracts the user has history for.\n\n```typescript\nconst contracts = await client.history.listContracts();\n```\n\n#### `getHistory(contractId, options?)`\n\nGet transaction history for a contract.\n\n```typescript\nconst history = await client.history.getHistory('contract-id', {\n  limit: 50,\n  cursor: 'optional-cursor',\n});\n```\n\n#### `getEventHistory(contractId, options?)`\n\nGet event history for a contract (events only).\n\n```typescript\nconst events = await client.history.getEventHistory('contract-id', {\n  limit: 50,\n});\n```\n\n### Tables Module\n\n#### `create(request)`\n\nCreate a new table.\n\n```typescript\nconst table = await client.tables.create({\n  game_type: 'battleship',\n  table_mode: 'tournament',\n  min_players: 2,\n  max_players: 2,\n});\n```\n\n#### `list(options?)`\n\nList tables with optional filters.\n\n```typescript\nconst tables = await client.tables.list({\n  game_type: 'battleship',\n  status: 'waiting',\n  limit: 20,\n});\n```\n\n#### `get(tableId)`\n\nGet table by ID.\n\n```typescript\nconst table = await client.tables.get('table-id');\n```\n\n#### `getByInvite(inviteCode, scopeId?)`\n\nGet table by invite code.\n\n```typescript\nconst table = await client.tables.getByInvite('ABC123');\n```\n\n#### `join(tableId, request?)`\n\nJoin a table.\n\n```typescript\nconst table = await client.tables.join('table-id', {\n  password: 'optional-password',\n  seat_number: 1,\n  buy_in_amount: '1000000000000000000',\n});\n```\n\n### Transactions Module\n\n#### `getStatus(transactionId)`\n\nGet transaction status.\n\n```typescript\nconst status = await client.transactions.getStatus('tx-id');\nif (status) {\n  console.log(status.status); // 'pending' | 'executing' | 'completed' | 'failed'\n}\n```\n\n#### `wait(transactionId, options?)`\n\nWait for transaction to complete.\n\n```typescript\nconst result = await client.transactions.wait('tx-id', {\n  timeout: 30000,      // 30 seconds\n  pollInterval: 100,   // 100ms\n});\n```\n\n## Error Handling\n\nThe SDK provides custom error classes:\n\n- `ArcanaApiError`: API errors (400, 401, 403, 500, etc.)\n- `ArcanaNetworkError`: Network errors\n- `ArcanaContractError`: Contract action errors\n\n```typescript\nimport { ArcanaApiError, ArcanaContractError } from '@arcanahq/sdk';\n\ntry {\n  await client.contracts.executeAction('contract-id', 'play', {});\n} catch (error) {\n  if (error instanceof ArcanaContractError) {\n    console.error('Contract error:', error.message);\n    console.error('Contract ID:', error.contractId);\n    console.error('Entrypoint:', error.entrypoint);\n  } else if (error instanceof ArcanaApiError) {\n    console.error('API error:', error.status, error.message);\n  } else {\n    console.error('Unknown error:', error);\n  }\n}\n```\n\n## TypeScript Support\n\nThe SDK is fully typed with TypeScript. All modules export their types:\n\n```typescript\nimport type {\n  ContractInfo,\n  ContractActionResponse,\n  Table,\n  TransactionResult,\n} from '@arcanahq/sdk';\n```\n\n## Examples\n\n### Complete Game Flow\n\n```typescript\nimport { ArcanaClient } from '@arcanahq/sdk';\nimport { createWalletClient, custom } from 'viem';\n\nconst client = new ArcanaClient({\n  apiUrl: 'http://localhost:3003',\n  getToken: () => localStorage.getItem('auth_token'),\n  setToken: (token) => localStorage.setItem('auth_token', token),\n});\n\n// 1. Authenticate\nconst walletClient = createWalletClient({\n  transport: custom(window.ethereum),\n});\nconst [account] = await walletClient.getAddresses();\nawait client.auth.signIn(walletClient, account);\n\n// 2. Create or join a table\nconst table = await client.tables.create({\n  game_type: 'battleship',\n  table_mode: 'tournament',\n  min_players: 2,\n  max_players: 2,\n});\n\n// 3. Join the table\nawait client.tables.join(table.id);\n\n// 4. Execute game actions\nconst result = await client.contracts.executeAction(\n  table.contract_id!,\n  'place_ship',\n  { row: 0, column: 0, shipType: 'carrier', horizontal: true }\n);\n\n// 5. Wait for transaction\nawait client.transactions.wait(result.transaction_id!);\n\n// 6. Get updated state\nconst view = await client.contracts.view(table.contract_id!);\n```\n\n### Polling for State Changes\n\n```typescript\nasync function waitForGameStart(contractId: string) {\n  while (true) {\n    const view = await client.contracts.view(contractId);\n    if (view.status === 'playing') {\n      return view;\n    }\n    await new Promise(resolve => setTimeout(resolve, 1000));\n  }\n}\n```\n\n### Error Handling\n\n```typescript\ntry {\n  const result = await client.contracts.executeAction(\n    'contract-id',\n    'play',\n    { move: 'rock' }\n  );\n  \n  if (result.error) {\n    console.error('Action failed:', result.error);\n    return;\n  }\n  \n  console.log('Action succeeded:', result.new_state);\n} catch (error) {\n  if (error instanceof ArcanaContractError) {\n    console.error('Contract error:', error.message);\n  } else {\n    console.error('Unexpected error:', error);\n  }\n}\n```\n\n## Differences from server/sdk\n\n- **General-purpose**: Not game-specific, works with any Arcana contract\n- **Frontend-focused**: Designed for client-side usage\n- **Type-safe**: Full TypeScript support\n- **Modular**: Clear separation of concerns\n- **Helper utilities**: Transaction waiting, polling, batching\n\n## License\n\nMIT\n","readmeFilename":"README.md","_rev":"1-4b060b0169b7ecf5a68e08ba5bc6da6a"}