Dart SDK
Full API reference for truthid_sdk — verifier and cross-device requester roles.
Full API reference for truthid_sdk. New to TruthID? Start with the Quickstart — this page is the detailed reference for every method and type once you're integrating for real.
Unlike the TypeScript/Python/Ruby SDKs — which are all verifier-only (the role a website's backend plays) — this package has two independent roles in one import:
TruthIDClient— the verifier, same role as the other 3 SDKs. For a backend written in Dart.TruthIDRequester— a Dart-only role: request a signature, a transaction execution, or a file pin from the user's TruthID mobile app, over the same LAN/dead-drop transport the mobile app itself uses. For any Flutter (mobile or desktop) app, or a plain Dart backend, that wants to be the requesting side of a cross-device flow — no TruthID-operated server involved, either way.
Pure Dart — no package:flutter dependency. Works in Flutter apps and plain Dart backends alike.
Installation
truthid_sdk isn't published to pub.dev yet — the other three SDKs are live on their respective registries (see Introduction → Integrating TruthID), but Dart is still install-from-source only. Point at the package directly, either from a local clone or from GitHub with a path to the sdk/dart subdirectory:
dependencies:
truthid_sdk:
git:
url: https://github.com/masterlxz/truthid.git
path: sdk/dartOr, if you already have the repo checked out locally:
dependencies:
truthid_sdk:
path: ../path/to/truthid/sdk/dartTruthIDClient (verifier)
import 'package:truthid_sdk/truthid_sdk.dart';
final truthid = TruthIDClient(network: Network.baseMainnet);Constructor
TruthIDClient({required Network network, String? rpcUrl})
| Field | Type | Required | Description |
|---|---|---|---|
network | Network.baseSepolia or Network.baseMainnet | Yes — no default | Which network to read contracts from |
rpcUrl | String? | No | Custom RPC endpoint. Defaults to the public Base RPC for the chosen network |
createChallenge(origin)
Creates a one-time challenge to embed in the QR code shown to the user.
Returns AuthChallenge
final challenge = truthid.createChallenge('yoursite.com');Building the QR code — the mobile app expects this exact shape:
{
"action": "truthid-auth",
"challenge": { "type": "challenge", "nonce": "...", "issuedAt": 1718000000000, "origin": "yoursite.com" },
"callbackUrl": "https://yoursite.com/auth/verify"
}callbackUrl must use https:// — the mobile app refuses to send the signed response to a plain http:// URL.
verifyAuthResponse(challenge, response, {ttlMs})
Verifies the signed response received from the user's phone. Runs six checks in sequence and stops at the first failure — same order as the other 3 SDKs:
- User approved (not rejected)
- Challenge is within TTL (default: 30 seconds)
- Nonce matches the original challenge
- Cryptographic signature is valid
- Device is registered and active on the blockchain
- Retrieves the identity ID linked to this device
Returns VerifyAuthResult
final result = await truthid.verifyAuthResponse(challenge, response);
if (result.valid) {
print('Authenticated! Identity ID: ${result.identityId}');
} else {
print('Failed: ${result.reason}');
}Failure reasons
reason | Cause |
|---|---|
"User rejected the login request" | User tapped "Reject" on their phone |
"Challenge expired" | More than ttlMs ms have passed since issuedAt |
"Nonce mismatch" | Response nonce doesn't match the challenge |
"Invalid signature format" | Signature is malformed |
"Signature does not match device address" | Signature was not made by deviceAddress |
"Device is not active or has been revoked" | Device was revoked by the identity owner |
verifySession(hash)
Checks whether a session hash is still valid (not revoked).
Returns SessionInfo
final session = await truthid.verifySession(sessionHash);
if (session.exists && !session.revoked) {
// still logged in
}checkDeviceStatus(devicePubKey)
Looks up a device's current status on the blockchain. Returns DeviceStatus.
computeSmartAccountAddress(ledgerAddress, {index})
Predicts a user's smart account (controller) address via CREATE2 — pure local computation, no RPC call and no server involved.
final smartAccount = truthid.computeSmartAccountAddress(ledgerAddress);A standalone function is also exported for when you don't need a full client instance:
import 'package:truthid_sdk/truthid_sdk.dart' show computeSmartAccountAddress, Network;
final smartAccount = computeSmartAccountAddress(ledgerAddress, Network.baseMainnet);TruthIDRequester (cross-device requester)
final requester = TruthIDRequester();Request a signature, a transaction execution, or a file pin from the user's phone — you show a QR code, the user scans it with the TruthID mobile app, approves or rejects, and the answer comes back over the same transport the mobile app itself uses for every other cross-device flow (a local-network sweep first, an IPFS/IPNS dead-drop as the fallback). No relayer, no TruthID server, no polling endpoint for you to host.
Each method returns a PendingRequest immediately — the QR payload is ready to render right away; result is a Future that resolves once the phone answers or the request expires.
Cross-device only, 4 flows
Deep-link transport (same device, no QR) is out of scope — it would require your app to register its own URI scheme, which is platform-specific and outside what a pure-Dart package can automate. See What's not covered yet.
signMessage({appName, purpose, timeout})
Asks the phone to sign an arbitrary short message — the actual string signed is always 'TruthID Message Signing: $appName:$purpose', reconstructed by the mobile app itself; you never choose the raw bytes.
| Name | Type | Required | Description |
|---|---|---|---|
appName | String | Yes | Shown to the user on the approval screen |
purpose | String | Yes | 1-64 chars, [A-Za-z0-9_.-]+ — shown to the user, part of the signed message |
timeout | Duration | No — default 3 minutes | How long the QR stays valid |
Returns PendingRequest<SignMessageResult>
final pending = requester.signMessage(appName: 'My App', purpose: 'login');
showQrCode(pending.qrPayload); // your own QR widget, e.g. qr_flutter
final result = await pending.result;
if (result.delivered && result.data!.status == 'signed') {
print('Signature: ${result.data!.signature}');
}signRequest({appName, dest, callData, functionSignature, value, timeout})
Asks the phone to execute an arbitrary contract call from the user's smart account — the phone builds, signs, and submits the UserOperation itself (the smart account pays its own gas). Use this for anything beyond a plain signature: token transfers, contract interactions, anything execute() can reach.
| Name | Type | Required | Description |
|---|---|---|---|
appName | String | Yes | Shown to the user |
dest | String | Yes | Destination contract address (0x...) |
callData | String | Yes | ABI-encoded call data (0x...) |
functionSignature | String | Yes | e.g. "transfer(address,uint256)" — shown to the user for verification |
value | String | No — default "0" | Wei to send, as a decimal string |
timeout | Duration | No — default 3 minutes |
Returns PendingRequest<SignRequestResult>
final pending = requester.signRequest(
appName: 'My App',
dest: tokenAddress,
callData: encodedTransferCall,
functionSignature: 'transfer(address,uint256)',
);
final result = await pending.result;
if (result.delivered && result.data!.status == 'executed') {
print('Transaction hash: ${result.data!.transactionHash}');
}This can take longer than the other two
The phone doesn't just sign here — it submits the UserOperation through a bundler and waits for the receipt before answering. Budget up to a minute of extra wait after the user approves.
pin({appName, content, timeout})
Asks the phone to publish arbitrary bytes to Arweave, paid for by the identity's own local Arweave wallet — there's no concept of "configured providers" here, it's always the requesting identity's wallet. Two phases under the hood, both handled for you: the encrypted content is pushed to the phone over the LAN first (there's no dead-drop for this phase — the phone only starts listening once it scans the QR, so this keeps retrying the push in the background until it lands or the request expires); once the phone has it, the usual result race (LAN + dead-drop) delivers the pin result.
| Name | Type | Required | Description |
|---|---|---|---|
appName | String | Yes | Shown to the user |
content | Uint8List | Yes | The bytes to pin |
timeout | Duration | No — default 3 minutes |
Returns PendingRequest<PinResult>
final pending = requester.pin(appName: 'My App', content: fileBytes);
final result = await pending.result;
if (result.delivered && result.data!.status == 'pinned') {
print('Content pointer: ${result.data!.cid}');
}vaultEdit({appName, site, url, username, password, notes, passkey, pinningEndpointUrl, timeout})
Proposes a new Vault credential (a password, a passkey, or both) for the user's Device to review and, if approved, persist and publish on-chain — the requester-side counterpart of what the TruthID browser extension does when it sees a signup form or a new navigator.credentials.create(). Use this for password-manager-style integrations: your app captured or generated a credential and wants the user's Vault to remember it.
Structurally different from the other three flows: there is no response phase at all — approval happens entirely on the Device, out of band. VaultEditPendingRequest.delivered only reports whether the encrypted proposal made it to a Device before timeout — it says nothing about whether the Device went on to approve or reject it.
The encrypted proposal is pushed over the LAN (retried until some Device accepts it or timeout passes, the same way pin's content push works) and, if pinningEndpointUrl is given, published in parallel to a Kubo node for cross-network delivery — best-effort, a missing or unreachable endpoint never fails the LAN push.
| Name | Type | Required | Description |
|---|---|---|---|
appName | String | Yes | Shown to the user on the approval screen |
site | String | Yes | Hostname/site the credential belongs to |
url | String | No — default '' | Full origin URL |
username | String | Yes | |
password | String | No — default '' | At least one of password/passkey is required |
notes | String | No — default '' | |
passkey | VaultEditPasskey? | No | At least one of password/passkey is required |
pinningEndpointUrl | String? | No | A Kubo node's base URL (e.g. http://127.0.0.1:5001) — enables the cross-network dead-drop leg. Without it, delivery is LAN-only |
timeout | Duration | No — default 3 minutes |
Returns VaultEditPendingRequest
final pending = requester.vaultEdit(
appName: 'My App',
site: 'example.com',
url: 'https://example.com',
username: 'alice',
password: generatedPassword,
);
showQrCode(pending.qrPayload);
final delivered = await pending.delivered;
if (delivered) {
print('Proposal handed off — waiting on the user to approve on their Device');
}What's not covered yet
- Deep-link (same-device) transport — requires the host app to register its own URI scheme, platform-specific and outside what a pure-Dart package can automate.
Types
Verifier types
All exported from package:truthid_sdk/truthid_sdk.dart.
Network
enum Network { baseSepolia, baseMainnet }AuthChallenge
class AuthChallenge {
final String type; // always "challenge"
final String nonce;
final int issuedAt; // Unix timestamp in ms
final String origin;
}AuthResponse
class AuthResponse {
final bool approved;
final String nonce;
final String signature; // secp256k1 signature, hex ("0x...")
final String deviceAddress; // Ethereum address of the device key
final String? sessionSignature; // personal_sign over keccak256(nonce) — always sent alongside signature
}VerifyAuthResult
class VerifyAuthResult {
final bool valid;
final BigInt? identityId;
final String? deviceAddress;
final String? reason;
}SessionInfo
class SessionInfo {
final bool exists;
final bool revoked;
final BigInt? identityId;
final String? devicePubKey;
final DateTime? createdAt;
}DeviceStatus
class DeviceStatus {
final bool exists;
final bool active;
final String? label;
final BigInt? identityId;
final DateTime? addedAt;
}Requester types
PendingRequest<T>
class PendingRequest<T> {
final String qrPayload; // JSON, ready to render as a QR code
final String sessionId;
final DateTime expiresAt;
final Future<TransportResult<T>> result;
}TransportResult<T>
class TransportResult<T> {
final bool delivered; // false = expired with no answer
final T? data; // present only when delivered is true
}SignMessageResult
class SignMessageResult {
final String status; // 'signed' | 'rejected'
final String? message;
final String? signature;
}SignRequestResult
class SignRequestResult {
final String status; // 'executed' | 'failed' | 'rejected'
final String? userOpHash;
final String? transactionHash;
final String? error;
}PinResult
class PinResult {
final String status; // 'pinned' | 'failed' | 'rejected'
final String? cid; // "ar://<tx_id>" — an Arweave pointer, not an IPFS CID
final String? contentHash;
final List<String>? providersOk;
final List<String>? providersFailed;
final String? error;
}VaultEditPasskey
class VaultEditPasskey {
final String rpId;
final String credentialIdB64;
final String userHandleB64;
final String privateKeyHex;
final int signCount;
final int createdAt;
}VaultEditPendingRequest
class VaultEditPendingRequest {
final String qrPayload; // JSON, ready to render as a QR code
final String sessionId;
final DateTime expiresAt;
final Future<bool> delivered; // no response phase — see vaultEdit() above
}Security notes
Nonce invalidation
Delete the challenge from your store before calling verifyAuthResponse, not after — deleting after leaves a race condition where the same signed response can be submitted twice within the TTL window.
TTL
The default is 30 seconds, matching the mobile app's own challenge expiry.
HTTPS only
The phone POSTs the signed response directly to your callbackUrl — the mobile app refuses non-https:// URLs, and your endpoint still needs a valid TLS certificate for the connection to succeed.
Networks
| Network | Chain ID | Description |
|---|---|---|
Network.baseSepolia | 84532 | Testnet — for development |
Network.baseMainnet | 8453 | Production |
Contract addresses for both networks are in Smart contracts.
Session visibility — nothing to do
Completed logins (via TruthIDClient.verifyAuthResponse, or via TruthIDRequester.signRequest) already show up in the user's TruthID mobile/desktop apps and can be individually revoked from there — the phone registers sessions on-chain itself. If you want to double-check a session landed on-chain, derive its hash from the nonce and call verifySession:
import 'package:web3dart/crypto.dart' show keccak256;
import 'dart:convert';
final sessionHash = '0x' + keccak256(Uint8List.fromList(utf8.encode(nonce)))
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join();
final session = await truthid.verifySession(sessionHash);Next steps
- Quickstart — full walkthrough from install to first login
- TypeScript SDK reference
- Python SDK reference
- Ruby SDK reference