Skip to content

feat: accept worker for custodial inbound transfers#268

Merged
sadiq1971 merged 14 commits into
mainfrom
feat/accept-worker
May 8, 2026
Merged

feat: accept worker for custodial inbound transfers#268
sadiq1971 merged 14 commits into
mainfrom
feat/accept-worker

Conversation

@sadiq1971
Copy link
Copy Markdown
Member

Summary

  • Adds background AcceptWorker that polls the indexer for pending inbound TransferOffers and auto-accepts them on behalf of custodial parties
  • Adds optional AcceptWorkerConfig to APIServer config; omitting the block disables the worker
  • Worker filters to custodial-only parties, paginates through all offers (200/page), logs and continues on individual accept errors

Test plan

  • TestAcceptWorker_SkipsExternalUsers — external-key users are skipped
  • TestAcceptWorker_AcceptsSingleOffer — happy path, single offer accepted
  • TestAcceptWorker_LogsAndContinuesOnAcceptError — first offer fails, second still processed
  • TestAcceptWorker_LogsAndContinuesOnIndexerError — indexer down, no panic
  • TestAcceptWorker_StopsOnContextCancel — goroutine exits cleanly on cancel
  • TestAcceptWorker_PaginatesAllOffers — 2-page scenario, both pages fetched and accepted
  • TestAcceptWorker_ListUsersError — DB error on ListUsers, no panic

sadiq1971 added 10 commits May 6, 2026 23:45
…tstrap

Switch from CIP56TransferFactory (atomic, local-only) to DA's AllocationFactory
from utility_registry_app_v0. This factory creates TransferOffer contracts on
TransferFactory_Transfer, matching devnet behaviour where the receiver must
exercise TransferInstruction_Accept to complete the transfer.

Also bootstraps TransferRule and InstrumentConfiguration (with empty
holderRequirements) which are required as choiceContextData when the receiver
accepts the transfer offer.

Closes #258
Adds the receiver-side accept context endpoint used by the two-step
AllocationFactory transfer flow. The endpoint returns TransferRule and
InstrumentConfiguration contract IDs as AV_ContractId AnyValues plus
their createdEventBlobs as disclosedContracts, matching DA's registrar
protocol.

Also migrates the sender-side factory lookup from CIP56TransferFactory
(cip56PkgID) to AllocationFactory (utility_registry_app_v0 package),
and adds a shared fetchContractFromACS helper used by both paths.

Closes #259
Adds receiver-side accept support to the Canton token SDK:

- types.go: AnyValue, AcceptChoiceContext, AcceptContextResponse
- encode.go: encodeAnyValue (full AV_* ADT encoder), encodeChoiceContextRecord
  (builds ChoiceContext { values: TextMap AnyValue } for TransferInstruction_Accept)
- registry_client.go: GetAcceptChoiceContext (POST to registrar accept endpoint),
  parseTemplateID (handles both "pkg:module:entity" string and object forms),
  convertDisclosedContractSlice (sets TemplateId on proto, shared by both paths);
  registryDisclosedContract.TemplateID changed to json.RawMessage
- config.go: UtilityRegistryAppPackageID for TransferOffer ACS queries
- client.go: FindPendingInboundTransferInstructions, AcceptTransferInstruction
  (calls registrar, encodes AnyValue context, submits via SubmitAndWait)
- encode_test.go: unit tests for all AV_* variants and encodeChoiceContextRecord
- mocks: update both Token mocks with the two new interface methods

Closes #260
…ng TransferOffer tracking

- Generalise Stream[*ParsedEvent] → Stream[any] with type-switched batch processing
- Fetcher now takes []TemplateID (was single) + any-typed decode function
- NewMultiDecoder wraps TokenTransfer + TransferOffer decoders into single any decoder
- Add PendingOffer type + IndexerPendingOffers table (migration #5)
- InsertPendingOffer on CREATED, DeletePendingOffer on ARCHIVED event
- GET /indexer/v1/admin/parties/{partyID}/pending-offers?after_offset=N endpoint
- Add GetPendingOffersForParty to service, store, and HTTP client interfaces
- All mocks regenerated to reflect interface changes

Closes #264
… pagination

- Replace DELETE on archive with status='ACCEPTED' UPDATE (historical audit trail)
- Add OfferStatus type (PENDING/ACCEPTED) to types.go and store model
- GetPendingOffersForParty now returns *Page[PendingOffer] matching all other list APIs
- engine.Store: rename DeletePendingOffer → MarkOfferAccepted; remove ListPendingOffersForParty
- service.Store: ListPendingOffersForParty uses Pagination (page/limit) not afterOffset
- client.Client: GetPendingOffersForParty uses Pagination and returns *Page[PendingOffer]
- Regenerate all affected mocks
- decoder: add logger to NewOfferDecoder; warn when receiver is empty on CREATED event
- processor: add default case in type switch to log unknown item types instead of silently dropping
- store: fix InsertPendingOffer to not mutate caller input (set status on DAO copy)
- migration: use composite index (receiver_party_id, status) for ListPendingOffersForParty query
- tests: add PendingOffer test cases (CREATED insert, ARCHIVED mark-accepted, mixed batch)
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented May 7, 2026

Codecov Report

❌ Patch coverage is 46.03175% with 68 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@d868b13). Learn more about missing BASE report.

Files with missing lines Patch % Lines
pkg/indexer/store/pg.go 0.00% 17 Missing ⚠️
pkg/app/api/server.go 0.00% 16 Missing ⚠️
pkg/userstore/pg.go 0.00% 11 Missing ⚠️
pkg/indexer/service/http.go 10.00% 9 Missing ⚠️
pkg/indexer/client/http.go 0.00% 6 Missing ⚠️
pkg/indexer/service/service.go 0.00% 5 Missing ⚠️
pkg/custodial/accept_worker.go 96.61% 2 Missing ⚠️
pkg/indexer/service/log.go 0.00% 2 Missing ⚠️

❌ Your patch status has failed because the patch coverage (46.03%) is below the target coverage (50.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main     #268   +/-   ##
=======================================
  Coverage        ?   31.96%           
=======================================
  Files           ?      129           
  Lines           ?     9485           
  Branches        ?        0           
=======================================
  Hits            ?     3032           
  Misses          ?     6189           
  Partials        ?      264           
Flag Coverage Δ
unittests 31.96% <46.03%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
pkg/config/config.go 71.18% <ø> (ø)
pkg/custodial/accept_worker.go 96.61% <96.61%> (ø)
pkg/indexer/service/log.go 0.00% <0.00%> (ø)
pkg/indexer/service/service.go 73.52% <0.00%> (ø)
pkg/indexer/client/http.go 77.77% <0.00%> (ø)
pkg/indexer/service/http.go 75.33% <10.00%> (ø)
pkg/userstore/pg.go 76.63% <0.00%> (ø)
pkg/app/api/server.go 0.00% <0.00%> (ø)
pkg/indexer/store/pg.go 64.21% <0.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a background worker, AcceptWorker, designed to automatically accept inbound USDCx TransferOffers for custodial parties by polling an indexer service. The implementation includes new configuration structures, integration into the API server startup, and comprehensive unit tests with mocks. Feedback focuses on improving responsiveness by performing an initial poll immediately upon startup and optimizing performance by filtering for custodial users at the database level rather than in memory.

Comment thread pkg/custodial/accept_worker.go
Comment thread pkg/transfer/accept_worker.go Outdated
@sadiq1971 sadiq1971 changed the base branch from feat/indexer-multi-template-dispatch to main May 7, 2026 20:09
sadiq1971 added 2 commits May 8, 2026 02:09
- Adds GetAllPendingOffers to indexer (store/service/http/client) so the
  worker can fetch all PENDING offers in one paginated stream instead of
  one query per custodial user
- Inverts loop: ListUsers once → build custodialParties map → paginate all
  offers → skip non-custodial receivers; O(1 DB + P pages) per cycle
- Moves AcceptWorker from pkg/transfer/ to new pkg/custodial/ package
@salindne salindne self-requested a review May 7, 2026 20:39
@salindne
Copy link
Copy Markdown
Contributor

salindne commented May 7, 2026

pkg/token/mocks/mock_canton_token.go
pkg/transfer/mocks/mock_canton_token.go
pkg/custodial/mocks/mock_canton_token.go
maybe more?

looks like several copies of Token mock.

just flagging, cleaning it up could be its own PR later, ill make issue

sadiq1971 added 2 commits May 8, 2026 14:17
Replace in-memory KeyMode filter with a DB-level WHERE clause.
UserLister.ListUsers renamed to ListCustodialUsers; pgStore implements
it with WHERE key_mode = 'custodial'.
@sadiq1971 sadiq1971 merged commit 5f2f9c2 into main May 8, 2026
3 checks passed
@sadiq1971 sadiq1971 deleted the feat/accept-worker branch May 8, 2026 08:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants