Skip to content

feat: integrate Transfer Factory Registry API for external token transfers#230

Merged
salindne merged 1 commit intofeat/213-usdcx-configfrom
feat/214-transfer-factory-registry
Apr 23, 2026
Merged

feat: integrate Transfer Factory Registry API for external token transfers#230
salindne merged 1 commit intofeat/213-usdcx-configfrom
feat/214-transfer-factory-registry

Conversation

@salindne
Copy link
Copy Markdown
Contributor

Summary

Route transfer factory discovery by InstrumentAdmin — local ACS query for our tokens, external registry HTTP call for third-party tokens (USDCx).

Key changes:

  • RegistryClient — HTTP POST to Splice Transfer Factory Registry API, parses factoryId + choiceContext + disclosedContracts
  • resolveTransferFactory — routes by InstrumentAdmin automatically (== IssuerParty → local, otherwise → registry)
  • DisclosedContracts from registry response threaded through PrepareSubmissionRequest (both custodial and non-custodial paths)
  • EncodeExtraArgs(choiceContext) — accepts choice context from registry response
  • Conditional ReadAs — IssuerParty for local tokens, empty for external tokens
  • ExternalTokens config maps InstrumentAdmin → registry URL
  • WithRegistryClient option on token client constructor

No breaking changes to public API (TransferByPartyID, PrepareTransfer). Routing is internal based on holdings data.

Depends on #229 (#213), #228 (#218), #227 (#212)
Closes #214

Test plan

  • go build ./... passes
  • All 19 test packages pass (no regressions)
  • Lint: 0 issues
  • Devnet test: configure external_tokens with USDCx registry URL, attempt USDCx transfer
  • DEMO regression: verify existing transfers still work

…sfers

Add support for transferring tokens issued by external parties (e.g.,
USDCx from Circle) by routing factory discovery based on InstrumentAdmin:

- Local tokens (InstrumentAdmin == IssuerParty): existing ACS query
- External tokens: POST to Transfer Factory Registry API

Key changes:
- New RegistryClient (HTTP POST) calls Splice Transfer Factory Registry
  and parses factoryId, choiceContext, disclosedContracts from response
- resolveTransferFactory routes by InstrumentAdmin automatically
- DisclosedContracts from registry response threaded through
  PrepareSubmissionRequest for both custodial and non-custodial paths
- EncodeExtraArgs now accepts choiceContext from registry response
- ReadAs conditionally set: IssuerParty for local, empty for external
- ExternalTokens config maps InstrumentAdmin → registry URL
- WithRegistryClient option on token client constructor

No breaking changes to public API (TransferByPartyID, PrepareTransfer).
Routing is internal based on holdings data.

Closes #214
@codecov-commenter
Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 148 lines in your changes missing coverage. Please review.
✅ Project coverage is 32.46%. Comparing base (ad333b4) to head (333a6f5).

Files with missing lines Patch % Lines
pkg/cantonsdk/token/client.go 0.00% 80 Missing ⚠️
pkg/cantonsdk/token/registry_client.go 0.00% 54 Missing ⚠️
pkg/cantonsdk/values/meta.go 0.00% 7 Missing ⚠️
pkg/cantonsdk/client/client.go 0.00% 4 Missing ⚠️
pkg/cantonsdk/token/options.go 0.00% 2 Missing ⚠️
pkg/cantonsdk/token/encode.go 0.00% 1 Missing ⚠️

❌ Your patch status has failed because the patch coverage (0.00%) 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                    @@
##           feat/213-usdcx-config     #230      +/-   ##
=========================================================
- Coverage                  32.90%   32.46%   -0.45%     
=========================================================
  Files                        123      124       +1     
  Lines                       8551     8643      +92     
=========================================================
- Hits                        2814     2806       -8     
- Misses                      5489     5589     +100     
  Partials                     248      248              
Flag Coverage Δ
unittests 32.46% <0.00%> (-0.45%) ⬇️

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

Files with missing lines Coverage Δ
pkg/cantonsdk/token/config.go 0.00% <ø> (ø)
pkg/cantonsdk/token/encode.go 0.00% <0.00%> (ø)
pkg/cantonsdk/token/options.go 0.00% <0.00%> (ø)
pkg/cantonsdk/client/client.go 0.00% <0.00%> (ø)
pkg/cantonsdk/values/meta.go 0.00% <0.00%> (ø)
pkg/cantonsdk/token/registry_client.go 0.00% <0.00%> (ø)
pkg/cantonsdk/token/client.go 0.00% <0.00%> (ø)

... and 1 file with indirect coverage changes

🚀 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 support for external tokens by implementing a RegistryClient that interacts with the Splice Transfer Factory Registry API. The token.Client has been updated to resolve transfer factories through this registry when dealing with external instrument admins, and the transfer logic now supports disclosed contracts and choice contexts. I have no feedback to provide.

if s.keyResolver != nil {
tokenOpts = append(tokenOpts, token.WithKeyResolver(s.keyResolver))
}
if len(cfg.Token.ExternalTokens) > 0 {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Are we planning to integrate this config in a separate PR?

@@ -0,0 +1,145 @@
package token
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What about moving this to a different package for simplicity? like pkg/cantonsdk/registry

@salindne salindne merged commit 61c6eab into feat/213-usdcx-config Apr 23, 2026
2 checks passed
@salindne salindne deleted the feat/214-transfer-factory-registry branch April 23, 2026 16:39
salindne added a commit that referenced this pull request Apr 23, 2026
* feat: add USDCx to supported tokens config

Add USDCx (USD Coin, 6 decimals) to supported_tokens in all config
YAML files. This enables MetaMask to query balanceOf, symbol, decimals,
and name for USDCx through the ERC-20 facade.

USDCx is automatically added to the non-custodial transfer allowlist
since it is derived from supported_tokens.

Mainnet config uses ${CANTON_USDCX_INSTRUMENT_ADMIN} env var for the
instrument admin (Circle's Bridge-Operator party).

Closes #213

* feat: integrate Transfer Factory Registry API for external token transfers (#230)

Add support for transferring tokens issued by external parties (e.g.,
USDCx from Circle) by routing factory discovery based on InstrumentAdmin:

- Local tokens (InstrumentAdmin == IssuerParty): existing ACS query
- External tokens: POST to Transfer Factory Registry API

Key changes:
- New RegistryClient (HTTP POST) calls Splice Transfer Factory Registry
  and parses factoryId, choiceContext, disclosedContracts from response
- resolveTransferFactory routes by InstrumentAdmin automatically
- DisclosedContracts from registry response threaded through
  PrepareSubmissionRequest for both custodial and non-custodial paths
- EncodeExtraArgs now accepts choiceContext from registry response
- ReadAs conditionally set: IssuerParty for local, empty for external
- ExternalTokens config maps InstrumentAdmin → registry URL
- WithRegistryClient option on token client constructor

No breaking changes to public API (TransferByPartyID, PrepareTransfer).
Routing is internal based on holdings data.

Closes #214
salindne added a commit that referenced this pull request Apr 23, 2026
* feat: unified balance query via Splice HoldingV1 interface

Add GetHoldingsByParty method that queries Canton using the Splice
HoldingV1 interface as the user's own party, instead of querying
CIP56Holding template as IssuerParty.

This enables visibility of tokens issued by third parties (e.g., USDCx
from Circle) where our IssuerParty is not a stakeholder on the holdings.

GetHoldings now delegates to GetHoldingsByParty. GetAllHoldings retains
the IssuerParty/template path for totalSupply and indexer use.

Closes #218

* docs: clarify that CreateArguments is always populated for interface queries

Canton Ledger API v2 proto marks create_arguments as Required on
CreatedEvent, so decodeHolding works identically for both template-based
and interface-based queries. Add comment to address review concern.

* chore: add TODO for GetHoldingsByParty unit tests

* feat: add USDCx to supported tokens config (#229)

* feat: add USDCx to supported tokens config

Add USDCx (USD Coin, 6 decimals) to supported_tokens in all config
YAML files. This enables MetaMask to query balanceOf, symbol, decimals,
and name for USDCx through the ERC-20 facade.

USDCx is automatically added to the non-custodial transfer allowlist
since it is derived from supported_tokens.

Mainnet config uses ${CANTON_USDCX_INSTRUMENT_ADMIN} env var for the
instrument admin (Circle's Bridge-Operator party).

Closes #213

* feat: integrate Transfer Factory Registry API for external token transfers (#230)

Add support for transferring tokens issued by external parties (e.g.,
USDCx from Circle) by routing factory discovery based on InstrumentAdmin:

- Local tokens (InstrumentAdmin == IssuerParty): existing ACS query
- External tokens: POST to Transfer Factory Registry API

Key changes:
- New RegistryClient (HTTP POST) calls Splice Transfer Factory Registry
  and parses factoryId, choiceContext, disclosedContracts from response
- resolveTransferFactory routes by InstrumentAdmin automatically
- DisclosedContracts from registry response threaded through
  PrepareSubmissionRequest for both custodial and non-custodial paths
- EncodeExtraArgs now accepts choiceContext from registry response
- ReadAs conditionally set: IssuerParty for local, empty for external
- ExternalTokens config maps InstrumentAdmin → registry URL
- WithRegistryClient option on token client constructor

No breaking changes to public API (TransferByPartyID, PrepareTransfer).
Routing is internal based on holdings data.

Closes #214
salindne added a commit that referenced this pull request Apr 28, 2026
…#227)

* feat: add instrument_id to token config and splice_holding_package_id

Add InstrumentID and InstrumentAdmin fields to ERC20Token struct
for per-token Canton instrument mapping. InstrumentID is required;
InstrumentAdmin defaults to IssuerParty when empty.

Add SpliceHoldingPackageID to cantonsdk/token.Config for Splice
HoldingV1 interface queries (needed by #218 unified balance query).

Update all config YAML files (defaults, test fixtures, e2e-local,
devnet) with instrument_id per token and splice_holding_package_id.

Closes #212

* fix: pass stack.Token by pointer in WaitForAPIBalance to fix hugeParam lint

* fix: use env vars for mainnet splice_holding_package_id, add test env

Address review feedback:
- Use ${CANTON_SPLICE_HOLDING_PACKAGE_ID} env var in mainnet configs
  (consistent with other package ID fields)
- Add CANTON_SPLICE_HOLDING_PACKAGE_ID to setDefaultConfigEnv test helper

* fix: update remaining WaitForAPIBalance callers to pass Token by pointer

* feat: unified balance query via Splice HoldingV1 interface (#228)

* feat: unified balance query via Splice HoldingV1 interface

Add GetHoldingsByParty method that queries Canton using the Splice
HoldingV1 interface as the user's own party, instead of querying
CIP56Holding template as IssuerParty.

This enables visibility of tokens issued by third parties (e.g., USDCx
from Circle) where our IssuerParty is not a stakeholder on the holdings.

GetHoldings now delegates to GetHoldingsByParty. GetAllHoldings retains
the IssuerParty/template path for totalSupply and indexer use.

Closes #218

* docs: clarify that CreateArguments is always populated for interface queries

Canton Ledger API v2 proto marks create_arguments as Required on
CreatedEvent, so decodeHolding works identically for both template-based
and interface-based queries. Add comment to address review concern.

* chore: add TODO for GetHoldingsByParty unit tests

* feat: add USDCx to supported tokens config (#229)

* feat: add USDCx to supported tokens config

Add USDCx (USD Coin, 6 decimals) to supported_tokens in all config
YAML files. This enables MetaMask to query balanceOf, symbol, decimals,
and name for USDCx through the ERC-20 facade.

USDCx is automatically added to the non-custodial transfer allowlist
since it is derived from supported_tokens.

Mainnet config uses ${CANTON_USDCX_INSTRUMENT_ADMIN} env var for the
instrument admin (Circle's Bridge-Operator party).

Closes #213

* feat: integrate Transfer Factory Registry API for external token transfers (#230)

Add support for transferring tokens issued by external parties (e.g.,
USDCx from Circle) by routing factory discovery based on InstrumentAdmin:

- Local tokens (InstrumentAdmin == IssuerParty): existing ACS query
- External tokens: POST to Transfer Factory Registry API

Key changes:
- New RegistryClient (HTTP POST) calls Splice Transfer Factory Registry
  and parses factoryId, choiceContext, disclosedContracts from response
- resolveTransferFactory routes by InstrumentAdmin automatically
- DisclosedContracts from registry response threaded through
  PrepareSubmissionRequest for both custodial and non-custodial paths
- EncodeExtraArgs now accepts choiceContext from registry response
- ReadAs conditionally set: IssuerParty for local, empty for external
- ExternalTokens config maps InstrumentAdmin → registry URL
- WithRegistryClient option on token client constructor

No breaking changes to public API (TransferByPartyID, PrepareTransfer).
Routing is internal based on holdings data.

Closes #214

* remove unused InstrumentAdmin field from ERC20Token

The field was never read anywhere in the codebase. Transfer-factory routing
uses InstrumentAdmin from the on-chain holding contract, not config. External
tokens are handled via cantonsdk/token.Config.ExternalTokens keyed by admin
party ID after #228.

Removes the field, the instrument_admin yaml keys in local-devnet and mainnet
configs, and the CANTON_USDCX_INSTRUMENT_ADMIN env var from config tests.

* fix(e2e): set splice_holding_package_id in canton shim token configs

PR 228 made SpliceHoldingPackageID required on cantonsdk/token.Config.
The e2e canton shim was missing it on both the DEMO and PROMPT token
clients, causing every test to fail with 'splice_holding_package_id is
required' at api/full stack init.

Adds the constant alongside the other devnet package IDs (matches the
value in config.api-server.local-devnet.yaml) and wires it into both
token.Config instances.
salindne added a commit that referenced this pull request Apr 29, 2026
* feat: add instrument_id to token config and splice_holding_package_id

Add InstrumentID and InstrumentAdmin fields to ERC20Token struct
for per-token Canton instrument mapping. InstrumentID is required;
InstrumentAdmin defaults to IssuerParty when empty.

Add SpliceHoldingPackageID to cantonsdk/token.Config for Splice
HoldingV1 interface queries (needed by #218 unified balance query).

Update all config YAML files (defaults, test fixtures, e2e-local,
devnet) with instrument_id per token and splice_holding_package_id.

Closes #212

* fix: pass stack.Token by pointer in WaitForAPIBalance to fix hugeParam lint

* fix: use env vars for mainnet splice_holding_package_id, add test env

Address review feedback:
- Use ${CANTON_SPLICE_HOLDING_PACKAGE_ID} env var in mainnet configs
  (consistent with other package ID fields)
- Add CANTON_SPLICE_HOLDING_PACKAGE_ID to setDefaultConfigEnv test helper

* fix: update remaining WaitForAPIBalance callers to pass Token by pointer

* feat(indexer): support external token indexing

* updated comment

* review fixes

* review fixes

* feat: unified balance query via Splice HoldingV1 interface (#228)

* feat: unified balance query via Splice HoldingV1 interface

Add GetHoldingsByParty method that queries Canton using the Splice
HoldingV1 interface as the user's own party, instead of querying
CIP56Holding template as IssuerParty.

This enables visibility of tokens issued by third parties (e.g., USDCx
from Circle) where our IssuerParty is not a stakeholder on the holdings.

GetHoldings now delegates to GetHoldingsByParty. GetAllHoldings retains
the IssuerParty/template path for totalSupply and indexer use.

Closes #218

* docs: clarify that CreateArguments is always populated for interface queries

Canton Ledger API v2 proto marks create_arguments as Required on
CreatedEvent, so decodeHolding works identically for both template-based
and interface-based queries. Add comment to address review concern.

* chore: add TODO for GetHoldingsByParty unit tests

* feat: add USDCx to supported tokens config (#229)

* feat: add USDCx to supported tokens config

Add USDCx (USD Coin, 6 decimals) to supported_tokens in all config
YAML files. This enables MetaMask to query balanceOf, symbol, decimals,
and name for USDCx through the ERC-20 facade.

USDCx is automatically added to the non-custodial transfer allowlist
since it is derived from supported_tokens.

Mainnet config uses ${CANTON_USDCX_INSTRUMENT_ADMIN} env var for the
instrument admin (Circle's Bridge-Operator party).

Closes #213

* feat: integrate Transfer Factory Registry API for external token transfers (#230)

Add support for transferring tokens issued by external parties (e.g.,
USDCx from Circle) by routing factory discovery based on InstrumentAdmin:

- Local tokens (InstrumentAdmin == IssuerParty): existing ACS query
- External tokens: POST to Transfer Factory Registry API

Key changes:
- New RegistryClient (HTTP POST) calls Splice Transfer Factory Registry
  and parses factoryId, choiceContext, disclosedContracts from response
- resolveTransferFactory routes by InstrumentAdmin automatically
- DisclosedContracts from registry response threaded through
  PrepareSubmissionRequest for both custodial and non-custodial paths
- EncodeExtraArgs now accepts choiceContext from registry response
- ReadAs conditionally set: IssuerParty for local, empty for external
- ExternalTokens config maps InstrumentAdmin → registry URL
- WithRegistryClient option on token client constructor

No breaking changes to public API (TransferByPartyID, PrepareTransfer).
Routing is internal based on holdings data.

Closes #214

* remove unused InstrumentAdmin field from ERC20Token

The field was never read anywhere in the codebase. Transfer-factory routing
uses InstrumentAdmin from the on-chain holding contract, not config. External
tokens are handled via cantonsdk/token.Config.ExternalTokens keyed by admin
party ID after #228.

Removes the field, the instrument_admin yaml keys in local-devnet and mainnet
configs, and the CANTON_USDCX_INSTRUMENT_ADMIN env var from config tests.

* fix(e2e): set splice_holding_package_id in canton shim token configs

PR 228 made SpliceHoldingPackageID required on cantonsdk/token.Config.
The e2e canton shim was missing it on both the DEMO and PROMPT token
clients, causing every test to fail with 'splice_holding_package_id is
required' at api/full stack init.

Adds the constant alongside the other devnet package IDs (matches the
value in config.api-server.local-devnet.yaml) and wires it into both
token.Config instances.

* usdcx contracts deployments on local docker setup

* usdcx contracts deployments on local docker setup

* usdcx contracts deployments on local docker setup

* feat(cantonsdk/token): add TransferInternalByPartyID for internal Canton party transfers

Closes #247

* feat(e2e): add USDCx cross-participant transfer test and multi-participant devstack

* fix(e2e): lint cleanup in Canton2Shim and types

- gofmt canton2.go imports
- drop unused-receiver names in stub methods
- nosec annotation on USDCxTokenVirtualAddr (public EVM address, not a credential)

* health check added for p2

* fix(ci): fix P2 package vetting race in USDCx bootstrap

Use CMD-SHELL list form for canton healthcheck test to avoid YAML fold
ambiguity, and add a 10s vetting propagation wait to the P2 bootstrap
section — mirroring the equivalent wait already done for P1 — so that
bootstrap-usdcx runs only after P2 packages are fully vetted on the
synchronizer.

---------

Co-authored-by: Sebastian Lindner <[email protected]>
Co-authored-by: Arun Dhyani <[email protected]>
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.

4 participants