The Purchase module manages the lifecycle of a storage agreement between a client and a host in Codex. It ensures that each purchase is correctly initialized, tracked, and completed according to the state of its corresponding `StorageRequest` in the marketplace.
Purchases are implemented as a state machine that progresses through defined states until reaching a deterministic terminal state (`finished`, `cancelled`, `failed`, or `errored`).
The `StorageRequest` contains all necessary data for the agreement (CID, collateral, expiry, etc.) and is stored in the `marketplace` dependency.
In this document, on-chain refers to interactions with that marketplace, but the abstraction could be replaceable: it could point to a different backend or custom storage system in future implementations.
- Every purchase represents exactly one`StorageRequest`.
- The purchase must have a unique, deterministic identifier `PurchaseId` derived from `requestId`.
- It must be possible to restore any purchase from its `requestId` after a restart.
- A purchase is considered expired when the expiry timestamp in its `StorageRequest` is reached before the request start, i.e, an event `RequestFulfilled` is emitted by the `marketplace`.
### 3.2 State Machine Progression
- New purchases start in the `pending` state (submission flow).
- Recovered purchases start in the `unknown` state (recovery flow).
- The state machine progresses step-by-step until a deterministic terminal state (`finished`, `cancelled`, `failed`, or `errored`) is reached.
- The choice of terminal state is based on the `RequestState` returned by the `marketplace`.
### 3.3 Failure Handling
- On `marketplace` failure events, immediately transition to `errored` without retries.
- If a `CancelledError` is raised, log the cancellation and stop further processing.
- If a `CatchableError` is raised, transition to `errored` and record the error.
---
## 4. Non-Functional Requirements (how it should behave)
- **Execution model:** A purchase is handled by a single thread; only one worker should process a given purchase instance at a time.
- **Reliability:** `load` supports recovery after process restarts.
- **Performance:** State transitions should be non-blocking; all I/O is async.
- **Logging:** All state transitions and errors should be clearly logged for traceability.
- **Safety:**
- Avoid side effects during `new` other than initialising internal fields; `on-chain` interactions are delegated to states using `marketplace `dependency.
- Retry policy for external calls.
- **Testing:**
- Unit tests check that each state handles success and error properly.
- Integration tests check that a full purchase flows correctly through states.
---
## 5. Internal Behavior
### 5.1 State identifiers
* PurchasePending: `pending`
* PurchaseSubmitted: `submitted`
* PurchaseStarted: `started`
* PurchaseFinished: `finished`
* PurchaseErrored: `errored`
* PurchaseCancelled: `cancelled`
* PurchaseFailed: `failed`
* PurchaseUnknown: `unknown`
### 5.2 General rules for all states
- If a `CancelledError` is raised, the state machine logs the cancellation message and takes no further action.
- If a `CatchableError` is raised, the state machine moves to `errored` with the error message.
### 5.3 State descriptions
`pending`: A storage request is being created by making a call `on-chain`. If the storage request creation fails, the state machine moves to the `errored` state with the corresponding error.
`submitted`: The storage request has been created and the purchase waits for the request to start. When it starts, an `on-chain` event `RequestFulfilled` is emitted, triggering the subscription callback, and the state machine moves to the `started` state. If the expiry is reached before the callback is called, the state machine moves to the `cancelled` state.
`started`: At this point, the purchase is active and waits until the end of the request—defined by the storage request parameters, before moving to the `finished` state. A subscription is made to the `marketplace` to be notified about request failure. If a request failure is notified, the state machine moves to `failed`.
`finished`: The purchase is considered successful and cleanup routines are called. The purchase module calls `marketplace.withdrawFunds` to release the funds locked by the `marketplace`:
After that, the purchase is done; no more states are called and the state machine stops successfully.
`failed`: If the `marketplace` emits a `RequestFailed` event, the state machine moves to the `failed` state and the purchase module calls `marketplace.withdrawFunds` (same signature as above) to release the funds locked by the `marketplace`. After that, the state machine moves to `errored`.
`cancelled`: The purchase is cancelled and the purchase module calls `marketplace.withdrawFunds` to release the funds locked by the `marketplace` (same signature as above). After that, the purchase is terminated; no more states are called and the state machine stops with the reason of failure as error.
`errored`: The purchase is terminated; no more states are called and the state machine stops with the reason of failure as error.
`unknown`: The purchase is in recovery mode, meaning that the state has to be determined. The purchase module calls the `marketplace` to get the request data (`getRequest`) and the request state (`requestState`):
Based on this information, it moves to the corresponding next state.
### 5.4 State diagram
``` |
v
------------------------- unknown
| / /
v v /
pending ----> submitted ----> started ---------> finished <----/
\ \ /
\ ------------> failed <----/
\ /
--> cancelled <-----------------------
```
**Note**:
Any state can transition to errored upon a `CatchableError`.
`failed` is an intermediate state before `errored`.
`finished`, `cancelled`, and `errored` are terminal states.
---
## 6. Dependencies
- **marketplace**: External dependency used to submit and monitor storage requests (`requestStorage`, `withdrawFunds`, subscriptions for request events).
- **clock**: Provides timing utilities, used for expiry and scheduling logic.
- **nim-chronos**: Async runtime used for futures, awaiting I/O, and cancellation handling.
- **asyncstatemachine**: Base state machine framework used to implement the purchase lifecycle.
- **hashes**: Standard Nim hashing for `PurchaseId`.
- **questionable**: Used for optional fields like `request`.