ChainSafe / ChainSafe/canton-middleware

Canton Coin (CC) read-only balance support

Open
#367 0 comments 0 reactions 0 assignees View on GitHub
Type: Feature
Dominant language
Go
Stars
1
Forks
1
Avg merge
40m
Merged PRs (30d)
1

Description

Part of the Fee Module epic (#366). **This is the first deliverable** — the fee engine sizes fees in CC, fails fast for users with no CC, and the reconciler tracks the operator's CC balance, so CC must be readable before any fee work lands. It's also independently useful: users can't currently see their Canton Coin.

## Current state

Canton Coin is **native Amulet** (`Splice.Amulet.Amulet`) and is **not indexed today**. Balances in the middleware are served from the indexer's `indexer_balances` table (both shipped api-server configs run `token_provider.mode: indexer`), maintained per template by bespoke decoders:

- `cip56_package_id` + `filter_mode: all` → every `CIP56.Events.TokenTransferEvent` (Amulet emits none of these).
- `utility_registry_holding_package_id` → `Utility.Registry.Holding.V0.Holding` (USDCx-style balances), via `NewHoldingDecoder`.

Amulet is a different template with a different field shape, so no existing subscription captures it. Scope: **read-only balance display. No CC transfer support.**

## Amulet contract shape

Unlike `Utility.Registry.Holding` (`{registrar, instrument, owner, amount}`), an Amulet is roughly:

```
Splice.Amulet.Amulet
dso : Party -- the DSO party = instrument admin for CC
owner : Party
amount : ExpiringAmount { initialAmount : Decimal, createdAt : Round, ratePerRound : Decimal }
```

Plus separate `LockedAmulet` contracts for locked CC. Amulet is 10 decimals.

## Indexer changes

### 1. Config field (`pkg/indexer/config.go`)

```go
// AmuletPackageID is the DAML package ID (or #package-name) for Splice Amulet
// (Splice.Amulet.Amulet), i.e. Canton Coin. Required to track CC balances —
// without it the indexer never sees Amulet create/archive events and CC stays
// at 0. Leave empty to disable Amulet tracking.
AmuletPackageID string `yaml:"amulet_package_id"`
```

### 2. Template subscription (`pkg/app/indexer/server.go`, `indexerTemplateIDs`)

```go
if cfg.AmuletPackageID != "" {
ids = append(ids, streaming.TemplateID{
PackageID: cfg.AmuletPackageID,
ModuleName: "Splice.Amulet",
EntityName: "Amulet",
})
}
```

### 3. Amulet decoder (`pkg/indexer/engine/decoder.go`) — mirrors `NewHoldingDecoder`

```go
const (
amuletModule = "Splice.Amulet"
amuletEntity = "Amulet"
)

// NewAmuletDecoder decodes Splice Amulet (Canton Coin) holding lifecycle events
// into balance changes, keyed by the DSO party as instrument admin and a fixed
// "CC" instrument id. LockedAmulet contracts are ignored (locked CC is not a
// spendable balance), matching how NewHoldingDecoder excludes locked holdings.
func NewAmuletDecoder(packageID string) Decoder {
return &holdingLikeDecoder{
module: amuletModule,
entity: amuletEntity,
toChange: func(e *lapiv2.CreatedEvent) (*indexer.HoldingChange, error) {
fields := values.RecordFields(e.CreateArguments)
owner := values.Party(fields["owner"])
dso := values.Party(fields["dso"])
amount := amuletAmount(fields["amount"]) // see "Amount decision" below
return &indexer.HoldingChange{
ContractID: e.ContractId,
Owner: owner,
InstrumentAdmin: dso,
InstrumentID: "CC",
Amount: amount,
LedgerOffset: e.Offset,
}, nil
},
}
}
```

Register it in `NewMultiDecoder` when `AmuletPackageID != ""`. No new migration — it writes into the existing `indexer_balances` / `indexer_holdings` tables via `Store.ApplyBalanceDelta` (CREATE increments owner, ARCHIVE decrements).

### Amount decision (needs verification against the Amulet DAR)

`ExpiringAmount` is not a flat number — CC decays by a holding fee each round. Two options:

- **Face value** — sum `amount.initialAmount`. Simplest; slightly overstates as fees accrue between rounds.
- **Interface view** — read the normalized `amount` from the `Splice.Api.Token.HoldingV1` interface view (the SDK already queries this interface in `pkg/cantonsdk/token/client.go`), which reflects the round-adjusted amount.

Recommend confirming what the HoldingV1 view exposes and preferring it for accuracy; fall back to `initialAmount` if the view isn't available on the stream. Document the choice.

## api-server changes — pure config, no code

CC surfaces via ERC-20 `balanceOf` (not `eth_getBalance`, which is hard-wired to 0 as the synthetic gas token). Two additions:

```yaml
# token_provider.indexer.instruments — map symbol -> instrument admin (DSO party)
CC: "${CANTON_CC_ISSUER_PARTY}"

# token.supported_tokens — read-only token entry (no external_transfer)
"0xCC00000000000000000000000000000000000001":
name: "Canton Coin"
symbol: "CC"
decimals: 10
instrument_id: "CC" # must match InstrumentID the Amulet decoder stores
```

Apply across `pkg/config/defaults/config.{api-server,indexer}.*.yaml` and the deployment charts. Omitting `external_transfer` (and adding no transfer wiring) keeps CC read-only.

## Acceptance criteria

- [ ] `amulet_package_id` config field added to the indexer config and default configs.
- [ ] Amulet template subscription appended in `indexerTemplateIDs` when configured.
- [ ] `NewAmuletDecoder` decodes Amulet CREATE/ARCHIVE into `HoldingChange`, excludes `LockedAmulet`, registered in the multi-decoder.
- [ ] Amount semantics (face vs. interface-view) decided and documented; decoder unit tests cover both create and archive deltas.
- [ ] api-server config exposes CC as a read-only token; `balanceOf` on the CC contract returns the indexed CC balance for a registered user.
- [ ] No transfer path added for CC (no `external_transfer`, no transfer choices).
- [ ] Verified end-to-end on a network with real Amulet: a party's CC balance appears via the api-server.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with pkg/indexer/config.go, pkg/app/indexer/server.go, pkg/indexer/engine/decoder.go, and pkg/cantonsdk/token/client.go; verify the Amulet DAR and HoldingV1 amount semantics. Then inspect the default api-server and indexer YAML configurations and deployment charts. Done means tested Amulet create/archive indexing, documented amount behavior, read-only CC balanceOf exposure, and end-to-end balance visibility without transfer support.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
api, backend, database
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.