Raxol.Payments.Checkpoint behaviour (Raxol Payments v0.2.0)

Copy Markdown View Source

Durable idempotency record for an in-flight payment intent.

Cross-chain settlement is asynchronous: there is a window between dispatching an intent (signing and submitting it to the solver) and confirming it landed. A crash in that window is where an ungoverned runtime loses money, the restarted agent has no memory of the in-flight payment, re-quotes, and signs a second time.

A Checkpoint persists the in-flight intent keyed by a stable idempotency key. A resumed Action looks the key up first and polls the existing intent instead of starting a new one, so the signature is released exactly once across a crash.

Injection

The store is passed through the Action context under :checkpoint as a {module, handle} pair, where module implements this behaviour and handle is whatever that module needs to address its storage (an ETS table, a pid, a bridge to an agent context store). When the context carries no checkpoint the recovery path is a no-op and the Action behaves exactly as it did before.

The store must outlive the calling process for recovery to work: an ETS table owned by the agent itself dies with the crash it is meant to survive. Use a table owned by a supervisor-level process (see Raxol.Payments.Checkpoint.ETS) or back it with durable storage for cross-restart recovery.

Record shape

A record is a plain map whose shape the calling Action defines. The load-bearing field is the id a resume polls -- :intent_id for the Xochi intent, :transfer_id for the Relay transfer. Implementations persist whatever else the caller stores; both payment Actions store the full result summary so a resume can return it without re-deriving anything.

Summary

Callbacks

Remove any record stored under key.

Fetch the record stored under key, or :error if none.

Store record under key, overwriting any existing record.

Functions

Drop the record for key. No-op when no store is configured.

Derive a stable idempotency key from the canonical fields of a logical payment.

Fetch the in-flight record for key.

Store the in-flight record for key. No-op when no store is configured.

Types

handle()

@type handle() :: term()

key()

@type key() :: String.t()

record()

@type record() :: map()

store()

@type store() :: {module(), handle()}

Callbacks

delete(handle, key)

@callback delete(handle(), key()) :: :ok

Remove any record stored under key.

fetch(handle, key)

@callback fetch(handle(), key()) :: {:ok, record()} | :error

Fetch the record stored under key, or :error if none.

put(handle, key, record)

@callback put(handle(), key(), record()) :: :ok

Store record under key, overwriting any existing record.

Functions

delete(arg1, key)

@spec delete(store() | nil, key()) :: :ok

Drop the record for key. No-op when no store is configured.

derive_key(fields)

@spec derive_key([term()]) :: key()

Derive a stable idempotency key from the canonical fields of a logical payment.

The same payment (same wallet, route, token, amount, and settlement intent) yields the same key, so a resumed Action recognizes it. Two payments that share every field collide by design; a caller that needs them treated as distinct supplies its own key via context[:idempotency_key].

fetch(arg1, key)

@spec fetch(store() | nil, key()) :: {:ok, record()} | :error

Fetch the in-flight record for key.

Returns :error when no store is configured (nil), so a caller can treat a missing checkpoint and a disabled checkpoint identically.

put(arg1, key, record)

@spec put(store() | nil, key(), record()) :: :ok

Store the in-flight record for key. No-op when no store is configured.