transfer

package
v0.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 17, 2026 License: Apache-2.0 Imports: 28 Imported by: 0

Documentation

Overview

Package transfer implements the non-custodial prepare/execute transfer API.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrTransferNotFound = errors.New("transfer not found")
	ErrTransferExpired  = errors.New("transfer expired")
	ErrCacheFull        = errors.New("cache is full")
)

Functions

func RegisterRoutes

func RegisterRoutes(r chi.Router, svc Service, logger *zap.Logger)

RegisterRoutes registers the non-custodial prepare/execute transfer endpoints.

Types

type CacheMetrics

type CacheMetrics struct {
	// PutsTotal counts Put calls by result: "ok" or "full".
	PutsTotal *prometheus.CounterVec

	// GetsTotal counts GetAndDelete calls by result: "ok", "not_found", or "expired".
	GetsTotal *prometheus.CounterVec
}

CacheMetrics holds Prometheus collectors for the prepared-transfer cache.

func NewCacheMetrics

func NewCacheMetrics(reg sharedmetrics.NamespacedRegisterer) *CacheMetrics

NewCacheMetrics registers transfer cache metrics against the given registerer.

func NewNopCacheMetrics

func NewNopCacheMetrics() *CacheMetrics

NewNopCacheMetrics returns a CacheMetrics instance backed by a throwaway registry. Use in tests where metric values are not asserted.

type CompletedTransfer added in v0.8.0

type CompletedTransfer struct {
	ContractID      string `json:"contract_id"`
	Kind            string `json:"kind"`   // "direct" | "offer"
	Status          string `json:"status"` // "completed"
	FromPartyID     string `json:"from_party_id"`
	ToPartyID       string `json:"to_party_id"`
	Amount          string `json:"amount"`
	InstrumentAdmin string `json:"instrument_admin"`
	InstrumentID    string `json:"instrument_id"`
	Timestamp       string `json:"timestamp"`       // RFC3339
	TxID            string `json:"tx_id,omitempty"` // ledger update id
	Symbol          string `json:"symbol,omitempty"`
	Decimals        int    `json:"decimals,omitempty"`
	Name            string `json:"name,omitempty"`
	ContractAddress string `json:"contract_address,omitempty"`
}

CompletedTransfer is a single settled transfer in the unified history view, generalized across all tokens (our CIP-56 tokens and external ones like USDCx) and both transfer shapes (direct CIP-56 and offer-based).

type CompletedTransfersList added in v0.8.0

type CompletedTransfersList struct {
	Items   []CompletedTransfer `json:"items"`
	Total   int64               `json:"total"`
	Page    int                 `json:"page"`
	Limit   int                 `json:"limit"`
	HasMore bool                `json:"has_more"`
}

CompletedTransfersList is the HTTP response body for GET /api/v2/transfer/completed.

type CustodialTransferRequest added in v0.8.0

type CustodialTransferRequest struct {
	ToPartyID       string `json:"to_party_id"`      // Recipient Canton party id (<hint>::<fingerprint>)
	Amount          string `json:"amount"`           // Token amount (decimal string)
	Token           string `json:"token"`            // Token symbol
	ValiditySeconds int64  `json:"validity_seconds"` // Offer validity window in seconds; must be > 0
}

CustodialTransferRequest is the HTTP request body for the custodial transfer endpoint, which sends a token to an arbitrary recipient party id in a single server-signed call (the middleware holds the custodial user's Canton key).

type ExecuteRequest

type ExecuteRequest struct {
	TransferID string `json:"transfer_id"`
	Signature  string `json:"signature"` // hex-encoded DER signature
	SignedBy   string `json:"signed_by"` // Canton multihash fingerprint
}

ExecuteRequest is the HTTP request body for executing a prepared transfer.

type ExecuteResponse

type ExecuteResponse struct {
	Status string `json:"status"` // "completed"
}

ExecuteResponse is the HTTP response body for a completed transfer.

type IncomingTransfer

type IncomingTransfer struct {
	ContractID      string `json:"contract_id"`
	SenderPartyID   string `json:"sender_party_id"`
	ReceiverPartyID string `json:"receiver_party_id"`
	Amount          string `json:"amount"`
	InstrumentAdmin string `json:"instrument_admin"`
	InstrumentID    string `json:"instrument_id"`
	Symbol          string `json:"symbol,omitempty"`
	Decimals        int    `json:"decimals,omitempty"`
	Name            string `json:"name,omitempty"`
	ContractAddress string `json:"contract_address,omitempty"`
}

IncomingTransfer represents a single pending inbound transfer offer. Fields downstream of the on-ledger TransferOffer are always populated; token-metadata fields (Symbol, Decimals, ContractAddress, Name) are populated when the instrument is in the api-server's supported_tokens config and omitted otherwise.

type IncomingTransfersList

type IncomingTransfersList struct {
	Items   []IncomingTransfer `json:"items"`
	Total   int64              `json:"total"`
	Page    int                `json:"page"`
	Limit   int                `json:"limit"`
	HasMore bool               `json:"has_more"`
}

IncomingTransfersList is the HTTP response body for GET /api/v2/transfer/incoming. Pagination is page/limit-based to match the indexer's underlying envelope (`pkg/indexer.Page[T]`): callers ask for a specific page rather than carrying an opaque cursor, since the indexer is keyed by ledger_offset and a stable numeric offset is the natural cursor. HasMore is derived from page*limit < total so clients can stop iterating without needing arithmetic.

type IndexerReader added in v0.8.0

type IndexerReader interface {
	GetTransfers(
		ctx context.Context, partyID string, query indexer.TransferQuery, p indexer.Pagination,
	) (*indexer.Page[indexer.Transfer], error)
	// GetTransfer returns a single transfer by its contract id (used to validate a
	// claim-back request directly, without scanning a party's transfer list).
	GetTransfer(ctx context.Context, contractID string) (*indexer.Transfer, error)
}

IndexerReader is the slice of indexer/client.Client the transfer service uses to read TransferOffer state. The indexer is the source of truth for the offer lifecycle (incoming/outgoing/expired/accepted), so the service reads from it rather than re-decoding Canton contracts.

type InstrumentedCache

type InstrumentedCache struct {
	// contains filtered or unexported fields
}

InstrumentedCache wraps a TransferCache and records Prometheus metrics for every Put and GetAndDelete call.

func NewInstrumentedCache

func NewInstrumentedCache(inner TransferCache, metrics *CacheMetrics) *InstrumentedCache

NewInstrumentedCache returns a metrics-instrumented wrapper around the given TransferCache.

func (*InstrumentedCache) GetAndDelete

func (c *InstrumentedCache) GetAndDelete(transferID string) (*token.PreparedTransfer, error)

func (*InstrumentedCache) Put

func (c *InstrumentedCache) Put(transfer *token.PreparedTransfer) error

type OutgoingTransfer added in v0.8.0

type OutgoingTransfer struct {
	ContractID      string `json:"contract_id"`
	SenderPartyID   string `json:"sender_party_id"`
	ReceiverPartyID string `json:"receiver_party_id"`
	Amount          string `json:"amount"`
	InstrumentAdmin string `json:"instrument_admin"`
	InstrumentID    string `json:"instrument_id"`
	Status          string `json:"status"`
	CreatedAt       string `json:"created_at"`           // RFC3339; ledger effective time of the offer's creation
	ExpiresAt       string `json:"expires_at,omitempty"` // RFC3339; omitted when the offer never expires
	Symbol          string `json:"symbol,omitempty"`
	Decimals        int    `json:"decimals,omitempty"`
	Name            string `json:"name,omitempty"`
	ContractAddress string `json:"contract_address,omitempty"`
}

OutgoingTransfer is a single TransferOffer the queried party sent. Mirrors IncomingTransfer but adds Status (pending/expired/completed/canceled/rejected) and ExpiresAt so callers can track an outbound offer through its lifecycle.

type OutgoingTransfersList added in v0.8.0

type OutgoingTransfersList struct {
	Items   []OutgoingTransfer `json:"items"`
	Total   int64              `json:"total"`
	Page    int                `json:"page"`
	Limit   int                `json:"limit"`
	HasMore bool               `json:"has_more"`
}

OutgoingTransfersList is the HTTP response body for GET /api/v2/transfer/outgoing.

type PartyRegistry added in v0.8.0

type PartyRegistry interface {
	PartyExists(ctx context.Context, partyID string) (bool, error)
}

PartyRegistry checks whether a party id is known to the participant's topology. Implemented by the Canton identity client; used to reject external-token transfers to nonexistent parties before submission.

type PrepareAcceptRequest

type PrepareAcceptRequest struct {
	InstrumentAdmin string `json:"instrument_admin"` // Canton party ID of the instrument admin
}

PrepareAcceptRequest is the HTTP request body for preparing a non-custodial accept.

type PrepareRequest

type PrepareRequest struct {
	To              string `json:"to,omitempty"`          // Recipient EVM address (0x...) of a registered user
	ToPartyID       string `json:"to_party_id,omitempty"` // Recipient Canton party id (<hint>::<fingerprint>)
	Amount          string `json:"amount"`                // Token amount (decimal string)
	Token           string `json:"token"`                 // "DEMO" or "PROMPT"
	ValiditySeconds int64  `json:"validity_seconds"`      // Offer validity window in seconds; must be > 0
}

PrepareRequest is the HTTP request body for preparing a non-custodial transfer. Exactly one of To (a registered user's EVM address) or ToPartyID (an arbitrary Canton party id, e.g. a party on an external participant node) must be set.

type PrepareResponse

type PrepareResponse struct {
	TransferID      string `json:"transfer_id"`
	TransactionHash string `json:"transaction_hash"` // hex-encoded hash to sign
	PartyID         string `json:"party_id"`
	ExpiresAt       string `json:"expires_at"` // RFC3339
}

PrepareResponse is the HTTP response body for a prepared transfer.

type PreparedTransferCache

type PreparedTransferCache struct {
	// contains filtered or unexported fields
}

PreparedTransferCache is an in-memory cache for prepared transfers awaiting external signatures.

func NewPreparedTransferCache

func NewPreparedTransferCache(ttl time.Duration, maxSize int) *PreparedTransferCache

NewPreparedTransferCache creates a new cache with the given TTL and a maximum number of entries.

func (*PreparedTransferCache) GetAndDelete

func (c *PreparedTransferCache) GetAndDelete(transferID string) (*token.PreparedTransfer, error)

GetAndDelete atomically retrieves and removes a prepared transfer. Returns ErrTransferNotFound if the ID doesn't exist, ErrTransferExpired if past TTL.

func (*PreparedTransferCache) Put

Put stores a prepared transfer in the cache. It sets ExpiresAt from the cache TTL. Returns ErrCacheFull if the maximum number of entries has been reached.

func (*PreparedTransferCache) Start

Start runs a background goroutine that periodically removes expired entries. It stops when the context is canceled.

type Service

type Service interface {
	Prepare(ctx context.Context, senderEVMAddr string, req *PrepareRequest) (*PrepareResponse, error)
	Execute(ctx context.Context, senderEVMAddr string, req *ExecuteRequest) (*ExecuteResponse, error)

	// SendCustodial performs a single-call, server-signed transfer for a custodial
	// user to an arbitrary recipient party id (the middleware holds the user's
	// Canton key, so there is no client prepare/execute round-trip).
	SendCustodial(ctx context.Context, senderEVMAddr string, req *CustodialTransferRequest) (*ExecuteResponse, error)

	// ListIncoming returns one page of pending inbound TransferOffer details for the
	// user with the given EVM address. This call is unauthenticated — anyone can
	// query any address's pending offers; the response is intentionally minimized
	// (party IDs truncated) to keep that from leaking counterparties.
	ListIncoming(ctx context.Context, evmAddr string, p indexer.Pagination) (*IncomingTransfersList, error)
	// ListOutgoing returns one page of the user's outbound transfers filtered
	// by status (pending / expired / completed / canceled / rejected / all).
	// Like ListIncoming it is unauthenticated and truncates party IDs.
	ListOutgoing(ctx context.Context, evmAddr string, status string, p indexer.Pagination) (*OutgoingTransfersList, error)
	// ListCompleted returns one page of the user's settled transfers across all
	// tokens (TokenTransferEvents and accepted TransferOffers), newest first.
	ListCompleted(ctx context.Context, evmAddr string, p indexer.Pagination) (*CompletedTransfersList, error)
	// PrepareAccept builds a Canton transaction for accepting an inbound offer.
	PrepareAccept(
		ctx context.Context, evmAddr, contractID string, req *PrepareAcceptRequest,
	) (*PrepareResponse, error)
	// ExecuteAccept completes a previously prepared accept using the client's DER signature.
	ExecuteAccept(ctx context.Context, evmAddr string, req *ExecuteRequest) (*ExecuteResponse, error)
	// PrepareWithdraw builds a Canton transaction for a non-custodial sender to claim
	// back (withdraw) a pending/expired offer they sent. Complete it via Execute.
	PrepareWithdraw(ctx context.Context, evmAddr, contractID string) (*PrepareResponse, error)
	// WithdrawCustodial claims back a pending/expired offer for a custodial sender in a
	// single server-signed call.
	WithdrawCustodial(ctx context.Context, evmAddr, contractID string) (*ExecuteResponse, error)
}

Service is the interface for the non-custodial prepare/execute transfer flow.

func NewLog

func NewLog(svc Service, logger *zap.Logger) Service

NewLog creates a logging decorator for the transfer Service. It logs method entry/exit, duration, errors, and sanitized request/response data.

type TransferCache

type TransferCache interface {
	Put(transfer *token.PreparedTransfer) error
	GetAndDelete(transferID string) (*token.PreparedTransfer, error)
}

TransferCache is the interface for caching prepared transfers.

type TransferService

type TransferService struct {
	// contains filtered or unexported fields
}

TransferService implements the non-custodial prepare/execute transfer flow.

func NewTransferService

func NewTransferService(
	cantonToken token.Token,
	userStore UserStore,
	cache TransferCache,
	tokenCfg *pkgtoken.Config,
	offerLister IndexerReader,
	partyRegistry PartyRegistry,
) *TransferService

NewTransferService creates a new TransferService. tokenCfg supplies the list of allowed token symbols (used by Prepare) and the instrument→EVM-contract mapping (used to enrich ListIncoming responses). offerLister is required — the api-server now wires every service to the same indexer client at startup, so ListIncoming relies on it being non-nil.

func (*TransferService) Execute

func (s *TransferService) Execute(ctx context.Context, senderEVMAddr string, req *ExecuteRequest) (*ExecuteResponse, error)

Execute completes a previously prepared transfer using the client's DER signature.

func (*TransferService) ExecuteAccept

func (s *TransferService) ExecuteAccept(ctx context.Context, evmAddr string, req *ExecuteRequest) (*ExecuteResponse, error)

ExecuteAccept completes a previously prepared accept using the client's DER signature.

func (*TransferService) ListCompleted added in v0.8.0

func (s *TransferService) ListCompleted(
	ctx context.Context, evmAddr string, p indexer.Pagination,
) (*CompletedTransfersList, error)

ListCompleted returns one page of the user's settled transfers across all tokens. Data comes from the indexer's generalized completed-transfers query; party IDs are truncated like the other read endpoints since this is unauthenticated.

func (*TransferService) ListIncoming

func (s *TransferService) ListIncoming(ctx context.Context, evmAddr string, p indexer.Pagination) (*IncomingTransfersList, error)

ListIncoming returns one page of pending inbound TransferOffer details for the user with the given EVM address. Unauthenticated: callers do not need to prove ownership of evmAddr. Data comes from the indexer's `indexer_pending_offers` table (already filtered to status=PENDING at the SQL level), so a single indexer call serves a single client page — no buffering, no re-aggregation.

func (*TransferService) ListOutgoing added in v0.8.0

func (s *TransferService) ListOutgoing(
	ctx context.Context, evmAddr string, status string, p indexer.Pagination,
) (*OutgoingTransfersList, error)

ListOutgoing returns one page of the user's outbound transfers filtered by status. Data comes from the indexer (role=sender); party IDs are truncated like ListIncoming since the endpoint is unauthenticated.

func (*TransferService) Prepare

func (s *TransferService) Prepare(ctx context.Context, senderEVMAddr string, req *PrepareRequest) (*PrepareResponse, error)

Prepare builds a Canton transaction and returns the hash for external signing.

func (*TransferService) PrepareAccept

func (s *TransferService) PrepareAccept(
	ctx context.Context, evmAddr, contractID string, req *PrepareAcceptRequest,
) (*PrepareResponse, error)

PrepareAccept builds a Canton transaction for accepting an inbound offer.

func (*TransferService) PrepareWithdraw added in v0.8.0

func (s *TransferService) PrepareWithdraw(ctx context.Context, evmAddr, contractID string) (*PrepareResponse, error)

PrepareWithdraw builds a Canton transaction for a non-custodial sender to claim back (withdraw) a pending or expired offer they sent. Returns the hash to sign; complete it via the standard Execute endpoint.

func (*TransferService) SendCustodial added in v0.8.0

func (s *TransferService) SendCustodial(
	ctx context.Context, senderEVMAddr string, req *CustodialTransferRequest,
) (*ExecuteResponse, error)

SendCustodial performs a single-call, server-signed transfer for a custodial user to an arbitrary recipient party id. The middleware holds the custodial user's Canton key, so it both prepares and executes the transfer (no client round-trip). The recipient must accept the resulting TransferOffer on their own participant node; this call only creates and submits the offer.

func (*TransferService) WithdrawCustodial added in v0.8.0

func (s *TransferService) WithdrawCustodial(ctx context.Context, evmAddr, contractID string) (*ExecuteResponse, error)

WithdrawCustodial claims back a pending or expired offer for a custodial sender in a single server-signed call (the middleware holds the user's Canton key).

type UserStore

type UserStore interface {
	GetUserByEVMAddress(ctx context.Context, evmAddress string) (*user.User, error)
	GetUserByCantonPartyID(ctx context.Context, partyID string) (*user.User, error)
}

UserStore is the narrow interface for looking up users.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL