diff --git a/config.e2e-local.yaml b/config.e2e-local.yaml index c63eb0f1..a5b06e79 100644 --- a/config.e2e-local.yaml +++ b/config.e2e-local.yaml @@ -74,6 +74,11 @@ token: symbol: "DEMO" decimals: 18 instrument_id: "DEMO" + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48": + name: "USD Coin" + symbol: "USDCx" + decimals: 6 + instrument_id: "USDCx" native_balance_wei: "1000000000000000000000" # Ethereum JSON-RPC facade (MetaMask compatibility) diff --git a/pkg/cantonsdk/client/client.go b/pkg/cantonsdk/client/client.go index 3d1f02f6..06448fac 100644 --- a/pkg/cantonsdk/client/client.go +++ b/pkg/cantonsdk/client/client.go @@ -7,6 +7,8 @@ package client import ( "context" "fmt" + "net/http" + "time" "github.com/chainsafe/canton-middleware/pkg/cantonsdk/bridge" "github.com/chainsafe/canton-middleware/pkg/cantonsdk/identity" @@ -60,6 +62,11 @@ func New(ctx context.Context, cfg *Config, opts ...Option) (*Client, error) { if s.keyResolver != nil { tokenOpts = append(tokenOpts, token.WithKeyResolver(s.keyResolver)) } + if len(cfg.Token.ExternalTokens) > 0 { + tokenOpts = append(tokenOpts, token.WithRegistryClient( + token.NewRegistryClient(&http.Client{Timeout: 10 * time.Second}), + )) + } tk, err := token.New(cfg.Token, l, id, tokenOpts...) if err != nil { _ = l.Close() diff --git a/pkg/cantonsdk/token/client.go b/pkg/cantonsdk/token/client.go index 4cc11d26..11071688 100644 --- a/pkg/cantonsdk/token/client.go +++ b/pkg/cantonsdk/token/client.go @@ -101,11 +101,12 @@ type Token interface { // Client implements CIP-56 token operations. type Client struct { - cfg *Config - ledger ledger.Ledger - identity identity.Identity - keyResolver KeyResolver - logger *zap.Logger + cfg *Config + ledger ledger.Ledger + identity identity.Identity + keyResolver KeyResolver + registryClient *RegistryClient + logger *zap.Logger } // New creates a new token client. @@ -123,11 +124,12 @@ func New(cfg *Config, l ledger.Ledger, id identity.Identity, opts ...Option) (*C s := applyOptions(opts) return &Client{ - cfg: cfg, - ledger: l, - identity: id, - keyResolver: s.keyResolver, - logger: s.logger, + cfg: cfg, + ledger: l, + identity: id, + keyResolver: s.keyResolver, + registryClient: s.registryClient, + logger: s.logger, }, nil } @@ -448,12 +450,7 @@ func (c *Client) TransferByPartyID(ctx context.Context, idempotencyKey, fromPart return fmt.Errorf("select holdings for transfer: %w", err) } - factoryCID, err := c.getTransferFactoryCID(ctx) - if err != nil { - return err - } - - return c.transferViaFactory(ctx, &transferFactoryRequest{ + req := &transferFactoryRequest{ CommandID: idempotencyKey, FromPartyID: fromParty, ToPartyID: toParty, @@ -461,19 +458,27 @@ func (c *Client) TransferByPartyID(ctx context.Context, idempotencyKey, fromPart InstrumentAdmin: selected.InstrumentAdmin, InstrumentID: selected.InstrumentID, InputHoldingCIDs: selected.CIDs, - FactoryCID: factoryCID, - }) + } + + if err := c.resolveTransferFactory(ctx, req); err != nil { + return err + } + + return c.transferViaFactory(ctx, req) } type transferFactoryRequest struct { - CommandID string - FromPartyID string - ToPartyID string - Amount string - InstrumentAdmin string - InstrumentID string - InputHoldingCIDs []string - FactoryCID string + CommandID string + FromPartyID string + ToPartyID string + Amount string + InstrumentAdmin string + InstrumentID string + InputHoldingCIDs []string + FactoryCID string + ChoiceContext map[string]string + DisclosedContracts []*lapiv2.DisclosedContract + IsExternal bool } func (c *Client) transferViaFactory(ctx context.Context, req *transferFactoryRequest) error { @@ -488,13 +493,19 @@ func (c *Client) transferViaFactory(ctx context.Context, req *transferFactoryReq cmd := c.buildTransferCommand(req) + readAs := []string{c.cfg.IssuerParty} + if req.IsExternal { + readAs = nil + } + commands := &lapiv2.Commands{ - SynchronizerId: c.cfg.DomainID, - CommandId: req.CommandID, - UserId: c.cfg.UserID, - ActAs: []string{req.FromPartyID}, - ReadAs: []string{c.cfg.IssuerParty}, - Commands: []*lapiv2.Command{cmd}, + SynchronizerId: c.cfg.DomainID, + CommandId: req.CommandID, + UserId: c.cfg.UserID, + ActAs: []string{req.FromPartyID}, + ReadAs: readAs, + Commands: []*lapiv2.Command{cmd}, + DisclosedContracts: req.DisclosedContracts, } return c.prepareAndExecuteAsUser(ctx, commands, signerKey, req.FromPartyID) @@ -527,6 +538,7 @@ func (c *Client) buildTransferCommand(req *transferFactoryRequest) *lapiv2.Comma now, now.Add(defaultTransferValidity), req.InputHoldingCIDs, + req.ChoiceContext, ), }, }, @@ -543,6 +555,58 @@ func (c *Client) getTransferFactoryCID(ctx context.Context) (string, error) { return info.ContractID, nil } +// resolveTransferFactory fills in factory info on the request by routing based on InstrumentAdmin. +// For our tokens (InstrumentAdmin == IssuerParty): uses local ACS query. +// For external tokens: calls the Transfer Factory Registry API. +func (c *Client) resolveTransferFactory(ctx context.Context, req *transferFactoryRequest) error { + if req.InstrumentAdmin == c.cfg.IssuerParty { + cid, err := c.getTransferFactoryCID(ctx) + if err != nil { + return err + } + req.FactoryCID = cid + return nil + } + + // External token — use registry + if c.registryClient == nil { + return fmt.Errorf("no registry client configured for external token transfers") + } + extCfg, ok := c.cfg.ExternalTokens[req.InstrumentAdmin] + if !ok { + return fmt.Errorf("unsupported external token issuer: %s", req.InstrumentAdmin) + } + + regResp, err := c.registryClient.GetTransferFactory(ctx, extCfg.RegistryURL, &RegistryRequest{ + ExpectedAdmin: req.InstrumentAdmin, + Transfer: RegistryTransferDetail{ + Sender: req.FromPartyID, + Receiver: req.ToPartyID, + Amount: req.Amount, + InstrumentID: req.InstrumentID, + InputHoldingCIDs: req.InputHoldingCIDs, + }, + }) + if err != nil { + return fmt.Errorf("registry lookup for %s: %w", req.InstrumentAdmin, err) + } + + req.FactoryCID = regResp.FactoryID + req.IsExternal = true + + req.ChoiceContext, err = ConvertChoiceContext(regResp.ChoiceContext) + if err != nil { + return fmt.Errorf("convert choice context: %w", err) + } + + req.DisclosedContracts, err = ConvertDisclosedContracts(regResp.DisclosedContracts, c.cfg.DomainID) + if err != nil { + return fmt.Errorf("convert disclosed contracts: %w", err) + } + + return nil +} + func (c *Client) GetTransferFactory(ctx context.Context) (*TransferFactoryInfo, error) { end, err := c.ledger.GetLedgerEnd(ctx) if err != nil { @@ -628,12 +692,13 @@ func (c *Client) prepareAndExecuteAsUser(ctx context.Context, commands *lapiv2.C authCtx := c.ledger.AuthContext(ctx) prepResp, err := c.ledger.Interactive().PrepareSubmission(authCtx, &interactivev2.PrepareSubmissionRequest{ - UserId: commands.UserId, - CommandId: commands.CommandId, - Commands: commands.Commands, - ActAs: commands.ActAs, - ReadAs: commands.ReadAs, - SynchronizerId: commands.SynchronizerId, + UserId: commands.UserId, + CommandId: commands.CommandId, + Commands: commands.Commands, + ActAs: commands.ActAs, + ReadAs: commands.ReadAs, + SynchronizerId: commands.SynchronizerId, + DisclosedContracts: commands.DisclosedContracts, }) if err != nil { return fmt.Errorf("prepare submission: %w", err) @@ -739,11 +804,6 @@ func (c *Client) PrepareTransfer(ctx context.Context, req *PrepareTransferReques return nil, fmt.Errorf("select holdings for transfer: %w", err) } - factoryCID, err := c.getTransferFactoryCID(ctx) - if err != nil { - return nil, err - } - factoryReq := &transferFactoryRequest{ FromPartyID: req.FromPartyID, ToPartyID: req.ToPartyID, @@ -751,27 +811,38 @@ func (c *Client) PrepareTransfer(ctx context.Context, req *PrepareTransferReques InstrumentAdmin: selected.InstrumentAdmin, InstrumentID: selected.InstrumentID, InputHoldingCIDs: selected.CIDs, - FactoryCID: factoryCID, } + + if resolveErr := c.resolveTransferFactory(ctx, factoryReq); resolveErr != nil { + return nil, resolveErr + } + cmd := c.buildTransferCommand(factoryReq) + readAs := []string{c.cfg.IssuerParty} + if factoryReq.IsExternal { + readAs = nil + } + commands := &lapiv2.Commands{ - SynchronizerId: c.cfg.DomainID, - CommandId: uuid.NewString(), - UserId: c.cfg.UserID, - ActAs: []string{req.FromPartyID}, - ReadAs: []string{c.cfg.IssuerParty}, - Commands: []*lapiv2.Command{cmd}, + SynchronizerId: c.cfg.DomainID, + CommandId: uuid.NewString(), + UserId: c.cfg.UserID, + ActAs: []string{req.FromPartyID}, + ReadAs: readAs, + Commands: []*lapiv2.Command{cmd}, + DisclosedContracts: factoryReq.DisclosedContracts, } authCtx := c.ledger.AuthContext(ctx) prepResp, err := c.ledger.Interactive().PrepareSubmission(authCtx, &interactivev2.PrepareSubmissionRequest{ - UserId: commands.UserId, - CommandId: commands.CommandId, - Commands: commands.Commands, - ActAs: commands.ActAs, - ReadAs: commands.ReadAs, - SynchronizerId: commands.SynchronizerId, + UserId: commands.UserId, + CommandId: commands.CommandId, + Commands: commands.Commands, + ActAs: commands.ActAs, + ReadAs: commands.ReadAs, + SynchronizerId: commands.SynchronizerId, + DisclosedContracts: commands.DisclosedContracts, }) if err != nil { return nil, fmt.Errorf("prepare submission: %w", err) diff --git a/pkg/cantonsdk/token/config.go b/pkg/cantonsdk/token/config.go index 9397ce3f..a93db492 100644 --- a/pkg/cantonsdk/token/config.go +++ b/pkg/cantonsdk/token/config.go @@ -2,6 +2,12 @@ package token import "errors" +// ExternalTokenConfig holds the registry endpoint for an external token issuer. +// Key in the map is the InstrumentAdmin party ID (e.g., Circle's Bridge-Operator). +type ExternalTokenConfig struct { + RegistryURL string `yaml:"registry_url" validate:"required"` +} + // Config contains the configuration required to initialize the token client. type Config struct { DomainID string `yaml:"domain_id"` @@ -11,6 +17,11 @@ type Config struct { CIP56PackageID string `yaml:"cip56_package_id" validate:"required"` SpliceTransferPackageID string `yaml:"splice_transfer_package_id" validate:"required"` SpliceHoldingPackageID string `yaml:"splice_holding_package_id" validate:"required"` + + // ExternalTokens maps InstrumentAdmin party IDs to their registry configuration. + // Tokens whose InstrumentAdmin matches IssuerParty use local ACS-based factory discovery. + // Tokens whose InstrumentAdmin is in this map use the external registry API. + ExternalTokens map[string]ExternalTokenConfig `yaml:"external_tokens"` } func (c *Config) validate() error { diff --git a/pkg/cantonsdk/token/encode.go b/pkg/cantonsdk/token/encode.go index 957d7b66..3fc9d581 100644 --- a/pkg/cantonsdk/token/encode.go +++ b/pkg/cantonsdk/token/encode.go @@ -41,6 +41,7 @@ func encodeTransferFactoryTransferArgs( requestedAt time.Time, executeBefore time.Time, inputHoldingCIDs []string, + choiceContext map[string]string, ) *lapiv2.Record { holdingCidValues := make([]*lapiv2.Value, len(inputHoldingCIDs)) for i, cid := range inputHoldingCIDs { @@ -68,7 +69,7 @@ func encodeTransferFactoryTransferArgs( Fields: []*lapiv2.RecordField{ {Label: "expectedAdmin", Value: values.PartyValue(expectedAdmin)}, {Label: "transfer", Value: transfer}, - {Label: "extraArgs", Value: values.EncodeExtraArgs()}, + {Label: "extraArgs", Value: values.EncodeExtraArgs(choiceContext)}, }, } } diff --git a/pkg/cantonsdk/token/options.go b/pkg/cantonsdk/token/options.go index e03e139d..624a603c 100644 --- a/pkg/cantonsdk/token/options.go +++ b/pkg/cantonsdk/token/options.go @@ -3,8 +3,9 @@ package token import "go.uber.org/zap" type settings struct { - logger *zap.Logger - keyResolver KeyResolver + logger *zap.Logger + keyResolver KeyResolver + registryClient *RegistryClient } // Option configures the token client. @@ -21,6 +22,12 @@ func WithKeyResolver(kr KeyResolver) Option { return func(s *settings) { s.keyResolver = kr } } +// WithRegistryClient sets the HTTP client for external Transfer Factory Registry API calls. +// Required for transferring tokens issued by external parties (e.g., USDCx). +func WithRegistryClient(rc *RegistryClient) Option { + return func(s *settings) { s.registryClient = rc } +} + func applyOptions(opts []Option) settings { s := settings{logger: zap.NewNop()} for _, opt := range opts { diff --git a/pkg/cantonsdk/token/registry_client.go b/pkg/cantonsdk/token/registry_client.go new file mode 100644 index 00000000..b6a57ec8 --- /dev/null +++ b/pkg/cantonsdk/token/registry_client.go @@ -0,0 +1,145 @@ +package token + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + lapiv2 "github.com/chainsafe/canton-middleware/pkg/cantonsdk/lapi/v2" +) + +const registryPath = "/registry/transfer-instruction/v1/transfer-factory" + +// RegistryClient calls the Splice Transfer Factory Registry API to discover +// transfer factories for external tokens (e.g., USDCx). +type RegistryClient struct { + httpClient *http.Client +} + +// NewRegistryClient creates a new registry client. +func NewRegistryClient(httpClient *http.Client) *RegistryClient { + if httpClient == nil { + httpClient = &http.Client{} + } + return &RegistryClient{httpClient: httpClient} +} + +// RegistryRequest is the POST body for the Transfer Factory Registry API. +type RegistryRequest struct { + ExpectedAdmin string `json:"expectedAdmin"` + Transfer RegistryTransferDetail `json:"transfer"` + ExtraArgs map[string]any `json:"extraArgs,omitempty"` +} + +// RegistryTransferDetail contains the transfer parameters for registry lookup. +type RegistryTransferDetail struct { + Sender string `json:"sender"` + Receiver string `json:"receiver"` + Amount string `json:"amount"` + InstrumentID string `json:"instrumentId"` + InputHoldingCIDs []string `json:"inputHoldingCids"` +} + +// RegistryResponse is the response from the Transfer Factory Registry API. +type RegistryResponse struct { + FactoryID string `json:"factoryId"` + TransferKind string `json:"transferKind"` + ChoiceContext json.RawMessage `json:"choiceContext"` + DisclosedContracts json.RawMessage `json:"disclosedContracts"` +} + +// registryDisclosedContract is the JSON shape of a disclosed contract from the registry. +type registryDisclosedContract struct { + ContractID string `json:"contractId"` + CreatedEventBlob string `json:"createdEventBlob"` // base64 + TemplateID string `json:"templateId"` + SynchronizerID string `json:"synchronizerId"` +} + +// GetTransferFactory calls the registry to discover the transfer factory for an external token. +func (rc *RegistryClient) GetTransferFactory(ctx context.Context, registryBaseURL string, req *RegistryRequest) (*RegistryResponse, error) { + body, err := json.Marshal(req) + if err != nil { + return nil, fmt.Errorf("marshal registry request: %w", err) + } + + url := strings.TrimRight(registryBaseURL, "/") + registryPath + httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) + if err != nil { + return nil, fmt.Errorf("create registry request: %w", err) + } + httpReq.Header.Set("Content-Type", "application/json") + + resp, err := rc.httpClient.Do(httpReq) + if err != nil { + return nil, fmt.Errorf("registry request failed: %w", err) + } + defer resp.Body.Close() + + const maxResponseBytes = 1 << 20 // 1 MB + respBody, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBytes)) + if err != nil { + return nil, fmt.Errorf("read registry response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("registry returned %d: %s", resp.StatusCode, string(respBody)) + } + + var result RegistryResponse + if err := json.Unmarshal(respBody, &result); err != nil { + return nil, fmt.Errorf("parse registry response: %w", err) + } + + return &result, nil +} + +// ConvertDisclosedContracts parses the registry's disclosed contracts JSON into proto messages. +func ConvertDisclosedContracts(raw json.RawMessage, fallbackDomainID string) ([]*lapiv2.DisclosedContract, error) { + if len(raw) == 0 || string(raw) == "null" { + return nil, nil + } + + var contracts []registryDisclosedContract + if err := json.Unmarshal(raw, &contracts); err != nil { + return nil, fmt.Errorf("parse disclosed contracts: %w", err) + } + + out := make([]*lapiv2.DisclosedContract, 0, len(contracts)) + for _, c := range contracts { + blob, err := base64.StdEncoding.DecodeString(c.CreatedEventBlob) + if err != nil { + return nil, fmt.Errorf("decode created_event_blob for %s: %w", c.ContractID, err) + } + + domainID := c.SynchronizerID + if domainID == "" { + domainID = fallbackDomainID + } + + out = append(out, &lapiv2.DisclosedContract{ + ContractId: c.ContractID, + CreatedEventBlob: blob, + SynchronizerId: domainID, + }) + } + return out, nil +} + +// ConvertChoiceContext parses the registry's choice context JSON into a map suitable for EncodeExtraArgs. +func ConvertChoiceContext(raw json.RawMessage) (map[string]string, error) { + if len(raw) == 0 || string(raw) == "null" { + return nil, nil + } + + var m map[string]string + if err := json.Unmarshal(raw, &m); err != nil { + return nil, fmt.Errorf("parse choice context: %w", err) + } + return m, nil +} diff --git a/pkg/cantonsdk/values/meta.go b/pkg/cantonsdk/values/meta.go index 2942c49d..3a4fcc04 100644 --- a/pkg/cantonsdk/values/meta.go +++ b/pkg/cantonsdk/values/meta.go @@ -104,30 +104,22 @@ func EncodeInstrumentId(admin, id string) *lapiv2.Value { } } -// EncodeExtraArgs creates a Splice ExtraArgs { context: ChoiceContext { values: {} }, meta: Metadata { values: {} } } value. -func EncodeExtraArgs() *lapiv2.Value { - emptyChoiceContext := &lapiv2.Value{ - Sum: &lapiv2.Value_Record{ - Record: &lapiv2.Record{ - Fields: []*lapiv2.RecordField{ - { - Label: "values", - Value: &lapiv2.Value{ - Sum: &lapiv2.Value_TextMap{ - TextMap: &lapiv2.TextMap{Entries: []*lapiv2.TextMap_Entry{}}, - }, - }, - }, - }, - }, - }, +// EncodeExtraArgs creates a Splice ExtraArgs { context: ChoiceContext { values: TextMap }, meta: Metadata { values: TextMap } } value. +// If choiceContext is nil or empty, the context field contains an empty TextMap (default behavior). +// If choiceContext is populated (e.g., from a Transfer Factory Registry response), its entries are encoded. +func EncodeExtraArgs(choiceContext map[string]string) *lapiv2.Value { + var contextValue *lapiv2.Value + if len(choiceContext) > 0 { + contextValue = EncodeMetadata(choiceContext) + } else { + contextValue = EmptyMetadata() } return &lapiv2.Value{ Sum: &lapiv2.Value_Record{ Record: &lapiv2.Record{ Fields: []*lapiv2.RecordField{ - {Label: "context", Value: emptyChoiceContext}, + {Label: "context", Value: contextValue}, {Label: "meta", Value: EmptyMetadata()}, }, }, diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index d13d60fb..e66f43b6 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -411,6 +411,7 @@ func setDefaultConfigEnv(t *testing.T) { t.Setenv("CANTON_SPLICE_TRANSFER_PACKAGE_ID", "splice-transfer-package-id") t.Setenv("CANTON_SPLICE_HOLDING_PACKAGE_ID", "splice-holding-package-id") t.Setenv("CANTON_BRIDGE_PACKAGE_ID", "bridge-package-id") + t.Setenv("CANTON_USDCX_INSTRUMENT_ADMIN", "Bridge-Operator::1220test") t.Setenv("ETHEREUM_RPC_URL", "https://eth.example") t.Setenv("ETHEREUM_WS_URL", "wss://eth.example/ws") t.Setenv("ETHEREUM_CHAIN_ID", "1") diff --git a/pkg/config/defaults/config.api-server.docker.yaml b/pkg/config/defaults/config.api-server.docker.yaml index c394a94b..2ae6035b 100644 --- a/pkg/config/defaults/config.api-server.docker.yaml +++ b/pkg/config/defaults/config.api-server.docker.yaml @@ -65,6 +65,11 @@ token: symbol: "DEMO" decimals: 18 instrument_id: "DEMO" + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48": + name: "USD Coin" + symbol: "USDCx" + decimals: 6 + instrument_id: "USDCx" native_balance_wei: "1000000000000000000000" # Ethereum JSON-RPC facade (MetaMask compatibility) diff --git a/pkg/config/defaults/config.api-server.local-devnet.yaml b/pkg/config/defaults/config.api-server.local-devnet.yaml index e1f3d9cb..5ba29195 100644 --- a/pkg/config/defaults/config.api-server.local-devnet.yaml +++ b/pkg/config/defaults/config.api-server.local-devnet.yaml @@ -59,6 +59,12 @@ token: symbol: "DEMO" decimals: 18 instrument_id: "DEMO" + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48": + name: "USD Coin" + symbol: "USDCx" + decimals: 6 + instrument_id: "USDCx" + instrument_admin: "" native_balance_wei: "1000000000000000000000" # Ethereum JSON-RPC facade (MetaMask compatibility) diff --git a/pkg/config/defaults/config.api-server.mainnet.yaml b/pkg/config/defaults/config.api-server.mainnet.yaml index 19acbb6c..fa939da9 100644 --- a/pkg/config/defaults/config.api-server.mainnet.yaml +++ b/pkg/config/defaults/config.api-server.mainnet.yaml @@ -52,6 +52,12 @@ token: symbol: "DEMO" decimals: 18 instrument_id: "DEMO" + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48": + name: "USD Coin" + symbol: "USDCx" + decimals: 6 + instrument_id: "USDCx" + instrument_admin: "${CANTON_USDCX_INSTRUMENT_ADMIN}" native_balance_wei: "1000000000000000000000" eth_rpc: diff --git a/pkg/config/tests/env-substitution.api.yaml b/pkg/config/tests/env-substitution.api.yaml index 34acfac0..ccc220fc 100644 --- a/pkg/config/tests/env-substitution.api.yaml +++ b/pkg/config/tests/env-substitution.api.yaml @@ -31,6 +31,11 @@ token: symbol: "PROMPT" decimals: 18 instrument_id: "PROMPT" + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48": + name: "USD Coin" + symbol: "USDCx" + decimals: 6 + instrument_id: "USDCx" eth_rpc: chain_id: 31337 diff --git a/pkg/config/tests/invalid-database-url.api.yaml b/pkg/config/tests/invalid-database-url.api.yaml index e522fa70..47f10408 100644 --- a/pkg/config/tests/invalid-database-url.api.yaml +++ b/pkg/config/tests/invalid-database-url.api.yaml @@ -31,6 +31,11 @@ token: symbol: "PROMPT" decimals: 18 instrument_id: "PROMPT" + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48": + name: "USD Coin" + symbol: "USDCx" + decimals: 6 + instrument_id: "USDCx" eth_rpc: chain_id: 31337 diff --git a/pkg/config/tests/minimal.api.yaml b/pkg/config/tests/minimal.api.yaml index 790c45e6..17c043a7 100644 --- a/pkg/config/tests/minimal.api.yaml +++ b/pkg/config/tests/minimal.api.yaml @@ -31,6 +31,11 @@ token: symbol: "PROMPT" decimals: 18 instrument_id: "PROMPT" + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48": + name: "USD Coin" + symbol: "USDCx" + decimals: 6 + instrument_id: "USDCx" eth_rpc: chain_id: 31337 diff --git a/pkg/config/tests/missing-env.api.yaml b/pkg/config/tests/missing-env.api.yaml index 77da5d8c..ee39fb6f 100644 --- a/pkg/config/tests/missing-env.api.yaml +++ b/pkg/config/tests/missing-env.api.yaml @@ -31,6 +31,11 @@ token: symbol: "PROMPT" decimals: 18 instrument_id: "PROMPT" + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48": + name: "USD Coin" + symbol: "USDCx" + decimals: 6 + instrument_id: "USDCx" eth_rpc: chain_id: 31337