tb_rs/protocol/types.rs
1//! TigerBeetle protocol data types.
2//!
3//! These types match the exact byte layout of the TigerBeetle wire protocol.
4//! All types use `#[repr(C)]` to ensure C-compatible memory layout.
5
6use bitflags::bitflags;
7
8/// TigerBeetle Account (128 bytes).
9///
10/// Accounts are the fundamental unit of accounting in TigerBeetle.
11/// They track debits and credits with pending and posted balances.
12#[repr(C)]
13#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
14pub struct Account {
15 /// Unique identifier for the account.
16 pub id: u128,
17 /// Sum of pending debit transfers.
18 pub debits_pending: u128,
19 /// Sum of posted debit transfers.
20 pub debits_posted: u128,
21 /// Sum of pending credit transfers.
22 pub credits_pending: u128,
23 /// Sum of posted credit transfers.
24 pub credits_posted: u128,
25 /// Opaque user data for external linking (128-bit indexed).
26 pub user_data_128: u128,
27 /// Opaque user data for external linking (64-bit indexed).
28 pub user_data_64: u64,
29 /// Opaque user data for external linking (32-bit indexed).
30 pub user_data_32: u32,
31 /// Reserved for accounting policy primitives.
32 pub reserved: u32,
33 /// The ledger this account belongs to.
34 pub ledger: u32,
35 /// Chart of accounts code describing the account type.
36 pub code: u16,
37 /// Account flags.
38 pub flags: AccountFlags,
39 /// Timestamp when the account was created (set by server).
40 pub timestamp: u64,
41}
42
43const _: () = assert!(std::mem::size_of::<Account>() == 128);
44
45bitflags! {
46 /// Flags for Account configuration.
47 #[repr(transparent)]
48 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash)]
49 pub struct AccountFlags: u16 {
50 /// Link this account with the next in a chain.
51 const LINKED = 1 << 0;
52 /// Enforce that debits do not exceed credits.
53 const DEBITS_MUST_NOT_EXCEED_CREDITS = 1 << 1;
54 /// Enforce that credits do not exceed debits.
55 const CREDITS_MUST_NOT_EXCEED_DEBITS = 1 << 2;
56 /// Enable balance history for this account.
57 const HISTORY = 1 << 3;
58 /// Mark this account as imported (for data migration).
59 const IMPORTED = 1 << 4;
60 /// Mark this account as closed.
61 const CLOSED = 1 << 5;
62 }
63}
64
65/// TigerBeetle Transfer (128 bytes).
66///
67/// Transfers move value between accounts by debiting one and crediting another.
68#[repr(C)]
69#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
70pub struct Transfer {
71 /// Unique identifier for the transfer.
72 pub id: u128,
73 /// Account ID to debit.
74 pub debit_account_id: u128,
75 /// Account ID to credit.
76 pub credit_account_id: u128,
77 /// Amount to transfer.
78 pub amount: u128,
79 /// ID of pending transfer to post or void (0 if not applicable).
80 pub pending_id: u128,
81 /// Opaque user data for external linking (128-bit indexed).
82 pub user_data_128: u128,
83 /// Opaque user data for external linking (64-bit indexed).
84 pub user_data_64: u64,
85 /// Opaque user data for external linking (32-bit indexed).
86 pub user_data_32: u32,
87 /// Timeout in seconds for pending transfers.
88 pub timeout: u32,
89 /// The ledger this transfer operates on.
90 pub ledger: u32,
91 /// Chart of accounts code describing the transfer type.
92 pub code: u16,
93 /// Transfer flags.
94 pub flags: TransferFlags,
95 /// Timestamp when the transfer was created (set by server).
96 pub timestamp: u64,
97}
98
99const _: () = assert!(std::mem::size_of::<Transfer>() == 128);
100
101bitflags! {
102 /// Flags for Transfer configuration.
103 #[repr(transparent)]
104 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash)]
105 pub struct TransferFlags: u16 {
106 /// Link this transfer with the next in a chain.
107 const LINKED = 1 << 0;
108 /// Create a pending (two-phase) transfer.
109 const PENDING = 1 << 1;
110 /// Post a pending transfer.
111 const POST_PENDING_TRANSFER = 1 << 2;
112 /// Void a pending transfer.
113 const VOID_PENDING_TRANSFER = 1 << 3;
114 /// Balance the debit side.
115 const BALANCING_DEBIT = 1 << 4;
116 /// Balance the credit side.
117 const BALANCING_CREDIT = 1 << 5;
118 /// Close the debit account after this transfer.
119 const CLOSING_DEBIT = 1 << 6;
120 /// Close the credit account after this transfer.
121 const CLOSING_CREDIT = 1 << 7;
122 /// Mark this transfer as imported (for data migration).
123 const IMPORTED = 1 << 8;
124 }
125}
126
127/// Account balance at a point in time (128 bytes).
128///
129/// Used for historical balance queries.
130#[repr(C)]
131#[derive(Clone, Copy, Debug, Eq, PartialEq)]
132pub struct AccountBalance {
133 /// Pending debits at this timestamp.
134 pub debits_pending: u128,
135 /// Posted debits at this timestamp.
136 pub debits_posted: u128,
137 /// Pending credits at this timestamp.
138 pub credits_pending: u128,
139 /// Posted credits at this timestamp.
140 pub credits_posted: u128,
141 /// Timestamp of this balance snapshot.
142 pub timestamp: u64,
143 /// Reserved for future use.
144 pub reserved: [u8; 56],
145}
146
147impl Default for AccountBalance {
148 fn default() -> Self {
149 Self {
150 debits_pending: 0,
151 debits_posted: 0,
152 credits_pending: 0,
153 credits_posted: 0,
154 timestamp: 0,
155 reserved: [0; 56],
156 }
157 }
158}
159
160const _: () = assert!(std::mem::size_of::<AccountBalance>() == 128);
161
162/// Filter for account-related queries (128 bytes).
163#[repr(C)]
164#[derive(Clone, Copy, Debug)]
165pub struct AccountFilter {
166 /// Account ID to query.
167 pub account_id: u128,
168 /// Filter by user_data_128 (0 for no filter).
169 pub user_data_128: u128,
170 /// Filter by user_data_64 (0 for no filter).
171 pub user_data_64: u64,
172 /// Filter by user_data_32 (0 for no filter).
173 pub user_data_32: u32,
174 /// Filter by code (0 for no filter).
175 pub code: u16,
176 /// Reserved for future use.
177 pub reserved: [u8; 58],
178 /// Minimum timestamp (inclusive, 0 for no filter).
179 pub timestamp_min: u64,
180 /// Maximum timestamp (inclusive, 0 for no filter).
181 pub timestamp_max: u64,
182 /// Maximum number of results.
183 pub limit: u32,
184 /// Query flags.
185 pub flags: AccountFilterFlags,
186}
187
188impl Default for AccountFilter {
189 fn default() -> Self {
190 Self {
191 account_id: 0,
192 user_data_128: 0,
193 user_data_64: 0,
194 user_data_32: 0,
195 code: 0,
196 reserved: [0; 58],
197 timestamp_min: 0,
198 timestamp_max: 0,
199 limit: 0,
200 flags: AccountFilterFlags::empty(),
201 }
202 }
203}
204
205const _: () = assert!(std::mem::size_of::<AccountFilter>() == 128);
206
207bitflags! {
208 /// Flags for AccountFilter queries.
209 #[repr(transparent)]
210 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash)]
211 pub struct AccountFilterFlags: u32 {
212 /// Include debit transfers.
213 const DEBITS = 1 << 0;
214 /// Include credit transfers.
215 const CREDITS = 1 << 1;
216 /// Return results in reverse order.
217 const REVERSED = 1 << 2;
218 }
219}
220
221/// Filter for general queries (64 bytes).
222#[repr(C)]
223#[derive(Clone, Copy, Debug, Default)]
224pub struct QueryFilter {
225 /// Filter by user_data_128 (0 for no filter).
226 pub user_data_128: u128,
227 /// Filter by user_data_64 (0 for no filter).
228 pub user_data_64: u64,
229 /// Filter by user_data_32 (0 for no filter).
230 pub user_data_32: u32,
231 /// Filter by ledger (0 for no filter).
232 pub ledger: u32,
233 /// Filter by code (0 for no filter).
234 pub code: u16,
235 /// Reserved for future use.
236 pub reserved: [u8; 6],
237 /// Minimum timestamp (inclusive, 0 for no filter).
238 pub timestamp_min: u64,
239 /// Maximum timestamp (inclusive, 0 for no filter).
240 pub timestamp_max: u64,
241 /// Maximum number of results.
242 pub limit: u32,
243 /// Query flags.
244 pub flags: QueryFilterFlags,
245}
246
247const _: () = assert!(std::mem::size_of::<QueryFilter>() == 64);
248
249bitflags! {
250 /// Flags for QueryFilter queries.
251 #[repr(transparent)]
252 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash)]
253 pub struct QueryFilterFlags: u32 {
254 /// Return results in reverse order.
255 const REVERSED = 1 << 0;
256 }
257}
258
259/// Result of a create_accounts operation (8 bytes).
260#[repr(C)]
261#[derive(Clone, Copy, Debug)]
262pub struct CreateAccountsResult {
263 /// Index of the account in the request batch.
264 pub index: u32,
265 /// Result code for this account.
266 pub result: CreateAccountResult,
267}
268
269const _: () = assert!(std::mem::size_of::<CreateAccountsResult>() == 8);
270
271/// Result of a create_transfers operation (8 bytes).
272#[repr(C)]
273#[derive(Clone, Copy, Debug)]
274pub struct CreateTransfersResult {
275 /// Index of the transfer in the request batch.
276 pub index: u32,
277 /// Result code for this transfer.
278 pub result: CreateTransferResult,
279}
280
281const _: () = assert!(std::mem::size_of::<CreateTransfersResult>() == 8);
282
283/// Register request body (256 bytes).
284#[repr(C)]
285#[derive(Clone, Copy, Debug)]
286pub struct RegisterRequest {
287 /// Batch size limit (0 for clients, >0 for prepares).
288 pub batch_size_limit: u32,
289 /// Reserved for future use.
290 pub reserved: [u8; 252],
291}
292
293impl Default for RegisterRequest {
294 fn default() -> Self {
295 Self {
296 batch_size_limit: 0,
297 reserved: [0; 252],
298 }
299 }
300}
301
302const _: () = assert!(std::mem::size_of::<RegisterRequest>() == 256);
303
304/// Register result body (64 bytes).
305#[repr(C)]
306#[derive(Clone, Copy, Debug)]
307pub struct RegisterResult {
308 /// Maximum body size for requests.
309 pub batch_size_limit: u32,
310 /// Reserved for future use.
311 pub reserved: [u8; 60],
312}
313
314impl Default for RegisterResult {
315 fn default() -> Self {
316 Self {
317 batch_size_limit: 0,
318 reserved: [0; 60],
319 }
320 }
321}
322
323const _: () = assert!(std::mem::size_of::<RegisterResult>() == 64);
324
325/// Create account result codes.
326///
327/// These match the exact values from the TigerBeetle protocol.
328#[repr(u32)]
329#[derive(Clone, Copy, Debug, Eq, PartialEq)]
330pub enum CreateAccountResult {
331 /// Account created successfully.
332 Ok = 0,
333 /// A linked event in the batch failed, so this event was not applied.
334 LinkedEventFailed = 1,
335 /// A linked event chain was not closed properly.
336 LinkedEventChainOpen = 2,
337 /// The timestamp field must be zero (server assigns timestamps).
338 TimestampMustBeZero = 3,
339 /// A reserved field was set to a non-zero value.
340 ReservedField = 4,
341 /// A reserved flag was set.
342 ReservedFlag = 5,
343 /// Account ID must not be zero.
344 IdMustNotBeZero = 6,
345 /// Account ID must not be `u128::MAX`.
346 IdMustNotBeIntMax = 7,
347 /// Mutually exclusive flags were set together.
348 FlagsAreMutuallyExclusive = 8,
349 /// `debits_pending` must be zero on creation.
350 DebitsPendingMustBeZero = 9,
351 /// `debits_posted` must be zero on creation.
352 DebitsPostedMustBeZero = 10,
353 /// `credits_pending` must be zero on creation.
354 CreditsPendingMustBeZero = 11,
355 /// `credits_posted` must be zero on creation.
356 CreditsPostedMustBeZero = 12,
357 /// Ledger must not be zero.
358 LedgerMustNotBeZero = 13,
359 /// Code must not be zero.
360 CodeMustNotBeZero = 14,
361 /// Account exists with different flags.
362 ExistsWithDifferentFlags = 15,
363 /// Account exists with different `user_data_128`.
364 ExistsWithDifferentUserData128 = 16,
365 /// Account exists with different `user_data_64`.
366 ExistsWithDifferentUserData64 = 17,
367 /// Account exists with different `user_data_32`.
368 ExistsWithDifferentUserData32 = 18,
369 /// Account exists with different ledger.
370 ExistsWithDifferentLedger = 19,
371 /// Account exists with different code.
372 ExistsWithDifferentCode = 20,
373 /// Account already exists (idempotent success).
374 Exists = 21,
375 /// Expected an imported event but `IMPORTED` flag not set.
376 ImportedEventExpected = 22,
377 /// `IMPORTED` flag set but not in import mode.
378 ImportedEventNotExpected = 23,
379 /// Imported event timestamp is out of valid range.
380 ImportedEventTimestampOutOfRange = 24,
381 /// Imported event timestamp must not advance beyond current.
382 ImportedEventTimestampMustNotAdvance = 25,
383 /// Imported event timestamp must not regress.
384 ImportedEventTimestampMustNotRegress = 26,
385}
386
387/// Create transfer result codes.
388///
389/// These match the exact values from the TigerBeetle protocol.
390#[repr(u32)]
391#[derive(Clone, Copy, Debug, Eq, PartialEq)]
392pub enum CreateTransferResult {
393 /// Transfer created successfully.
394 Ok = 0,
395 /// A linked event in the batch failed, so this event was not applied.
396 LinkedEventFailed = 1,
397 /// A linked event chain was not closed properly.
398 LinkedEventChainOpen = 2,
399 /// The timestamp field must be zero (server assigns timestamps).
400 TimestampMustBeZero = 3,
401 /// A reserved flag was set.
402 ReservedFlag = 4,
403 /// Transfer ID must not be zero.
404 IdMustNotBeZero = 5,
405 /// Transfer ID must not be `u128::MAX`.
406 IdMustNotBeIntMax = 6,
407 /// Mutually exclusive flags were set together.
408 FlagsAreMutuallyExclusive = 7,
409 /// Debit account ID must not be zero.
410 DebitAccountIdMustNotBeZero = 8,
411 /// Debit account ID must not be `u128::MAX`.
412 DebitAccountIdMustNotBeIntMax = 9,
413 /// Credit account ID must not be zero.
414 CreditAccountIdMustNotBeZero = 10,
415 /// Credit account ID must not be `u128::MAX`.
416 CreditAccountIdMustNotBeIntMax = 11,
417 /// Debit and credit accounts must be different.
418 AccountsMustBeDifferent = 12,
419 /// `pending_id` must be zero for non-posting/voiding transfers.
420 PendingIdMustBeZero = 13,
421 /// `pending_id` must not be zero for posting/voiding transfers.
422 PendingIdMustNotBeZero = 14,
423 /// `pending_id` must not be `u128::MAX`.
424 PendingIdMustNotBeIntMax = 15,
425 /// `pending_id` must be different from the transfer ID.
426 PendingIdMustBeDifferent = 16,
427 /// Timeout is only valid for pending transfers.
428 TimeoutReservedForPendingTransfer = 17,
429 // 18 is deprecated (amount_must_not_be_zero)
430 /// Ledger must not be zero.
431 LedgerMustNotBeZero = 19,
432 /// Code must not be zero.
433 CodeMustNotBeZero = 20,
434 /// Debit account not found.
435 DebitAccountNotFound = 21,
436 /// Credit account not found.
437 CreditAccountNotFound = 22,
438 /// Debit and credit accounts must have the same ledger.
439 AccountsMustHaveTheSameLedger = 23,
440 /// Transfer ledger must match the accounts' ledger.
441 TransferMustHaveTheSameLedgerAsAccounts = 24,
442 /// Referenced pending transfer not found.
443 PendingTransferNotFound = 25,
444 /// Referenced transfer is not pending.
445 PendingTransferNotPending = 26,
446 /// Pending transfer has different debit account.
447 PendingTransferHasDifferentDebitAccountId = 27,
448 /// Pending transfer has different credit account.
449 PendingTransferHasDifferentCreditAccountId = 28,
450 /// Pending transfer has different ledger.
451 PendingTransferHasDifferentLedger = 29,
452 /// Pending transfer has different code.
453 PendingTransferHasDifferentCode = 30,
454 /// Post amount exceeds pending transfer amount.
455 ExceedsPendingTransferAmount = 31,
456 /// Pending transfer has different amount (for void).
457 PendingTransferHasDifferentAmount = 32,
458 /// Pending transfer was already posted.
459 PendingTransferAlreadyPosted = 33,
460 /// Pending transfer was already voided.
461 PendingTransferAlreadyVoided = 34,
462 /// Pending transfer has expired.
463 PendingTransferExpired = 35,
464 /// Transfer exists with different flags.
465 ExistsWithDifferentFlags = 36,
466 /// Transfer exists with different debit account.
467 ExistsWithDifferentDebitAccountId = 37,
468 /// Transfer exists with different credit account.
469 ExistsWithDifferentCreditAccountId = 38,
470 /// Transfer exists with different amount.
471 ExistsWithDifferentAmount = 39,
472 /// Transfer exists with different `pending_id`.
473 ExistsWithDifferentPendingId = 40,
474 /// Transfer exists with different `user_data_128`.
475 ExistsWithDifferentUserData128 = 41,
476 /// Transfer exists with different `user_data_64`.
477 ExistsWithDifferentUserData64 = 42,
478 /// Transfer exists with different `user_data_32`.
479 ExistsWithDifferentUserData32 = 43,
480 /// Transfer exists with different timeout.
481 ExistsWithDifferentTimeout = 44,
482 /// Transfer exists with different code.
483 ExistsWithDifferentCode = 45,
484 /// Transfer already exists (idempotent success).
485 Exists = 46,
486 /// Transfer would overflow debit account's `debits_pending`.
487 OverflowsDebitsPending = 47,
488 /// Transfer would overflow credit account's `credits_pending`.
489 OverflowsCreditsPending = 48,
490 /// Transfer would overflow debit account's `debits_posted`.
491 OverflowsDebitsPosted = 49,
492 /// Transfer would overflow credit account's `credits_posted`.
493 OverflowsCreditsPosted = 50,
494 /// Transfer would overflow debit account's total debits.
495 OverflowsDebits = 51,
496 /// Transfer would overflow credit account's total credits.
497 OverflowsCredits = 52,
498 /// Transfer timeout would overflow.
499 OverflowsTimeout = 53,
500 /// Transfer exceeds credit account's available credits.
501 ExceedsCredits = 54,
502 /// Transfer exceeds debit account's available debits.
503 ExceedsDebits = 55,
504 /// Expected an imported event but `IMPORTED` flag not set.
505 ImportedEventExpected = 56,
506 /// `IMPORTED` flag set but not in import mode.
507 ImportedEventNotExpected = 57,
508 /// Imported event timestamp is out of valid range.
509 ImportedEventTimestampOutOfRange = 58,
510 /// Imported event timestamp must not advance beyond current.
511 ImportedEventTimestampMustNotAdvance = 59,
512 /// Imported event timestamp must not regress.
513 ImportedEventTimestampMustNotRegress = 60,
514 /// Imported event timestamp must postdate the debit account.
515 ImportedEventTimestampMustPostdateDebitAccount = 61,
516 /// Imported event timestamp must postdate the credit account.
517 ImportedEventTimestampMustPostdateCreditAccount = 62,
518 /// Imported event timeout must be zero.
519 ImportedEventTimeoutMustBeZero = 63,
520 /// Closing transfer must reference a pending transfer.
521 ClosingTransferMustBePending = 64,
522 /// Debit account is already closed.
523 DebitAccountAlreadyClosed = 65,
524 /// Credit account is already closed.
525 CreditAccountAlreadyClosed = 66,
526 /// Transfer exists with different ledger.
527 ExistsWithDifferentLedger = 67,
528 /// This ID was previously used in a failed transfer.
529 IdAlreadyFailed = 68,
530}
531
532#[cfg(test)]
533mod tests {
534 use super::*;
535
536 #[test]
537 fn test_account_size() {
538 assert_eq!(std::mem::size_of::<Account>(), 128);
539 assert_eq!(std::mem::align_of::<Account>(), 16);
540 }
541
542 #[test]
543 fn test_transfer_size() {
544 assert_eq!(std::mem::size_of::<Transfer>(), 128);
545 assert_eq!(std::mem::align_of::<Transfer>(), 16);
546 }
547
548 #[test]
549 fn test_account_balance_size() {
550 assert_eq!(std::mem::size_of::<AccountBalance>(), 128);
551 }
552
553 #[test]
554 fn test_account_filter_size() {
555 assert_eq!(std::mem::size_of::<AccountFilter>(), 128);
556 }
557
558 #[test]
559 fn test_query_filter_size() {
560 assert_eq!(std::mem::size_of::<QueryFilter>(), 64);
561 }
562
563 #[test]
564 fn test_create_accounts_result_size() {
565 assert_eq!(std::mem::size_of::<CreateAccountsResult>(), 8);
566 }
567
568 #[test]
569 fn test_create_transfers_result_size() {
570 assert_eq!(std::mem::size_of::<CreateTransfersResult>(), 8);
571 }
572
573 #[test]
574 fn test_register_request_size() {
575 assert_eq!(std::mem::size_of::<RegisterRequest>(), 256);
576 }
577
578 #[test]
579 fn test_register_result_size() {
580 assert_eq!(std::mem::size_of::<RegisterResult>(), 64);
581 }
582
583 #[test]
584 fn test_account_flags() {
585 let flags = AccountFlags::LINKED | AccountFlags::HISTORY;
586 assert_eq!(flags.bits(), 0b1001);
587 }
588
589 #[test]
590 fn test_transfer_flags() {
591 let flags = TransferFlags::PENDING | TransferFlags::LINKED;
592 assert_eq!(flags.bits(), 0b11);
593 }
594}