From 04ba83fc4acab714b96cf4b58101845be316c372 Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Tue, 4 Aug 2026 14:38:25 +0100 Subject: [PATCH 01/19] Add Stellar data feeds changeset suite (cache and proxy) ChangeSetV2 suite under deployment/data-feeds/changeset/stellar: deploy, configure, feed admin, freeze, ownership, upgrade, and recover-tokens operations driving the generated bindings, with unit fakes and a live-network e2e (TestStellarDataFeedsE2E). --- .../data-feeds/changeset/stellar/README.md | 113 ++++ .../changeset/stellar/changeset_test.go | 617 ++++++++++++++++++ .../data-feeds/changeset/stellar/config.go | 10 + .../changeset/stellar/configure_cache.go | 126 ++++ .../changeset/stellar/deploy_cache.go | 92 +++ .../changeset/stellar/deploy_proxy.go | 108 +++ .../data-feeds/changeset/stellar/deps.go | 82 +++ .../data-feeds/changeset/stellar/doc.go | 4 + .../data-feeds/changeset/stellar/e2e_test.go | 487 ++++++++++++++ .../changeset/stellar/feed_admin.go | 81 +++ .../changeset/stellar/operation/operation.go | 254 +++++++ .../data-feeds/changeset/stellar/ownership.go | 123 ++++ .../changeset/stellar/recover_tokens.go | 60 ++ .../changeset/stellar/remove_feeds.go | 63 ++ .../changeset/stellar/report_encoding_test.go | 71 ++ .../changeset/stellar/set_proxy_cache.go | 66 ++ .../data-feeds/changeset/stellar/state.go | 45 ++ .../changeset/stellar/state_test.go | 61 ++ .../changeset/stellar/testdata/.gitignore | 5 + .../changeset/stellar/testdata/README.md | 32 + .../stellar/testdata/data_feeds_cache.wasm | Bin 0 -> 21500 bytes .../stellar/testdata/data_feeds_proxy.wasm | Bin 0 -> 9463 bytes .../changeset/stellar/testutil_test.go | 61 ++ .../changeset/stellar/upgrade_cache.go | 61 ++ .../changeset/stellar/validation.go | 66 ++ .../changeset/stellar/validation_test.go | 75 +++ deployment/go.mod | 33 +- deployment/go.sum | 8 +- 28 files changed, 2785 insertions(+), 19 deletions(-) create mode 100644 deployment/data-feeds/changeset/stellar/README.md create mode 100644 deployment/data-feeds/changeset/stellar/changeset_test.go create mode 100644 deployment/data-feeds/changeset/stellar/config.go create mode 100644 deployment/data-feeds/changeset/stellar/configure_cache.go create mode 100644 deployment/data-feeds/changeset/stellar/deploy_cache.go create mode 100644 deployment/data-feeds/changeset/stellar/deploy_proxy.go create mode 100644 deployment/data-feeds/changeset/stellar/deps.go create mode 100644 deployment/data-feeds/changeset/stellar/doc.go create mode 100644 deployment/data-feeds/changeset/stellar/e2e_test.go create mode 100644 deployment/data-feeds/changeset/stellar/feed_admin.go create mode 100644 deployment/data-feeds/changeset/stellar/operation/operation.go create mode 100644 deployment/data-feeds/changeset/stellar/ownership.go create mode 100644 deployment/data-feeds/changeset/stellar/recover_tokens.go create mode 100644 deployment/data-feeds/changeset/stellar/remove_feeds.go create mode 100644 deployment/data-feeds/changeset/stellar/report_encoding_test.go create mode 100644 deployment/data-feeds/changeset/stellar/set_proxy_cache.go create mode 100644 deployment/data-feeds/changeset/stellar/state.go create mode 100644 deployment/data-feeds/changeset/stellar/state_test.go create mode 100644 deployment/data-feeds/changeset/stellar/testdata/.gitignore create mode 100644 deployment/data-feeds/changeset/stellar/testdata/README.md create mode 100644 deployment/data-feeds/changeset/stellar/testdata/data_feeds_cache.wasm create mode 100644 deployment/data-feeds/changeset/stellar/testdata/data_feeds_proxy.wasm create mode 100644 deployment/data-feeds/changeset/stellar/testutil_test.go create mode 100644 deployment/data-feeds/changeset/stellar/upgrade_cache.go create mode 100644 deployment/data-feeds/changeset/stellar/validation.go create mode 100644 deployment/data-feeds/changeset/stellar/validation_test.go diff --git a/deployment/data-feeds/changeset/stellar/README.md b/deployment/data-feeds/changeset/stellar/README.md new file mode 100644 index 00000000000..a11683b7e74 --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/README.md @@ -0,0 +1,113 @@ +### Stellar Data Feeds Changesets + +CLDF `ChangeSetV2` suite for the Soroban `DataFeedsCache` and `DataFeedsProxy` +contracts. Style follows the Solana suite (`ChangeSetV2` + operations); +functional coverage mirrors the EVM Data Feeds suite (deploy / configure / +admin / remove / ownership / proxy — `jd_*`, `migrate_feeds`, `import_address` +are EVM/JD-specific and deliberately have no Stellar equivalent here). + +#### Package layout + +``` +data-feeds/changeset/stellar/ +├── deploy_cache.go, deploy_proxy.go # instantiate contracts, record AddressRefs +├── configure_cache.go # SetFeedConfigs — descriptions + permissions +├── remove_feeds.go, feed_admin.go # remove feed configs, grant/revoke feed-admin +├── ownership.go # transfer / accept / renounce (cache + proxy) +├── upgrade_cache.go, recover_tokens.go, set_proxy_cache.go +├── deps.go, state.go # shared resolve-ref/build-deps + client loaders +├── validation.go, config.go # input validation, encoding helpers +├── operation/operation.go # one CLDF operation per on-chain call +└── testdata/ # e2e WASM fixtures — see testdata/README.md +``` + +Each changeset's `Apply` resolves a `datastore.AddressRef` (chain selector + +`ContractType` + version + qualifier) to a `stellarApplyDeps` bundle — the +contract ID plus the `stellardeps.StellarDeps` (Deploy + Invoker) needed to +call it — then executes one or more `operation.*` calls from +`operation/operation.go` through `env.OperationsBundle`. Operations are thin: +each wraps exactly one generated-binding call +(`chainlink-stellar/bindings/contracts/data_feeds_{cache,proxy}`) or one +`stellardeps.Deploy` call. `deps.go`'s `verifyContractRef` / +`resolveContractDeps` hold the chain-exists + version-parse + ref-exists + +build-deps skeleton shared by every changeset in this package. + +#### Permissions model — no standalone "set forwarder" + +There is no `SetForwarder`-style changeset here. A feed's writer allowlist is +part of its `FeedConfig` and is set via `SetFeedConfigs` +(`configure_cache.go`): each `FeedPermission` carries an `AllowedSender` — +the CRE forwarder contract address — plus `AllowedWorkflowOwner` and +`AllowedWorkflowName`, checked on-chain by `on_report`. This mirrors EVM's +`WorkflowMetadata{AllowedSender, AllowedWorkflowOwner, AllowedWorkflowName}` +gate. To point feeds at a new forwarder, re-run `SetFeedConfigs` with updated +`Permissions` — there's no separate on-chain "forwarder" slot to update. + +#### Cross-contract version coupling + +`DeployProxy` and `SetProxyCache` both resolve their cache `AddressRef` using +the *proxy's own* `req.Version`, not a separate cache-version field. This +suite's convention is that cross-contract datastore lookups share the acting +changeset's `Version` — so a cache recorded under a different version needs a +matching-version record before a proxy at that version can find it. An +optional `CacheVersion` field on these requests (to let the two diverge) is a +recorded follow-up, not implemented here. + +#### Regenerating testdata WASM + bindings + +The e2e test (`e2e_test.go`) deploys real WASM to a local devnet; the fixtures +in `testdata/` and the generated Go bindings in +`chainlink-stellar/bindings/contracts/data_feeds_{cache,proxy}` are a +**matched pair** — a contract ABI change requires regenerating both together, +or the e2e test will fail to compile or decode. See `testdata/README.md` for +checksums and the exact regeneration steps; in short: + +```sh +# 1. rebuild WASM from the contract source repo +cd /contracts/data-feeds +stellar contract build # stellar CLI 27.0.0 +cp target/wasm32v1-none/release/data_feeds_{cache,proxy}.wasm \ + /deployment/data-feeds/changeset/stellar/testdata/ +# (older stellar CLIs output to target/wasm32-unknown-unknown/release/ instead) + +# 2. regenerate bindings from the new WASM, in chainlink-stellar +./scripts/gen_bindings.sh +``` + +#### Running the e2e test + +`TestStellarDataFeedsE2E` boots a local CTF Soroban devnet, deploys both +contracts, and asserts 35 exported function calls succeed plus 2 +(`recover_tokens` on each contract) fail with the documented `InvalidAction` +host error (self-transfer re-entry — see the test's `recover_tokens` +comments). It needs Docker and does not run in CI (`skipInCI` skips when +`CI=true`). + +```sh +go test ./data-feeds/changeset/stellar/ -run TestStellarDataFeedsE2E -v -timeout 25m +``` + +The default path boots `stellar/quickstart:future` via CLDF's +`NewCTFChainProvider`, hardcoded by the CTF framework to `linux/amd64` — on a +native arm64 host without amd64 emulation this fails (`exec format error`). +Override with `STELLAR_E2E_RPC_URL` / `STELLAR_E2E_FRIENDBOT_URL` to attach to +a quickstart container started natively instead: + +```sh +docker run -d -p 8000:8000 stellar/quickstart:latest \ + --local --enable-soroban-rpc --protocol-version 26 +STELLAR_E2E_RPC_URL=http://127.0.0.1:8000/rpc \ +STELLAR_E2E_FRIENDBOT_URL=http://127.0.0.1:8000/friendbot \ +go test ./data-feeds/changeset/stellar/ -run TestStellarDataFeedsE2E -v -timeout 25m +``` + +See the doc comment on `newE2EChain` for why protocol 26 (not quickstart's +default 25) is required. + +#### Out of scope + +- **Durable-pipeline registration** in `chainlink-deployments` — follow-up + work, coordinate with PLEX-2923. +- **MCMS** (multi-chain multisig) integration — PLEX-owned. +- **Forwarder / timelock deployment** — PLEX-owned; this package only wires + an already-deployed forwarder's address into `FeedPermission.AllowedSender`. diff --git a/deployment/data-feeds/changeset/stellar/changeset_test.go b/deployment/data-feeds/changeset/stellar/changeset_test.go new file mode 100644 index 00000000000..d5976d410bf --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/changeset_test.go @@ -0,0 +1,617 @@ +package stellar + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/Masterminds/semver/v3" + chainsel "github.com/smartcontractkit/chain-selectors" + "github.com/stellar/go-stellar-sdk/keypair" + "github.com/stellar/go-stellar-sdk/xdr" + "github.com/stretchr/testify/require" + + cldfchain "github.com/smartcontractkit/chainlink-deployments-framework/chain" + cldfstellar "github.com/smartcontractkit/chainlink-deployments-framework/chain/stellar" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink-deployments-framework/offchain/ocr" + cldflogger "github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger" + "github.com/smartcontractkit/chainlink-stellar/bindings/scval" + stellardeploy "github.com/smartcontractkit/chainlink-stellar/deployment" + + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/stellar/operation" +) + +const testContractID = "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA" + +// testChainSel is a real Stellar chain selector (from chain-selectors) so +// that ChainMetadata methods (Family, NetworkType, ...) resolve correctly. +var testChainSel = chainsel.STELLAR_TESTNET.Selector + +// newTestEnv returns an Environment with one fake Stellar chain and swapped-in fake deps. +// It swaps the package-level newStellarDeps seam to hand back fakeInvoker/fakeDeployer +// instead of talking to a real Soroban RPC endpoint, and restores the seam on cleanup. +func newTestEnv(t *testing.T) (cldf.Environment, *fakeInvoker, *fakeDeployer) { + t.Helper() + + kp, err := keypair.Random() + require.NoError(t, err) + + chain := cldfstellar.Chain{ + ChainMetadata: cldfstellar.ChainMetadata{Selector: testChainSel}, + Signer: cldfstellar.NewStellarKeypairSigner(kp), + } + + invoker := &fakeInvoker{} + deployer := &fakeDeployer{contractID: testContractID} + + origDeps := newStellarDeps + t.Cleanup(func() { newStellarDeps = origDeps }) + newStellarDeps = func(_ cldfstellar.Chain) (operation.StellarDeps, error) { + return operation.StellarDeps{Deploy: deployer, Invoker: invoker}, nil + } + + blockChains := cldfchain.NewBlockChains(map[uint64]cldfchain.BlockChain{ + testChainSel: chain, + }) + + env := cldf.NewEnvironment( + "test", + cldflogger.Test(t), + nil, // ExistingAddresses: unused by these changesets, DataStore is authoritative + datastore.NewMemoryDataStore().Seal(), + nil, + nil, + func() context.Context { return context.Background() }, + ocr.OCRSecrets{}, + blockChains, + ) + + return *env, invoker, deployer +} + +// contractRefSpec describes one AddressRef to seed via seedContractRefs. +type contractRefSpec struct { + contractType datastore.ContractType + address string + qualifier string + version string +} + +// seedContractRefs pre-seeds env.DataStore with the given AddressRefs (all in +// one MemoryDataStore) so changesets can resolve contracts by +// (ChainSel, Type, Version, Qualifier). Replaces any refs seeded by a prior +// call on the same env — call once per test with every ref the test needs +// (e.g. both a proxy and a cache ref for SetProxyCache). +func seedContractRefs(t *testing.T, env *cldf.Environment, refs ...contractRefSpec) { + t.Helper() + ds := datastore.NewMemoryDataStore() + for _, r := range refs { + require.NoError(t, ds.Addresses().Add(datastore.AddressRef{ + Address: r.address, + ChainSelector: testChainSel, + Type: r.contractType, + Version: semver.MustParse(r.version), + Qualifier: r.qualifier, + })) + } + env.DataStore = ds.Seal() +} + +// seedCacheRef pre-seeds env.DataStore with a single cache AddressRef so +// config changesets (SetFeedConfigs, RemoveFeedConfigs, Add/RemoveFeedAdmin) +// can resolve the cache contract by (ChainSel, CacheContract, Version, Qualifier). +func seedCacheRef(t *testing.T, env *cldf.Environment, address, qualifier, version string) { + t.Helper() + seedContractRefs(t, env, contractRefSpec{CacheContract, address, qualifier, version}) +} + +// seedProxyRef pre-seeds env.DataStore with a single proxy AddressRef so +// changesets can resolve the proxy contract by +// (ChainSel, ProxyContract, Version, Qualifier). +func seedProxyRef(t *testing.T, env *cldf.Environment, address, qualifier, version string) { + t.Helper() + seedContractRefs(t, env, contractRefSpec{ProxyContract, address, qualifier, version}) +} + +// writeDummyWasm writes a placeholder wasm file under t.TempDir() so +// VerifyPreconditions' filesystem check (os.Stat) has a real path to find. +// The contents are never read — deployment is faked via fakeDeployer. +func writeDummyWasm(t *testing.T, name string) string { + t.Helper() + path := filepath.Join(t.TempDir(), name) + require.NoError(t, os.WriteFile(path, []byte("dummy wasm"), 0o600)) + return path +} + +func TestDeployCacheChangeset(t *testing.T) { + env, _, dep := newTestEnv(t) + + req := &DeployCacheRequest{ + ChainSel: testChainSel, + WasmPath: writeDummyWasm(t, "data_feeds_cache.wasm"), + Qualifier: "test-cache", + Version: "1.0.0", + } + + require.NoError(t, DeployCache{}.VerifyPreconditions(env, req)) + + out, err := DeployCache{}.Apply(env, req) + require.NoError(t, err) + require.NotNil(t, out.DataStore) + + require.Len(t, dep.deploys, 1) + require.Len(t, dep.deploys[0].Args, 1) // owner ctor arg + + owner := env.BlockChains.StellarChains()[testChainSel].Signer.Address() + wantSalt := stellardeploy.GenerateDeterministicSalt(owner, "data_feeds_cache-"+req.Qualifier) + require.Equal(t, wantSalt, dep.deploys[0].Salt) + + key := datastore.NewAddressRefKey(testChainSel, CacheContract, semver.MustParse("1.0.0"), "test-cache") + ref, err := out.DataStore.Addresses().Get(key) + require.NoError(t, err) + require.Equal(t, testContractID, ref.Address) + + // invalid explicit Owner must fail preconditions + badOwner := *req + badOwner.Owner = "not-a-key" + require.Error(t, DeployCache{}.VerifyPreconditions(env, &badOwner)) +} + +func TestDeployCacheChangeset_MissingChain(t *testing.T) { + env, _, _ := newTestEnv(t) + + req := &DeployCacheRequest{ + ChainSel: 999999, + WasmPath: writeDummyWasm(t, "data_feeds_cache.wasm"), + Qualifier: "test-cache", + Version: "1.0.0", + } + + require.Error(t, DeployCache{}.VerifyPreconditions(env, req)) +} + +// TestDeployCacheChangeset_ExplicitOwner pins that an explicit req.Owner +// seeds the deploy salt directly. deploy_cache.go's Apply only falls back to +// ch.Signer.Address() when req.Owner is empty; when it's set, the salt is +// GenerateDeterministicSalt(req.Owner, ...) -- not the chain signer's address. +func TestDeployCacheChangeset_ExplicitOwner(t *testing.T) { + env, _, dep := newTestEnv(t) + + req := &DeployCacheRequest{ + ChainSel: testChainSel, + WasmPath: writeDummyWasm(t, "data_feeds_cache.wasm"), + Owner: testAdmin, + Qualifier: "test-cache-owner", + Version: "1.0.0", + } + + require.NoError(t, DeployCache{}.VerifyPreconditions(env, req)) + + out, err := DeployCache{}.Apply(env, req) + require.NoError(t, err) + require.NotNil(t, out.DataStore) + + require.Len(t, dep.deploys, 1) + require.Len(t, dep.deploys[0].Args, 1) // owner ctor arg + + wantSalt := stellardeploy.GenerateDeterministicSalt(testAdmin, "data_feeds_cache-"+req.Qualifier) + require.Equal(t, wantSalt, dep.deploys[0].Salt) +} + +func TestDeployProxyChangeset(t *testing.T) { + env, _, dep := newTestEnv(t) + + // Seed the datastore with a cache AddressRef so DeployProxy can resolve it. + seedDS := datastore.NewMemoryDataStore() + require.NoError(t, seedDS.Addresses().Add(datastore.AddressRef{ + Address: "CCACHEFAKE0000000000000000000000000000000000000000000000", + ChainSelector: testChainSel, + Type: CacheContract, + Version: semver.MustParse("1.0.0"), + Qualifier: "test-cache", + })) + env.DataStore = seedDS.Seal() + + req := &DeployProxyRequest{ + ChainSel: testChainSel, + WasmPath: writeDummyWasm(t, "data_feeds_proxy.wasm"), + CacheQualifier: "test-cache", + Qualifier: "test-proxy", + Version: "1.0.0", + } + + require.NoError(t, DeployProxy{}.VerifyPreconditions(env, req)) + + out, err := DeployProxy{}.Apply(env, req) + require.NoError(t, err) + require.NotNil(t, out.DataStore) + + require.Len(t, dep.deploys, 1) + require.Len(t, dep.deploys[0].Args, 2) // owner + cache ctor args + + owner := env.BlockChains.StellarChains()[testChainSel].Signer.Address() + wantSalt := stellardeploy.GenerateDeterministicSalt(owner, "data_feeds_proxy-"+req.Qualifier) + require.Equal(t, wantSalt, dep.deploys[0].Salt) + + key := datastore.NewAddressRefKey(testChainSel, ProxyContract, semver.MustParse("1.0.0"), "test-proxy") + ref, err := out.DataStore.Addresses().Get(key) + require.NoError(t, err) + require.Equal(t, testContractID, ref.Address) +} + +func TestDeployProxyChangeset_MissingCache(t *testing.T) { + env, _, _ := newTestEnv(t) + + req := &DeployProxyRequest{ + ChainSel: testChainSel, + WasmPath: writeDummyWasm(t, "data_feeds_proxy.wasm"), + CacheQualifier: "does-not-exist", + Qualifier: "test-proxy", + Version: "1.0.0", + } + + require.Error(t, DeployProxy{}.VerifyPreconditions(env, req)) +} + +const testAdmin = "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN7" + +func TestSetFeedConfigsChangeset(t *testing.T) { + env, inv, _ := newTestEnv(t) + seedCacheRef(t, &env, testContractID, "test", "1.0.0") + + req := &SetFeedConfigsRequest{ + ChainSel: testChainSel, + Qualifier: "test", + Version: "1.0.0", + Admin: testAdmin, + DataIDs: []string{"0x018e16c39e00032000000"}, + Descriptions: []string{"BTC/USD"}, + Permissions: []FeedPermission{{ + AllowedSender: testContractID, // the forwarder + AllowedWorkflowOwner: "0x0102030405060708090a0b0c0d0e0f1011121314", + AllowedWorkflowName: "abc", + }}, + } + require.NoError(t, SetFeedConfigs{}.VerifyPreconditions(env, req)) + + _, err := SetFeedConfigs{}.Apply(env, req) + require.NoError(t, err) + require.Len(t, inv.calls, 1) + require.Equal(t, "set_feed_configs", inv.calls[0].Function) + require.Equal(t, testContractID, inv.calls[0].ContractID) + + // Permissions apply to every feed in the batch: a single permission set + // with two DataIDs must still pass length validation. + multi := *req + multi.DataIDs = []string{"0x01", "0x02"} + multi.Descriptions = []string{"BTC/USD", "ETH/USD"} + require.NoError(t, SetFeedConfigs{}.VerifyPreconditions(env, &multi)) + + // mismatched lengths must fail preconditions + bad := *req + bad.Descriptions = nil + require.Error(t, SetFeedConfigs{}.VerifyPreconditions(env, &bad)) + + // empty DataIDs must fail preconditions + empty := *req + empty.DataIDs = nil + empty.Descriptions = nil + require.Error(t, SetFeedConfigs{}.VerifyPreconditions(env, &empty)) + + // invalid admin address must fail preconditions + badAdmin := *req + badAdmin.Admin = "not-a-key" + require.Error(t, SetFeedConfigs{}.VerifyPreconditions(env, &badAdmin)) + + // missing cache ref must fail preconditions + badQualifier := *req + badQualifier.Qualifier = "does-not-exist" + require.Error(t, SetFeedConfigs{}.VerifyPreconditions(env, &badQualifier)) + + // invalid permission fields must fail preconditions + badPerm := *req + badPerm.Permissions = []FeedPermission{{ + AllowedSender: "not-a-key", + AllowedWorkflowOwner: "0x0102030405060708090a0b0c0d0e0f1011121314", + AllowedWorkflowName: "abc", + }} + require.Error(t, SetFeedConfigs{}.VerifyPreconditions(env, &badPerm)) + + // empty Permissions must fail preconditions + emptyPerm := *req + emptyPerm.Permissions = nil + require.Error(t, SetFeedConfigs{}.VerifyPreconditions(env, &emptyPerm)) +} + +func TestRemoveFeedConfigsChangeset(t *testing.T) { + env, inv, _ := newTestEnv(t) + seedCacheRef(t, &env, testContractID, "test", "1.0.0") + + req := &RemoveFeedConfigsRequest{ + ChainSel: testChainSel, + Qualifier: "test", + Version: "1.0.0", + Admin: testAdmin, + DataIDs: []string{"0x018e16c39e00032000000"}, + } + require.NoError(t, RemoveFeedConfigs{}.VerifyPreconditions(env, req)) + + _, err := RemoveFeedConfigs{}.Apply(env, req) + require.NoError(t, err) + require.Len(t, inv.calls, 1) + require.Equal(t, "remove_feed_configs", inv.calls[0].Function) + require.Equal(t, testContractID, inv.calls[0].ContractID) + + // empty DataIDs must fail preconditions + empty := *req + empty.DataIDs = nil + require.Error(t, RemoveFeedConfigs{}.VerifyPreconditions(env, &empty)) + + // invalid admin address must fail preconditions + badAdmin := *req + badAdmin.Admin = "not-a-key" + require.Error(t, RemoveFeedConfigs{}.VerifyPreconditions(env, &badAdmin)) + + // missing cache ref must fail preconditions + badQualifier := *req + badQualifier.Qualifier = "does-not-exist" + require.Error(t, RemoveFeedConfigs{}.VerifyPreconditions(env, &badQualifier)) +} + +func TestFeedAdminChangesets(t *testing.T) { + env, inv, _ := newTestEnv(t) + seedCacheRef(t, &env, testContractID, "test", "1.0.0") + + req := &FeedAdminRequest{ + ChainSel: testChainSel, + Qualifier: "test", + Version: "1.0.0", + Admin: testAdmin, + } + + require.NoError(t, AddFeedAdmin{}.VerifyPreconditions(env, req)) + _, err := AddFeedAdmin{}.Apply(env, req) + require.NoError(t, err) + require.Len(t, inv.calls, 1) + require.Equal(t, "add_feed_admin", inv.calls[0].Function) + require.Equal(t, testContractID, inv.calls[0].ContractID) + + require.NoError(t, RemoveFeedAdmin{}.VerifyPreconditions(env, req)) + _, err = RemoveFeedAdmin{}.Apply(env, req) + require.NoError(t, err) + require.Len(t, inv.calls, 2) + require.Equal(t, "remove_feed_admin", inv.calls[1].Function) + require.Equal(t, testContractID, inv.calls[1].ContractID) + + // invalid admin address must fail preconditions for both changesets + badAdmin := *req + badAdmin.Admin = "not-a-key" + require.Error(t, AddFeedAdmin{}.VerifyPreconditions(env, &badAdmin)) + require.Error(t, RemoveFeedAdmin{}.VerifyPreconditions(env, &badAdmin)) + + // missing cache ref must fail preconditions for both changesets + badQualifier := *req + badQualifier.Qualifier = "does-not-exist" + require.Error(t, AddFeedAdmin{}.VerifyPreconditions(env, &badQualifier)) + require.Error(t, RemoveFeedAdmin{}.VerifyPreconditions(env, &badQualifier)) +} + +// testProxyAddress is a second, distinct contract address used wherever a +// test needs to tell the proxy and cache refs apart (e.g. asserting Apply +// invoked the proxy, not the cache). +const testProxyAddress = "CBPROXY00000000000000000000000000000000000000000000000AA" + +func TestOwnershipChangesets(t *testing.T) { + env, inv, _ := newTestEnv(t) + seedContractRefs(t, &env, + contractRefSpec{CacheContract, testContractID, "test-cache", "1.0.0"}, + contractRefSpec{ProxyContract, testProxyAddress, "test-proxy", "1.0.0"}, + ) + + cacheReq := &OwnershipRequest{ + ChainSel: testChainSel, + Qualifier: "test-cache", + Version: "1.0.0", + Contract: CacheContract, + NewOwner: testAdmin, + LiveUntilLedger: 100, + } + + require.NoError(t, TransferOwnership{}.VerifyPreconditions(env, cacheReq)) + _, err := TransferOwnership{}.Apply(env, cacheReq) + require.NoError(t, err) + require.Len(t, inv.calls, 1) + require.Equal(t, "transfer_ownership", inv.calls[0].Function) + require.Equal(t, testContractID, inv.calls[0].ContractID) + require.Len(t, inv.calls[0].Args, 2) + + require.NoError(t, AcceptOwnership{}.VerifyPreconditions(env, cacheReq)) + _, err = AcceptOwnership{}.Apply(env, cacheReq) + require.NoError(t, err) + require.Len(t, inv.calls, 2) + require.Equal(t, "accept_ownership", inv.calls[1].Function) + require.Equal(t, testContractID, inv.calls[1].ContractID) + require.Empty(t, inv.calls[1].Args) + + require.NoError(t, RenounceOwnership{}.VerifyPreconditions(env, cacheReq)) + _, err = RenounceOwnership{}.Apply(env, cacheReq) + require.NoError(t, err) + require.Len(t, inv.calls, 3) + require.Equal(t, "renounce_ownership", inv.calls[2].Function) + require.Equal(t, testContractID, inv.calls[2].ContractID) + + // same three ops against the proxy: IsProxy selects the proxy client and + // the resolved contractID is the proxy's address, not the cache's. + proxyReq := *cacheReq + proxyReq.Qualifier = "test-proxy" + proxyReq.Contract = ProxyContract + + require.NoError(t, TransferOwnership{}.VerifyPreconditions(env, &proxyReq)) + _, err = TransferOwnership{}.Apply(env, &proxyReq) + require.NoError(t, err) + require.Equal(t, "transfer_ownership", inv.calls[3].Function) + require.Equal(t, testProxyAddress, inv.calls[3].ContractID) + + require.NoError(t, AcceptOwnership{}.VerifyPreconditions(env, &proxyReq)) + _, err = AcceptOwnership{}.Apply(env, &proxyReq) + require.NoError(t, err) + require.Equal(t, "accept_ownership", inv.calls[4].Function) + require.Equal(t, testProxyAddress, inv.calls[4].ContractID) + + require.NoError(t, RenounceOwnership{}.VerifyPreconditions(env, &proxyReq)) + _, err = RenounceOwnership{}.Apply(env, &proxyReq) + require.NoError(t, err) + require.Equal(t, "renounce_ownership", inv.calls[5].Function) + require.Equal(t, testProxyAddress, inv.calls[5].ContractID) + + // unknown Contract type must fail preconditions for all three changesets + badContract := *cacheReq + badContract.Contract = datastore.ContractType("SomethingElse") + require.Error(t, TransferOwnership{}.VerifyPreconditions(env, &badContract)) + require.Error(t, AcceptOwnership{}.VerifyPreconditions(env, &badContract)) + require.Error(t, RenounceOwnership{}.VerifyPreconditions(env, &badContract)) + + // TransferOwnership requires a valid NewOwner and nonzero LiveUntilLedger; + // Accept/Renounce don't touch those fields so the same request stays valid. + missingOwner := *cacheReq + missingOwner.NewOwner = "" + require.Error(t, TransferOwnership{}.VerifyPreconditions(env, &missingOwner)) + require.NoError(t, AcceptOwnership{}.VerifyPreconditions(env, &missingOwner)) + + badOwner := *cacheReq + badOwner.NewOwner = "not-a-key" + require.Error(t, TransferOwnership{}.VerifyPreconditions(env, &badOwner)) + + zeroLedger := *cacheReq + zeroLedger.LiveUntilLedger = 0 + require.Error(t, TransferOwnership{}.VerifyPreconditions(env, &zeroLedger)) + + // missing address ref must fail preconditions + badQualifier := *cacheReq + badQualifier.Qualifier = "does-not-exist" + require.Error(t, TransferOwnership{}.VerifyPreconditions(env, &badQualifier)) +} + +func TestUpgradeCacheChangeset(t *testing.T) { + env, inv, dep := newTestEnv(t) + seedCacheRef(t, &env, testContractID, "test", "1.0.0") + + var wantHash xdr.Hash + for i := range wantHash { + wantHash[i] = byte(i + 1) + } + dep.wasmHash = wantHash + + req := &UpgradeCacheRequest{ + ChainSel: testChainSel, + Qualifier: "test", + Version: "1.0.0", + WasmPath: writeDummyWasm(t, "new_cache.wasm"), + } + require.NoError(t, UpgradeCache{}.VerifyPreconditions(env, req)) + + _, err := UpgradeCache{}.Apply(env, req) + require.NoError(t, err) + + // the upload happened, against the requested wasm path + require.Len(t, dep.uploads, 1) + require.Equal(t, req.WasmPath, dep.uploads[0]) + + // the upgrade invocation carried the fake's uploaded hash through + require.Len(t, inv.calls, 1) + require.Equal(t, "upgrade", inv.calls[0].Function) + require.Equal(t, testContractID, inv.calls[0].ContractID) + require.Len(t, inv.calls[0].Args, 1) + + gotHash, err := scval.Bytes32FromScVal(inv.calls[0].Args[0]) + require.NoError(t, err) + require.Equal(t, [32]byte(wantHash), gotHash) + + // missing wasm path must fail preconditions + badPath := *req + badPath.WasmPath = "/does/not/exist.wasm" + require.Error(t, UpgradeCache{}.VerifyPreconditions(env, &badPath)) + + // missing cache ref must fail preconditions + badQualifier := *req + badQualifier.Qualifier = "does-not-exist" + require.Error(t, UpgradeCache{}.VerifyPreconditions(env, &badQualifier)) +} + +func TestRecoverTokensChangeset(t *testing.T) { + env, inv, _ := newTestEnv(t) + seedCacheRef(t, &env, testContractID, "test", "1.0.0") + + req := &RecoverTokensRequest{ + ChainSel: testChainSel, + Qualifier: "test", + Version: "1.0.0", + Token: testContractID, + To: testAdmin, + Amount: 1000, + } + require.NoError(t, RecoverTokens{}.VerifyPreconditions(env, req)) + + _, err := RecoverTokens{}.Apply(env, req) + require.NoError(t, err) + require.Len(t, inv.calls, 1) + require.Equal(t, "recover_tokens", inv.calls[0].Function) + require.Equal(t, testContractID, inv.calls[0].ContractID) + require.Len(t, inv.calls[0].Args, 3) + + // bad token address must fail preconditions + badToken := *req + badToken.Token = "not-a-key" + require.Error(t, RecoverTokens{}.VerifyPreconditions(env, &badToken)) + + // bad recipient address must fail preconditions + badTo := *req + badTo.To = "not-a-key" + require.Error(t, RecoverTokens{}.VerifyPreconditions(env, &badTo)) + + // non-positive amount must fail preconditions + badAmount := *req + badAmount.Amount = 0 + require.Error(t, RecoverTokens{}.VerifyPreconditions(env, &badAmount)) + + // missing cache ref must fail preconditions + badQualifier := *req + badQualifier.Qualifier = "does-not-exist" + require.Error(t, RecoverTokens{}.VerifyPreconditions(env, &badQualifier)) +} + +func TestSetProxyCacheChangeset(t *testing.T) { + env, inv, _ := newTestEnv(t) + seedContractRefs(t, &env, + contractRefSpec{ProxyContract, testProxyAddress, "test-proxy", "1.0.0"}, + contractRefSpec{CacheContract, testContractID, "test-cache", "1.0.0"}, + ) + + req := &SetProxyCacheRequest{ + ChainSel: testChainSel, + Qualifier: "test-proxy", + Version: "1.0.0", + CacheQualifier: "test-cache", + } + require.NoError(t, SetProxyCache{}.VerifyPreconditions(env, req)) + + _, err := SetProxyCache{}.Apply(env, req) + require.NoError(t, err) + require.Len(t, inv.calls, 1) + require.Equal(t, "set_cache", inv.calls[0].Function) + require.Equal(t, testProxyAddress, inv.calls[0].ContractID) + require.Len(t, inv.calls[0].Args, 1) + + // missing cache ref must fail preconditions (proxy ref alone isn't enough) + proxyOnly, _, _ := newTestEnv(t) + seedProxyRef(t, &proxyOnly, testProxyAddress, "test-proxy", "1.0.0") + require.Error(t, SetProxyCache{}.VerifyPreconditions(proxyOnly, req)) + + // missing proxy ref must fail preconditions (cache ref alone isn't enough) + cacheOnly, _, _ := newTestEnv(t) + seedCacheRef(t, &cacheOnly, testContractID, "test-cache", "1.0.0") + require.Error(t, SetProxyCache{}.VerifyPreconditions(cacheOnly, req)) +} diff --git a/deployment/data-feeds/changeset/stellar/config.go b/deployment/data-feeds/changeset/stellar/config.go new file mode 100644 index 00000000000..a0d85714c7c --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/config.go @@ -0,0 +1,10 @@ +package stellar + +import "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + +const ( + // CacheContract is the datastore contract type for the DataFeedsCache Soroban contract. + CacheContract datastore.ContractType = "DataFeedsCache" + // ProxyContract is the datastore contract type for the DataFeedsProxy Soroban contract. + ProxyContract datastore.ContractType = "DataFeedsProxy" +) diff --git a/deployment/data-feeds/changeset/stellar/configure_cache.go b/deployment/data-feeds/changeset/stellar/configure_cache.go new file mode 100644 index 00000000000..2fb4e45421c --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/configure_cache.go @@ -0,0 +1,126 @@ +package stellar + +import ( + "errors" + "fmt" + + "github.com/Masterminds/semver/v3" + + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" + cache "github.com/smartcontractkit/chainlink-stellar/bindings/contracts/data_feeds_cache" + + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/stellar/operation" +) + +// FeedPermission is the operator-facing shape of a workflow write-permission. +// AllowedSender is the on-chain caller permitted to invoke on_report — the CRE +// forwarder contract. Mirrors EVM WorkflowMetadata{AllowedSender, ...}. +type FeedPermission struct { + AllowedSender string // Stellar address (C... or G...) + AllowedWorkflowOwner string // 20-byte hex + AllowedWorkflowName string // <= 10 ASCII chars +} + +// SetFeedConfigsRequest sets descriptions + workflow permissions for a batch of +// feeds. Permissions apply to every feed in the batch (EVM-aligned semantics). +type SetFeedConfigsRequest struct { + ChainSel uint64 + Qualifier string + Version string + Admin string + DataIDs []string + Descriptions []string + Permissions []FeedPermission +} + +var _ cldf.ChangeSetV2[*SetFeedConfigsRequest] = SetFeedConfigs{} + +// SetFeedConfigs sets per-feed descriptions and shared workflow write-permissions +// on an already-deployed DataFeedsCache contract. +type SetFeedConfigs struct{} + +func (SetFeedConfigs) VerifyPreconditions(env cldf.Environment, req *SetFeedConfigsRequest) error { + if err := verifyContractRef(env, req.ChainSel, CacheContract, req.Qualifier, req.Version); err != nil { + return err + } + if err := validateAddress(req.Admin); err != nil { + return err + } + if len(req.DataIDs) == 0 { + return errors.New("DataIDs cannot be empty") + } + if len(req.DataIDs) != len(req.Descriptions) { + return errors.New("DataIDs and Descriptions must have the same length") + } + if len(req.Permissions) == 0 { + return errors.New("Permissions cannot be empty") + } + if _, err := dataIDsToBytes(req.DataIDs); err != nil { + return err + } + if _, err := req.permissions(); err != nil { + return err + } + return nil +} + +// permissions converts the operator shape to the generated binding shape. +func (req *SetFeedConfigsRequest) permissions() ([]cache.WorkflowPermission, error) { + out := make([]cache.WorkflowPermission, 0, len(req.Permissions)) + for _, p := range req.Permissions { + if err := validateAddress(p.AllowedSender); err != nil { + return nil, fmt.Errorf("allowed sender: %w", err) + } + owner, err := workflowOwnerToBytes(p.AllowedWorkflowOwner) + if err != nil { + return nil, err + } + name, err := workflowNameToBytes(p.AllowedWorkflowName) + if err != nil { + return nil, err + } + out = append(out, cache.WorkflowPermission{ + AllowedSender: p.AllowedSender, + AllowedWorkflowOwner: owner, + AllowedWorkflowName: name, + }) + } + return out, nil +} + +func (SetFeedConfigs) Apply(env cldf.Environment, req *SetFeedConfigsRequest) (cldf.ChangesetOutput, error) { + var out cldf.ChangesetOutput + version := semver.MustParse(req.Version) + d, _, err := resolveContractDeps(env, req.ChainSel, CacheContract, req.Qualifier, version) + if err != nil { + return out, err + } + + ids, err := dataIDsToBytes(req.DataIDs) + if err != nil { + return out, err + } + perms, err := req.permissions() + if err != nil { + return out, err + } + + entries := make([]cache.FeedConfigEntry, len(ids)) + for i, id := range ids { + entries[i] = cache.FeedConfigEntry{ + DataId: id, + Config: cache.FeedConfig{ + Description: req.Descriptions[i], + WorkflowPermissions: perms, + }, + } + } + + _, err = operations.ExecuteOperation(env.OperationsBundle, operation.SetFeedConfigs, d.deps, operation.SetFeedConfigsInput{ + ContractID: d.contractID, + Admin: req.Admin, + Entries: entries, + }) + return out, err +} diff --git a/deployment/data-feeds/changeset/stellar/deploy_cache.go b/deployment/data-feeds/changeset/stellar/deploy_cache.go new file mode 100644 index 00000000000..b3456d02e70 --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/deploy_cache.go @@ -0,0 +1,92 @@ +package stellar + +import ( + "fmt" + "os" + + "github.com/Masterminds/semver/v3" + + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" + stellardeploy "github.com/smartcontractkit/chainlink-stellar/deployment" + + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/stellar/operation" +) + +// DeployCacheRequest configures a DataFeedsCache deployment. The real cache +// contract's __constructor(owner) takes a single argument — there is no +// RetentionTTLLedgers constructor input (that's a hardcoded on-chain constant). +type DeployCacheRequest struct { + ChainSel uint64 + WasmPath string + Owner string // defaults to the chain's deployer address when empty + Qualifier string + Version string + LabelSet datastore.LabelSet +} + +var _ cldf.ChangeSetV2[*DeployCacheRequest] = DeployCache{} + +// DeployCache uploads and instantiates the DataFeedsCache contract and records +// its address in the datastore under CacheContract. +type DeployCache struct{} + +func (DeployCache) VerifyPreconditions(env cldf.Environment, req *DeployCacheRequest) error { + if _, ok := env.BlockChains.StellarChains()[req.ChainSel]; !ok { + return fmt.Errorf("stellar chain not found for chain selector %d", req.ChainSel) + } + if _, err := semver.NewVersion(req.Version); err != nil { + return fmt.Errorf("invalid version %q: %w", req.Version, err) + } + if _, err := os.Stat(req.WasmPath); err != nil { + return fmt.Errorf("wasm path: %w", err) + } + if req.Owner != "" { + if err := validateAddress(req.Owner); err != nil { + return err + } + } + return nil +} + +func (DeployCache) Apply(env cldf.Environment, req *DeployCacheRequest) (cldf.ChangesetOutput, error) { + var out cldf.ChangesetOutput + ch, ok := env.BlockChains.StellarChains()[req.ChainSel] + if !ok { + return out, fmt.Errorf("stellar chain not found for chain selector %d", req.ChainSel) + } + + deps, err := newStellarDeps(ch) + if err != nil { + return out, err + } + + owner := req.Owner + if owner == "" { + if ch.Signer == nil { + return out, fmt.Errorf("owner not set and chain has no signer") + } + owner = ch.Signer.Address() + } + salt := stellardeploy.GenerateDeterministicSalt(owner, "data_feeds_cache-"+req.Qualifier) + + report, err := operations.ExecuteOperation(env.OperationsBundle, operation.DeployCache, deps, operation.DeployCacheInput{ + WasmPath: req.WasmPath, + Salt: salt, + Owner: owner, + }) + if err != nil { + return out, err + } + + out.DataStore = datastore.NewMemoryDataStore() + return out, out.DataStore.Addresses().Add(datastore.AddressRef{ + Address: report.Output.ContractID, + ChainSelector: req.ChainSel, + Type: CacheContract, + Version: semver.MustParse(req.Version), + Qualifier: req.Qualifier, + Labels: req.LabelSet, + }) +} diff --git a/deployment/data-feeds/changeset/stellar/deploy_proxy.go b/deployment/data-feeds/changeset/stellar/deploy_proxy.go new file mode 100644 index 00000000000..dbae47623ac --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/deploy_proxy.go @@ -0,0 +1,108 @@ +package stellar + +import ( + "fmt" + "os" + + "github.com/Masterminds/semver/v3" + + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" + stellardeploy "github.com/smartcontractkit/chainlink-stellar/deployment" + + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/stellar/operation" +) + +// DeployProxyRequest configures a DataFeedsProxy deployment. The proxy's +// __constructor(owner, cache) resolves the cache address from the datastore +// by (ChainSel, CacheContract, Version, CacheQualifier) — the cache must +// already be deployed and recorded. The cache lookup reuses this request's +// own Version (there is no separate CacheVersion field): per this suite's +// convention, cross-contract lookups share the acting changeset's datastore +// Version, so a cache recorded under a different version needs a +// matching-version record (an optional CacheVersion field is a recorded +// follow-up if operators need split versions). +type DeployProxyRequest struct { + ChainSel uint64 + WasmPath string + Owner string // defaults to the chain's deployer address when empty + CacheQualifier string + Qualifier string + Version string + LabelSet datastore.LabelSet +} + +var _ cldf.ChangeSetV2[*DeployProxyRequest] = DeployProxy{} + +// DeployProxy uploads and instantiates the DataFeedsProxy contract and records +// its address in the datastore under ProxyContract. +type DeployProxy struct{} + +func (DeployProxy) VerifyPreconditions(env cldf.Environment, req *DeployProxyRequest) error { + if err := verifyContractRef(env, req.ChainSel, CacheContract, req.CacheQualifier, req.Version); err != nil { + return err + } + if _, err := os.Stat(req.WasmPath); err != nil { + return fmt.Errorf("wasm path: %w", err) + } + if req.Owner != "" { + if err := validateAddress(req.Owner); err != nil { + return err + } + } + if req.CacheQualifier == "" { + return fmt.Errorf("cache qualifier must be set") + } + return nil +} + +func (DeployProxy) Apply(env cldf.Environment, req *DeployProxyRequest) (cldf.ChangesetOutput, error) { + var out cldf.ChangesetOutput + ch, ok := env.BlockChains.StellarChains()[req.ChainSel] + if !ok { + return out, fmt.Errorf("stellar chain not found for chain selector %d", req.ChainSel) + } + + version := semver.MustParse(req.Version) + cacheRef, err := env.DataStore.Addresses().Get( + datastore.NewAddressRefKey(req.ChainSel, CacheContract, version, req.CacheQualifier), + ) + if err != nil { + return out, fmt.Errorf("cache address ref not found for qualifier %q: %w", req.CacheQualifier, err) + } + + deps, err := newStellarDeps(ch) + if err != nil { + return out, err + } + + owner := req.Owner + if owner == "" { + if ch.Signer == nil { + return out, fmt.Errorf("owner not set and chain has no signer") + } + owner = ch.Signer.Address() + } + salt := stellardeploy.GenerateDeterministicSalt(owner, "data_feeds_proxy-"+req.Qualifier) + + report, err := operations.ExecuteOperation(env.OperationsBundle, operation.DeployProxy, deps, operation.DeployProxyInput{ + WasmPath: req.WasmPath, + Salt: salt, + Owner: owner, + Cache: cacheRef.Address, + }) + if err != nil { + return out, err + } + + out.DataStore = datastore.NewMemoryDataStore() + return out, out.DataStore.Addresses().Add(datastore.AddressRef{ + Address: report.Output.ContractID, + ChainSelector: req.ChainSel, + Type: ProxyContract, + Version: version, + Qualifier: req.Qualifier, + Labels: req.LabelSet, + }) +} diff --git a/deployment/data-feeds/changeset/stellar/deps.go b/deployment/data-feeds/changeset/stellar/deps.go new file mode 100644 index 00000000000..ab63c762f96 --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/deps.go @@ -0,0 +1,82 @@ +package stellar + +import ( + "fmt" + + "github.com/Masterminds/semver/v3" + + cldfstellar "github.com/smartcontractkit/chainlink-deployments-framework/chain/stellar" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + stellardeploy "github.com/smartcontractkit/chainlink-stellar/deployment" + + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/stellar/operation" +) + +// newStellarDeps builds the deploy/invoke dependencies for a CLDF Stellar chain. +// Package-level var so unit tests can substitute fakes (the Invoker seam). +var newStellarDeps = func(ch cldfstellar.Chain) (operation.StellarDeps, error) { + d, err := stellardeploy.NewDeployerFromChain(ch) + if err != nil { + return operation.StellarDeps{}, err + } + return operation.StellarDeps{Deploy: d, Invoker: d}, nil +} + +// stellarApplyDeps bundles a resolved contract's address with the chain deps +// needed to invoke an operation against it — shared by every stellar +// changeset that acts on a single already-deployed contract. +type stellarApplyDeps struct { + deps operation.StellarDeps + contractID string +} + +// verifyContractRef checks that chainSel names a known Stellar chain, +// versionStr parses, and an AddressRef exists for (chainSel, contractType, +// version, qualifier). This is the chain/version/ref-existence skeleton +// shared by every changeset's VerifyPreconditions; only the extra checks +// differ. +func verifyContractRef(env cldf.Environment, chainSel uint64, contractType datastore.ContractType, qualifier, versionStr string) error { + if _, ok := env.BlockChains.StellarChains()[chainSel]; !ok { + return fmt.Errorf("stellar chain not found for chain selector %d", chainSel) + } + version, err := semver.NewVersion(versionStr) + if err != nil { + return fmt.Errorf("invalid version %q: %w", versionStr, err) + } + if _, err := env.DataStore.Addresses().Get( + datastore.NewAddressRefKey(chainSel, contractType, version, qualifier), + ); err != nil { + return fmt.Errorf("%s address ref not found for qualifier %q: %w", contractType, qualifier, err) + } + return nil +} + +// resolveContractDeps looks up the AddressRef for (chainSel, contractType, +// version, qualifier) and bundles it with the chain's deploy/invoke deps, +// returning the AddressRef itself alongside for callers that need more than +// just its Address (e.g. LoadCacheClient/LoadProxyClient). version must +// already be parsed (see semver.MustParse in callers that have validated +// req.Version in VerifyPreconditions, typically via verifyContractRef). +func resolveContractDeps(env cldf.Environment, chainSel uint64, contractType datastore.ContractType, qualifier string, version *semver.Version) (stellarApplyDeps, datastore.AddressRef, error) { + var zero stellarApplyDeps + var zeroRef datastore.AddressRef + ch, ok := env.BlockChains.StellarChains()[chainSel] + if !ok { + return zero, zeroRef, fmt.Errorf("stellar chain not found for chain selector %d", chainSel) + } + + ref, err := env.DataStore.Addresses().Get( + datastore.NewAddressRefKey(chainSel, contractType, version, qualifier), + ) + if err != nil { + return zero, zeroRef, fmt.Errorf("%s address ref not found for qualifier %q: %w", contractType, qualifier, err) + } + + deps, err := newStellarDeps(ch) + if err != nil { + return zero, zeroRef, err + } + + return stellarApplyDeps{deps: deps, contractID: ref.Address}, ref, nil +} diff --git a/deployment/data-feeds/changeset/stellar/doc.go b/deployment/data-feeds/changeset/stellar/doc.go new file mode 100644 index 00000000000..88509c8608e --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/doc.go @@ -0,0 +1,4 @@ +// Package stellar contains CLD changesets for the Stellar (Soroban) Data Feeds +// cache and proxy contracts. Style follows the Solana suite (ChangeSetV2 + +// operations); functional coverage follows the EVM suite. +package stellar diff --git a/deployment/data-feeds/changeset/stellar/e2e_test.go b/deployment/data-feeds/changeset/stellar/e2e_test.go new file mode 100644 index 00000000000..cc6fd331ee2 --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/e2e_test.go @@ -0,0 +1,487 @@ +package stellar + +import ( + "bytes" + "fmt" + "io" + "math/big" + "net/http" + "net/url" + "os" + "sync" + "testing" + "time" + + chainsel "github.com/smartcontractkit/chain-selectors" + "github.com/stellar/go-stellar-sdk/xdr" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + cldfchain "github.com/smartcontractkit/chainlink-deployments-framework/chain" + cldfstellar "github.com/smartcontractkit/chainlink-deployments-framework/chain/stellar" + stellarprovider "github.com/smartcontractkit/chainlink-deployments-framework/chain/stellar/provider" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/environment" + cache "github.com/smartcontractkit/chainlink-stellar/bindings/contracts/data_feeds_cache" + "github.com/smartcontractkit/chainlink-stellar/bindings/scval" + + commonchangeset "github.com/smartcontractkit/chainlink/deployment/common/changeset" +) + +// TestStellarDataFeedsE2E is the Definition-of-Done gate: it stands up a local +// CTF Soroban devnet, deploys both Data Feeds contracts, and invokes EVERY +// exported function on-chain — the 23 cache fns and the 14 proxy fns — through +// the real changesets/operations/bindings (no fakes). +// +// Function-coverage checklist. Outcome (a) = on-chain success; outcome (b) = a +// documented, asserted, expected contract-domain/host error (never ignored). +// +// DataFeedsCache (23): +// __constructor (a) DeployCache changeset +// upgrade (a) UpgradeCache changeset +// recover_tokens (b) RecoverTokens changeset — SAC transfer(from=self) +// re-enters the contract => host trap Error(Context, +// InvalidAction), asserted +// add_feed_admin (a) AddFeedAdmin changeset +// remove_feed_admin (a) RemoveFeedAdmin changeset +// set_feed_configs (a) SetFeedConfigs changeset +// remove_feed_configs (a) RemoveFeedConfigs changeset +// transfer_ownership (a) TransferOwnership changeset (self-transfer) +// accept_ownership (a) AcceptOwnership changeset +// renounce_ownership (a) RenounceOwnership changeset (LAST — irreversible) +// version (a) LoadCacheClient direct +// type_and_version (a) LoadCacheClient direct +// get_owner (a) LoadCacheClient direct +// decimals (a) LoadCacheClient direct +// description (a) LoadCacheClient direct +// is_feed_admin (a) LoadCacheClient direct +// has_permission (a) LoadCacheClient direct +// get_feed_permissions (a) LoadCacheClient direct +// on_report (a) LoadCacheClient direct — valid metadata+XDR report from the +// configured AllowedSender; stores a round +// latest_round (a) LoadCacheClient direct — returns the stored round +// get_round (a) LoadCacheClient direct — returns the stored round +// round_range (a) LoadCacheClient direct +// find_round (a) LoadCacheClient direct +// +// DataFeedsProxy (14): +// __constructor (a) DeployProxy changeset +// set_cache (a) SetProxyCache changeset +// transfer_ownership (a) TransferOwnership changeset (self-transfer) +// accept_ownership (a) AcceptOwnership changeset +// renounce_ownership (a) RenounceOwnership changeset (LAST — irreversible) +// version (a) LoadProxyClient direct +// type_and_version (a) LoadProxyClient direct +// get_owner (a) LoadProxyClient direct +// decimals (a) LoadProxyClient direct +// description (a) LoadProxyClient direct +// latest_round (a) LoadProxyClient direct — reads the round stored via on_report +// get_round (a) LoadProxyClient direct — reads the round stored via on_report +// upgrade (a) LoadProxyClient direct — fresh wasm hash (no changeset by design) +// recover_tokens (b) LoadProxyClient direct — SAC transfer(from=self) +// re-enters the contract => host trap Error(Context, +// InvalidAction), asserted +// +// The proxy deliberately has no upgrade/recover_tokens changeset (no operator +// need identified yet), so those two are driven through the generated proxy +// client directly. + +// e2eOnce guards the CTF container lifecycle for this suite (the provider +// requires a *sync.Once; one test => one Once). +var e2eOnce sync.Once + +const ( + e2eQualifier = "e2e" + e2eVersion = "1.0.0" + // e2eDataID is a canonical left-aligned feed id (BTC/USD-shaped). After + // dataIDsToBytes left-justifies it into [16]byte, byte[7] is 0x00, which is + // outside the cache's decimals window [0x20,0x60], so cache.decimals returns 0 + // (the else branch). Non-zero id => a valid feed id (is_valid_id passes). + e2eDataID = "0x018e16c39e00032000000" + e2eDescription = "BTC/USD" + e2eWorkflowName = "e2e" + // e2eWorkflowOwner is the 20-byte workflow owner shared by the feed config + // permission and the on_report metadata (they must match for the report to + // be accepted and stored). + e2eWorkflowOwner = "0x0102030405060708090a0b0c0d0e0f1011121314" + // e2eReportTS must be > 0 (the initial stored timestamp) so the report is + // recorded as a new round rather than rejected as stale. + e2eReportTS = uint64(1_700_000_000) +) + +func TestStellarDataFeedsE2E(t *testing.T) { + skipInCI(t) + + sel := chainsel.STELLAR_LOCALNET.Selector + ctx := t.Context() + + // --- stand up / attach to the local Soroban devnet --------------------- + bc := newE2EChain(t, sel) + + e, err := environment.New(ctx, + environment.WithChains(bc), + environment.WithLogger(logger.Test(t)), + ) + require.NoError(t, err) + env := *e + + ch, ok := env.BlockChains.StellarChains()[sel] + require.True(t, ok, "stellar chain %d must be present in the environment", sel) + deployer := ch.Signer.Address() + + // The random deployer key has no balance; friendbot funds it before any fee- + // bearing operation. Tolerates "already funded". + fundViaFriendbot(t, ch.FriendbotURL, deployer) + + const cacheWasm = "testdata/data_feeds_cache.wasm" + const proxyWasm = "testdata/data_feeds_proxy.wasm" + + // --- deploy + configure (changeset pipeline) --------------------------- + env = apply(t, env, DeployCache{}, &DeployCacheRequest{ + ChainSel: sel, WasmPath: cacheWasm, Qualifier: e2eQualifier, Version: e2eVersion, + }) + env = apply(t, env, DeployProxy{}, &DeployProxyRequest{ + ChainSel: sel, WasmPath: proxyWasm, CacheQualifier: e2eQualifier, + Qualifier: e2eQualifier, Version: e2eVersion, + }) + env = apply(t, env, AddFeedAdmin{}, &FeedAdminRequest{ + ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, Admin: deployer, + }) + env = apply(t, env, SetFeedConfigs{}, &SetFeedConfigsRequest{ + ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, Admin: deployer, + DataIDs: []string{e2eDataID}, Descriptions: []string{e2eDescription}, + Permissions: []FeedPermission{{ + AllowedSender: deployer, + AllowedWorkflowOwner: e2eWorkflowOwner, + AllowedWorkflowName: e2eWorkflowName, + }}, + }) + env = apply(t, env, SetProxyCache{}, &SetProxyCacheRequest{ + ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, CacheQualifier: e2eQualifier, + }) + + // Precompute the binding-typed feed id / permission tuple used by the + // direct calls below (reuse the package's own hex->bytes converters). + ids, err := dataIDsToBytes([]string{e2eDataID}) + require.NoError(t, err) + did := [16]byte(ids[0]) + + ownerBytes, err := workflowOwnerToBytes(e2eWorkflowOwner) + require.NoError(t, err) + nameBytes, err := workflowNameToBytes(e2eWorkflowName) + require.NoError(t, err) + + // --- cache: on_report (stores a round) + every read ------------------- + cacheClient, cacheRef, err := LoadCacheClient(env, sel, e2eQualifier, e2eVersion) + require.NoError(t, err) + cacheAddr := cacheRef.Address + + metadata := buildReportMetadata([10]byte(nameBytes), [20]byte(ownerBytes)) + report := buildReport(t, ids[0], big.NewInt(123456), e2eReportTS) + require.NoError(t, cacheClient.OnReport(ctx, deployer, metadata, report), + "on_report with a valid encoding from the configured AllowedSender must succeed") + + // Prove the report was actually stored (this is what makes the proxy + // latest_round/get_round reads return data, i.e. outcome (a)). + stored, err := cacheClient.LatestRound(ctx, did) + require.NoError(t, err) + require.NotNil(t, stored, "on_report should have stored a round") + require.Equalf(t, 0, stored.Answer.Cmp(big.NewInt(123456)), + "stored answer mismatch: got %v", stored.Answer) + require.Equal(t, uint64(1), stored.RoundId) + + mustCall(t, "cache.version", func() error { _, e := cacheClient.Version(ctx); return e }) + mustCall(t, "cache.type_and_version", func() error { _, e := cacheClient.TypeAndVersion(ctx); return e }) + mustCall(t, "cache.get_owner", func() error { _, e := cacheClient.GetOwner(ctx); return e }) + mustCall(t, "cache.decimals", func() error { _, e := cacheClient.Decimals(ctx, did); return e }) + mustCall(t, "cache.description", func() error { _, e := cacheClient.Description(ctx, did); return e }) + mustCall(t, "cache.is_feed_admin", func() error { _, e := cacheClient.IsFeedAdmin(ctx, deployer); return e }) + mustCall(t, "cache.has_permission", func() error { + _, e := cacheClient.HasPermission(ctx, did, deployer, [20]byte(ownerBytes), [10]byte(nameBytes)) + return e + }) + mustCall(t, "cache.get_feed_permissions", func() error { _, e := cacheClient.GetFeedPermissions(ctx, did); return e }) + mustCall(t, "cache.get_round", func() error { + r, e := cacheClient.GetRound(ctx, did, 1) + if e != nil { + return e + } + require.NotNil(t, r, "get_round(did,1) must return the stored round, not void") + return nil + }) + mustCall(t, "cache.round_range", func() error { + rounds, e := cacheClient.RoundRange(ctx, did, 1, 1) + if e != nil { + return e + } + require.NotEmpty(t, rounds, "round_range should return the stored round") + return nil + }) + mustCall(t, "cache.find_round", func() error { + r, e := cacheClient.FindRound(ctx, did, e2eReportTS, cache.BoundAtOrBefore) + if e != nil { + return e + } + require.NotNil(t, r, "find_round must return the stored round, not void") + return nil + }) + + // --- proxy reads (read through to the cache's stored round) ----------- + proxyClient, _, err := LoadProxyClient(env, sel, e2eQualifier, e2eVersion) + require.NoError(t, err) + pdid := [16]byte(ids[0]) + + mustCall(t, "proxy.version", func() error { _, e := proxyClient.Version(ctx); return e }) + mustCall(t, "proxy.type_and_version", func() error { _, e := proxyClient.TypeAndVersion(ctx); return e }) + mustCall(t, "proxy.get_owner", func() error { _, e := proxyClient.GetOwner(ctx); return e }) + mustCall(t, "proxy.decimals", func() error { _, e := proxyClient.Decimals(ctx, pdid); return e }) + mustCall(t, "proxy.description", func() error { _, e := proxyClient.Description(ctx, pdid); return e }) + mustCall(t, "proxy.latest_round", func() error { _, e := proxyClient.LatestRound(ctx, pdid); return e }) + mustCall(t, "proxy.get_round", func() error { _, e := proxyClient.GetRound(ctx, pdid, 1); return e }) + + // --- proxy upgrade + recover_tokens (no changesets by design) --------- + deps, err := newStellarDeps(ch) + require.NoError(t, err) + proxyHash, err := deps.Deploy.UploadContractWASM(ctx, proxyWasm) + require.NoError(t, err) + require.NoError(t, proxyClient.Upgrade(ctx, [32]byte(proxyHash)), + "proxy upgrade to a freshly uploaded wasm hash must succeed") + + // recover_tokens invokes the SAC transfer(from=self, to, amount). We pass the + // contract's own address as the token, so the transfer sub-call re-enters the + // same contract; Soroban forbids contract re-entry and the host traps with + // Error(Context, InvalidAction) ("Contract re-entry is not allowed"). This is + // the documented (b) outcome: recover_tokens is really invoked (its three args + // are decoded and the transfer is dispatched — the tooling's encode/invoke path + // is what's under test) and the trap is a contract/host-domain error, not a + // transport/XDR/auth-plumbing failure. Standing up a funded native-XLM SAC + // balance for a success path is disproportionate for this gate. + proxyRecoverErr := proxyClient.RecoverTokens(ctx, proxyClient.ContractID(), deployer, 1) + require.Error(t, proxyRecoverErr, "proxy recover_tokens must fail: self-transfer re-enters the contract") + require.ErrorContains(t, proxyRecoverErr, "InvalidAction", "expected a contract re-entry host trap") + t.Logf("(b) proxy.recover_tokens error: %v", proxyRecoverErr) + + // --- proxy ownership: transfer -> accept -> renounce (renounce LAST) --- + proxyLiveUntil := nextLiveUntil(t, ch) + env = apply(t, env, TransferOwnership{}, &OwnershipRequest{ + ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, + Contract: ProxyContract, NewOwner: deployer, LiveUntilLedger: proxyLiveUntil, + }) + env = apply(t, env, AcceptOwnership{}, &OwnershipRequest{ + ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, Contract: ProxyContract, + }) + env = apply(t, env, RenounceOwnership{}, &OwnershipRequest{ + ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, Contract: ProxyContract, + }) + + // --- cache upgrade + recover + config/admin removal -------------------- + env = apply(t, env, UpgradeCache{}, &UpgradeCacheRequest{ + ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, WasmPath: cacheWasm, + }) + + // recover_tokens: same real-transfer semantics as the proxy. Token is the + // cache's own address, so the SAC transfer(from=self) re-enters the contract + // and the host traps with Error(Context, InvalidAction) — documented (b). Run + // while the deployer is still owner (ownership renounce happens last). + cacheRecoverErr := applyErr(t, env, RecoverTokens{}, &RecoverTokensRequest{ + ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, + Token: cacheAddr, To: deployer, Amount: 1, + }) + require.Error(t, cacheRecoverErr, "cache recover_tokens must fail: self-transfer re-enters the contract") + require.ErrorContains(t, cacheRecoverErr, "InvalidAction", "expected a contract re-entry host trap") + t.Logf("(b) cache.recover_tokens error: %v", cacheRecoverErr) + + env = apply(t, env, RemoveFeedConfigs{}, &RemoveFeedConfigsRequest{ + ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, Admin: deployer, + DataIDs: []string{e2eDataID}, + }) + env = apply(t, env, RemoveFeedAdmin{}, &FeedAdminRequest{ + ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, Admin: deployer, + }) + + // --- cache ownership: transfer -> accept -> renounce (renounce LAST) --- + cacheLiveUntil := nextLiveUntil(t, ch) + env = apply(t, env, TransferOwnership{}, &OwnershipRequest{ + ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, + Contract: CacheContract, NewOwner: deployer, LiveUntilLedger: cacheLiveUntil, + }) + env = apply(t, env, AcceptOwnership{}, &OwnershipRequest{ + ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, Contract: CacheContract, + }) + env = apply(t, env, RenounceOwnership{}, &OwnershipRequest{ + ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, Contract: CacheContract, + }) +} + +// skipInCI mirrors the Solana suite convention: container-backed e2e tests do +// not run in CI (they need a local Docker daemon). +func skipInCI(t *testing.T) { + t.Helper() + if os.Getenv("CI") == "true" { + t.Skip("Skipping container-backed e2e test in CI") + } +} + +// newE2EChain provisions the Stellar devnet chain. +// +// The contracts under test are built with soroban-sdk 26.1.0, i.e. their wasm +// env-meta requires ledger protocol 26. quickstart:latest arms its local +// network at protocol 25 (uploads fail with `HostError: Error(Context, +// InternalError)`), so the CTF path pins stellar/quickstart:future, whose +// local network defaults to protocol 26. +// +// Default path: boot a CTF quickstart container via NewCTFChainProvider. +// +// Override path (STELLAR_E2E_RPC_URL set): attach to an externally started +// quickstart container via NewRPCChainProvider. This exists because the CTF +// framework hardcodes the container platform to linux/amd64 +// (chainlink-testing-framework framework/components/blockchain/stellar.go) and +// CLDF's CTFChainProviderConfig does not expose the ImagePlatform knob — so on +// a native linux/arm64 host without amd64 emulation the CTF path fails with +// "exec /start: exec format error" even though the quickstart images ship an +// arm64 variant. On such hosts run the same image natively and point the test +// at it: +// +// docker run -d -p 8000:8000 stellar/quickstart:latest \ +// --local --enable-soroban-rpc --protocol-version 26 +// STELLAR_E2E_RPC_URL=http://127.0.0.1:8000/rpc \ +// STELLAR_E2E_FRIENDBOT_URL=http://127.0.0.1:8000/friendbot \ +// go test ./data-feeds/changeset/stellar/ -run TestStellarDataFeedsE2E -v -timeout 25m +func newE2EChain(t *testing.T, sel uint64) cldfchain.BlockChain { + t.Helper() + + if rpcURL := os.Getenv("STELLAR_E2E_RPC_URL"); rpcURL != "" { + // Passphrase for the quickstart `--local` standalone network — the same + // one the CTF path uses (blockchain.DefaultStellarNetworkPassphrase). + const localPassphrase = "Standalone Network ; February 2017" + p := stellarprovider.NewRPCChainProvider(sel, stellarprovider.RPCChainProviderConfig{ + SorobanRPCURL: rpcURL, + NetworkPassphrase: localPassphrase, + FriendbotURL: os.Getenv("STELLAR_E2E_FRIENDBOT_URL"), + DeployerKeypairGen: stellarprovider.KeypairRandom(), + }) + bc, err := p.Initialize(t.Context()) + require.NoError(t, err) + return bc + } + + p := stellarprovider.NewCTFChainProvider(t, sel, stellarprovider.CTFChainProviderConfig{ + DeployerKeypairGen: stellarprovider.KeypairRandom(), + Once: &e2eOnce, + // protocol 26 local network; see doc comment above. + Image: "stellar/quickstart:future", + }) + bc, err := p.Initialize(t.Context()) + require.NoError(t, err) + return bc +} + +// apply configures and applies a single changeset, requiring success, and +// returns the merged environment (datastore + address book threaded forward). +func apply[C any](t *testing.T, env cldf.Environment, cs cldf.ChangeSetV2[C], cfg C) cldf.Environment { + t.Helper() + out, err := commonchangeset.Apply(t, env, commonchangeset.Configure(cs, cfg)) + require.NoError(t, err) + return out +} + +// applyErr configures and applies a single changeset expected to fail, and +// returns the error (the environment is unchanged on failure). +func applyErr[C any](t *testing.T, env cldf.Environment, cs cldf.ChangeSetV2[C], cfg C) error { + t.Helper() + _, err := commonchangeset.Apply(t, env, commonchangeset.Configure(cs, cfg)) + return err +} + +// mustCall runs a read/query closure and requires it to return no error. +func mustCall(t *testing.T, name string, fn func() error) { + t.Helper() + require.NoErrorf(t, fn(), "call %s", name) +} + +// nextLiveUntil returns a pending-transfer expiry ledger valid for the current +// chain: strictly greater than the current ledger and comfortably below +// max_live_until_ledger. u32::MAX would trip InvalidLiveUntilLedger. +func nextLiveUntil(t *testing.T, ch cldfstellar.Chain) uint32 { + t.Helper() + resp, err := ch.Client.GetLatestLedger(t.Context()) + require.NoError(t, err) + return resp.Sequence + 100_000 +} + +// buildReportMetadata lays out the 64-byte on_report metadata header the cache +// decodes: [0:32) workflow_cid | [32:42) workflow_name | [42:62) workflow_owner +// | [62:64) report_id. Only workflow_name and workflow_owner participate in the +// permission hash, so the cid/report_id bytes are left zero. +func buildReportMetadata(name [10]byte, owner [20]byte) []byte { + md := make([]byte, 64) + copy(md[32:42], name[:]) + copy(md[42:62], owner[:]) + return md +} + +// buildReport builds the on_report `report` payload: the XDR encoding of a +// Soroban `Vec` with a single entry. The cache truncates the +// 32-byte wire data id to its high 16 bytes, so wireHi16 is placed in the top +// half and the rest left zero. +func buildReport(t *testing.T, wireHi16 [16]byte, answer *big.Int, ts uint64) []byte { + t.Helper() + var wire [32]byte + copy(wire[0:16], wireHi16[:]) + + // #[contracttype] structs serialize as an ScMap with symbol keys sorted + // alphabetically (answer < data_id < timestamp) — BuildStructScVal sorts. + entry, err := scval.BuildStructScVal(map[string]xdr.ScVal{ + "data_id": scval.Bytes32ToScVal(wire), + "answer": scval.MustToScVal(scval.I256ToScVal(answer)), + "timestamp": scval.Uint64ToScVal(ts), + }) + require.NoError(t, err) + + vec := xdr.ScVec{entry} + vecPtr := &vec + reportVal := xdr.ScVal{Type: xdr.ScValTypeScvVec, Vec: &vecPtr} + b, err := reportVal.MarshalBinary() + require.NoError(t, err) + return b +} + +// fundViaFriendbot funds addr through the CTF chain's friendbot, tolerating an +// already-funded account. Friendbot may not be ready the instant the container +// starts, so it retries. +func fundViaFriendbot(t *testing.T, friendbotURL, addr string) { + t.Helper() + require.NotEmpty(t, friendbotURL, "friendbot URL must be set to fund the deployer") + + target := friendbotURL + "?addr=" + url.QueryEscape(addr) + var lastErr error + for i := 0; i < 15; i++ { + req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, target, nil) //nolint:gosec // local CTF devnet URL + if err != nil { + lastErr = err + time.Sleep(2 * time.Second) + continue + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + lastErr = err + time.Sleep(2 * time.Second) + continue + } + body, _ := io.ReadAll(resp.Body) + _ = resp.Body.Close() + if resp.StatusCode == http.StatusOK { + return + } + // Friendbot returns a 400 with op_already_exists when the account is + // already funded — that's a success for our purposes. + if bytes.Contains(body, []byte("already")) || bytes.Contains(body, []byte("exists")) { + return + } + lastErr = fmt.Errorf("friendbot status %d: %s", resp.StatusCode, string(body)) + time.Sleep(2 * time.Second) + } + // Don't fail here — let the first fee-bearing deploy surface a precise + // underfunded error if funding genuinely failed. + t.Logf("warning: friendbot funding did not confirm success: %v", lastErr) +} diff --git a/deployment/data-feeds/changeset/stellar/feed_admin.go b/deployment/data-feeds/changeset/stellar/feed_admin.go new file mode 100644 index 00000000000..4d9ae66e254 --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/feed_admin.go @@ -0,0 +1,81 @@ +package stellar + +import ( + "github.com/Masterminds/semver/v3" + + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" + + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/stellar/operation" +) + +// FeedAdminRequest grants or revokes feed-admin rights on an already-deployed +// DataFeedsCache contract. +type FeedAdminRequest struct { + ChainSel uint64 + Qualifier string + Version string + Admin string +} + +func (req *FeedAdminRequest) verifyPreconditions(env cldf.Environment) error { + if err := verifyContractRef(env, req.ChainSel, CacheContract, req.Qualifier, req.Version); err != nil { + return err + } + if err := validateAddress(req.Admin); err != nil { + return err + } + return nil +} + +// resolve looks up the cache contract's dependencies + address for req. +func (req *FeedAdminRequest) resolve(env cldf.Environment) (stellarApplyDeps, error) { + version := semver.MustParse(req.Version) + d, _, err := resolveContractDeps(env, req.ChainSel, CacheContract, req.Qualifier, version) + return d, err +} + +var ( + _ cldf.ChangeSetV2[*FeedAdminRequest] = AddFeedAdmin{} + _ cldf.ChangeSetV2[*FeedAdminRequest] = RemoveFeedAdmin{} +) + +// AddFeedAdmin grants feed-admin rights on the cache. +type AddFeedAdmin struct{} + +func (AddFeedAdmin) VerifyPreconditions(env cldf.Environment, req *FeedAdminRequest) error { + return req.verifyPreconditions(env) +} + +func (AddFeedAdmin) Apply(env cldf.Environment, req *FeedAdminRequest) (cldf.ChangesetOutput, error) { + var out cldf.ChangesetOutput + d, err := req.resolve(env) + if err != nil { + return out, err + } + _, err = operations.ExecuteOperation(env.OperationsBundle, operation.AddFeedAdmin, d.deps, operation.FeedAdminInput{ + ContractID: d.contractID, + Admin: req.Admin, + }) + return out, err +} + +// RemoveFeedAdmin revokes feed-admin rights on the cache. +type RemoveFeedAdmin struct{} + +func (RemoveFeedAdmin) VerifyPreconditions(env cldf.Environment, req *FeedAdminRequest) error { + return req.verifyPreconditions(env) +} + +func (RemoveFeedAdmin) Apply(env cldf.Environment, req *FeedAdminRequest) (cldf.ChangesetOutput, error) { + var out cldf.ChangesetOutput + d, err := req.resolve(env) + if err != nil { + return out, err + } + _, err = operations.ExecuteOperation(env.OperationsBundle, operation.RemoveFeedAdmin, d.deps, operation.FeedAdminInput{ + ContractID: d.contractID, + Admin: req.Admin, + }) + return out, err +} diff --git a/deployment/data-feeds/changeset/stellar/operation/operation.go b/deployment/data-feeds/changeset/stellar/operation/operation.go new file mode 100644 index 00000000000..13211cb1487 --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/operation/operation.go @@ -0,0 +1,254 @@ +// Package operation contains the CLDF operations for the Stellar Data Feeds +// cache and proxy contracts. Each operation is one on-chain interaction; the +// changesets in the parent package compose and execute them. +package operation + +import ( + "context" + + "github.com/Masterminds/semver/v3" + + cldfops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + "github.com/smartcontractkit/chainlink-stellar/bindings" + cache "github.com/smartcontractkit/chainlink-stellar/bindings/contracts/data_feeds_cache" + proxy "github.com/smartcontractkit/chainlink-stellar/bindings/contracts/data_feeds_proxy" + "github.com/smartcontractkit/chainlink-stellar/bindings/scval" + "github.com/stellar/go-stellar-sdk/xdr" +) + +var Version1_0_0 = semver.MustParse("1.0.0") + +// SorobanContractDeployer is the deploy-time surface these operations need +// from chainlink-stellar's concrete deployment.Deployer, defined locally so +// the upstream package does not have to export a widened interface. +type SorobanContractDeployer interface { + DeployContractWithArgs(ctx context.Context, wasmPath string, salt [32]byte, ctorArgs []xdr.ScVal) (string, error) + UploadContractWASM(ctx context.Context, wasmPath string) (xdr.Hash, error) +} + +// StellarDeps bundles deploy-time and runtime chain I/O for these operations. +// The same *deployment.Deployer satisfies both fields. +type StellarDeps struct { + Deploy SorobanContractDeployer + Invoker bindings.Invoker +} + +// Void is the output for operations that return no payload. +type Void struct{} + +// DeployOutput carries a deployed contract address (C... strkey). +type DeployOutput struct { + ContractID string `json:"contract_id"` +} + +type DeployCacheInput struct { + WasmPath string `json:"wasm_path"` + Salt [32]byte `json:"salt"` + Owner string `json:"owner"` +} + +// DeployCache uploads the cache WASM and instantiates it via CreateContractV2 +// with __constructor(owner). The cache constructor takes a single argument; +// the data-retention TTL is a hardcoded on-chain constant, not a ctor input. +var DeployCache = cldfops.NewOperation( + "df-cache:deploy", Version1_0_0, + "Deploys the DataFeedsCache Soroban contract", + func(b cldfops.Bundle, d StellarDeps, in DeployCacheInput) (DeployOutput, error) { + args := []xdr.ScVal{ + scval.AddressToScVal(in.Owner), + } + cid, err := d.Deploy.DeployContractWithArgs(b.GetContext(), in.WasmPath, in.Salt, args) + if err != nil { + return DeployOutput{}, err + } + return DeployOutput{ContractID: cid}, nil + }, +) + +type DeployProxyInput struct { + WasmPath string `json:"wasm_path"` + Salt [32]byte `json:"salt"` + Owner string `json:"owner"` + Cache string `json:"cache"` +} + +// DeployProxy instantiates the proxy via __constructor(owner, cache). +var DeployProxy = cldfops.NewOperation( + "df-proxy:deploy", Version1_0_0, + "Deploys the DataFeedsProxy Soroban contract", + func(b cldfops.Bundle, d StellarDeps, in DeployProxyInput) (DeployOutput, error) { + args := []xdr.ScVal{ + scval.AddressToScVal(in.Owner), + scval.AddressToScVal(in.Cache), + } + cid, err := d.Deploy.DeployContractWithArgs(b.GetContext(), in.WasmPath, in.Salt, args) + if err != nil { + return DeployOutput{}, err + } + return DeployOutput{ContractID: cid}, nil + }, +) + +type SetFeedConfigsInput struct { + ContractID string `json:"contract_id"` + Admin string `json:"admin"` + Entries []cache.FeedConfigEntry `json:"entries"` +} + +var SetFeedConfigs = cldfops.NewOperation( + "df-cache:set-feed-configs", Version1_0_0, + "Sets per-feed descriptions and workflow write-permissions on the cache", + func(b cldfops.Bundle, d StellarDeps, in SetFeedConfigsInput) (Void, error) { + c := cache.NewDataFeedsCacheClient(d.Invoker, in.ContractID) + return Void{}, c.SetFeedConfigs(b.GetContext(), in.Admin, in.Entries) + }, +) + +type RemoveFeedConfigsInput struct { + ContractID string `json:"contract_id"` + Admin string `json:"admin"` + DataIDs [][16]byte `json:"data_ids"` +} + +var RemoveFeedConfigs = cldfops.NewOperation( + "df-cache:remove-feed-configs", Version1_0_0, + "Removes feed configs from the cache", + func(b cldfops.Bundle, d StellarDeps, in RemoveFeedConfigsInput) (Void, error) { + c := cache.NewDataFeedsCacheClient(d.Invoker, in.ContractID) + return Void{}, c.RemoveFeedConfigs(b.GetContext(), in.Admin, in.DataIDs) + }, +) + +type FeedAdminInput struct { + ContractID string `json:"contract_id"` + Admin string `json:"admin"` +} + +var AddFeedAdmin = cldfops.NewOperation( + "df-cache:add-feed-admin", Version1_0_0, + "Grants feed-admin rights on the cache", + func(b cldfops.Bundle, d StellarDeps, in FeedAdminInput) (Void, error) { + c := cache.NewDataFeedsCacheClient(d.Invoker, in.ContractID) + return Void{}, c.AddFeedAdmin(b.GetContext(), in.Admin) + }, +) + +var RemoveFeedAdmin = cldfops.NewOperation( + "df-cache:remove-feed-admin", Version1_0_0, + "Revokes feed-admin rights on the cache", + func(b cldfops.Bundle, d StellarDeps, in FeedAdminInput) (Void, error) { + c := cache.NewDataFeedsCacheClient(d.Invoker, in.ContractID) + return Void{}, c.RemoveFeedAdmin(b.GetContext(), in.Admin) + }, +) + +// Ownership ops work for both contracts: the generated cache and proxy clients +// expose identical two-step-ownership methods, selected by ContractID + IsProxy. +type OwnershipInput struct { + ContractID string `json:"contract_id"` + IsProxy bool `json:"is_proxy"` + NewOwner string `json:"new_owner"` + LiveUntilLedger uint32 `json:"live_until_ledger"` +} + +var TransferOwnership = cldfops.NewOperation( + "df:transfer-ownership", Version1_0_0, + "Begins two-step ownership transfer", + func(b cldfops.Bundle, d StellarDeps, in OwnershipInput) (Void, error) { + if in.IsProxy { + c := proxy.NewDataFeedsProxyClient(d.Invoker, in.ContractID) + return Void{}, c.TransferOwnership(b.GetContext(), in.NewOwner, in.LiveUntilLedger) + } + c := cache.NewDataFeedsCacheClient(d.Invoker, in.ContractID) + return Void{}, c.TransferOwnership(b.GetContext(), in.NewOwner, in.LiveUntilLedger) + }, +) + +var AcceptOwnership = cldfops.NewOperation( + "df:accept-ownership", Version1_0_0, + "Accepts a pending ownership transfer (caller must be the pending owner)", + func(b cldfops.Bundle, d StellarDeps, in OwnershipInput) (Void, error) { + if in.IsProxy { + c := proxy.NewDataFeedsProxyClient(d.Invoker, in.ContractID) + return Void{}, c.AcceptOwnership(b.GetContext()) + } + c := cache.NewDataFeedsCacheClient(d.Invoker, in.ContractID) + return Void{}, c.AcceptOwnership(b.GetContext()) + }, +) + +var RenounceOwnership = cldfops.NewOperation( + "df:renounce-ownership", Version1_0_0, + "Renounces ownership permanently", + func(b cldfops.Bundle, d StellarDeps, in OwnershipInput) (Void, error) { + if in.IsProxy { + c := proxy.NewDataFeedsProxyClient(d.Invoker, in.ContractID) + return Void{}, c.RenounceOwnership(b.GetContext()) + } + c := cache.NewDataFeedsCacheClient(d.Invoker, in.ContractID) + return Void{}, c.RenounceOwnership(b.GetContext()) + }, +) + +type UploadWASMInput struct { + WasmPath string `json:"wasm_path"` +} +type UploadWASMOutput struct { + WasmHash [32]byte `json:"wasm_hash"` +} + +var UploadWASM = cldfops.NewOperation( + "df:upload-wasm", Version1_0_0, + "Uploads a WASM blob and returns its code hash (for upgrades)", + func(b cldfops.Bundle, d StellarDeps, in UploadWASMInput) (UploadWASMOutput, error) { + h, err := d.Deploy.UploadContractWASM(b.GetContext(), in.WasmPath) + if err != nil { + return UploadWASMOutput{}, err + } + return UploadWASMOutput{WasmHash: [32]byte(h)}, nil + }, +) + +type UpgradeCacheInput struct { + ContractID string `json:"contract_id"` + NewWasmHash [32]byte `json:"new_wasm_hash"` +} + +var UpgradeCache = cldfops.NewOperation( + "df-cache:upgrade", Version1_0_0, + "Points the cache at a new WASM implementation", + func(b cldfops.Bundle, d StellarDeps, in UpgradeCacheInput) (Void, error) { + c := cache.NewDataFeedsCacheClient(d.Invoker, in.ContractID) + return Void{}, c.Upgrade(b.GetContext(), in.NewWasmHash) + }, +) + +type RecoverTokensInput struct { + ContractID string `json:"contract_id"` + Token string `json:"token"` + To string `json:"to"` + Amount int64 `json:"amount"` +} + +var RecoverTokens = cldfops.NewOperation( + "df-cache:recover-tokens", Version1_0_0, + "Recovers tokens accidentally sent to the cache", + func(b cldfops.Bundle, d StellarDeps, in RecoverTokensInput) (Void, error) { + c := cache.NewDataFeedsCacheClient(d.Invoker, in.ContractID) + return Void{}, c.RecoverTokens(b.GetContext(), in.Token, in.To, in.Amount) + }, +) + +type SetProxyCacheInput struct { + ContractID string `json:"contract_id"` + Cache string `json:"cache"` +} + +var SetProxyCache = cldfops.NewOperation( + "df-proxy:set-cache", Version1_0_0, + "Points the proxy at a cache contract", + func(b cldfops.Bundle, d StellarDeps, in SetProxyCacheInput) (Void, error) { + c := proxy.NewDataFeedsProxyClient(d.Invoker, in.ContractID) + return Void{}, c.SetCache(b.GetContext(), in.Cache) + }, +) diff --git a/deployment/data-feeds/changeset/stellar/ownership.go b/deployment/data-feeds/changeset/stellar/ownership.go new file mode 100644 index 00000000000..46e76f8961b --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/ownership.go @@ -0,0 +1,123 @@ +package stellar + +import ( + "fmt" + + "github.com/Masterminds/semver/v3" + + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" + + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/stellar/operation" +) + +// OwnershipRequest drives the two-step ownership changesets for either DF +// contract (CacheContract or ProxyContract) — the generated cache and proxy +// clients expose identical ownership methods. +type OwnershipRequest struct { + ChainSel uint64 + Qualifier string + Version string + Contract datastore.ContractType // CacheContract or ProxyContract + NewOwner string // TransferOwnership only + LiveUntilLedger uint32 // TransferOwnership only; pending-transfer expiry ledger +} + +func (req *OwnershipRequest) verifyPreconditions(env cldf.Environment) error { + if _, ok := env.BlockChains.StellarChains()[req.ChainSel]; !ok { + return fmt.Errorf("stellar chain not found for chain selector %d", req.ChainSel) + } + if req.Contract != CacheContract && req.Contract != ProxyContract { + return fmt.Errorf("unsupported contract type %q: must be %q or %q", req.Contract, CacheContract, ProxyContract) + } + if err := verifyContractRef(env, req.ChainSel, req.Contract, req.Qualifier, req.Version); err != nil { + return err + } + return nil +} + +// resolve looks up the target contract's dependencies + address for req. +func (req *OwnershipRequest) resolve(env cldf.Environment) (stellarApplyDeps, error) { + version := semver.MustParse(req.Version) + d, _, err := resolveContractDeps(env, req.ChainSel, req.Contract, req.Qualifier, version) + return d, err +} + +var ( + _ cldf.ChangeSetV2[*OwnershipRequest] = TransferOwnership{} + _ cldf.ChangeSetV2[*OwnershipRequest] = AcceptOwnership{} + _ cldf.ChangeSetV2[*OwnershipRequest] = RenounceOwnership{} +) + +// TransferOwnership begins a two-step ownership transfer on the cache or proxy. +type TransferOwnership struct{} + +func (TransferOwnership) VerifyPreconditions(env cldf.Environment, req *OwnershipRequest) error { + if err := req.verifyPreconditions(env); err != nil { + return err + } + if err := validateAddress(req.NewOwner); err != nil { + return fmt.Errorf("new owner: %w", err) + } + if req.LiveUntilLedger == 0 { + return fmt.Errorf("LiveUntilLedger must be nonzero") + } + return nil +} + +func (TransferOwnership) Apply(env cldf.Environment, req *OwnershipRequest) (cldf.ChangesetOutput, error) { + var out cldf.ChangesetOutput + d, err := req.resolve(env) + if err != nil { + return out, err + } + _, err = operations.ExecuteOperation(env.OperationsBundle, operation.TransferOwnership, d.deps, operation.OwnershipInput{ + ContractID: d.contractID, + IsProxy: req.Contract == ProxyContract, + NewOwner: req.NewOwner, + LiveUntilLedger: req.LiveUntilLedger, + }) + return out, err +} + +// AcceptOwnership accepts a pending ownership transfer on the cache or proxy +// (the caller signing the underlying transaction must be the pending owner). +type AcceptOwnership struct{} + +func (AcceptOwnership) VerifyPreconditions(env cldf.Environment, req *OwnershipRequest) error { + return req.verifyPreconditions(env) +} + +func (AcceptOwnership) Apply(env cldf.Environment, req *OwnershipRequest) (cldf.ChangesetOutput, error) { + var out cldf.ChangesetOutput + d, err := req.resolve(env) + if err != nil { + return out, err + } + _, err = operations.ExecuteOperation(env.OperationsBundle, operation.AcceptOwnership, d.deps, operation.OwnershipInput{ + ContractID: d.contractID, + IsProxy: req.Contract == ProxyContract, + }) + return out, err +} + +// RenounceOwnership permanently renounces ownership of the cache or proxy. +type RenounceOwnership struct{} + +func (RenounceOwnership) VerifyPreconditions(env cldf.Environment, req *OwnershipRequest) error { + return req.verifyPreconditions(env) +} + +func (RenounceOwnership) Apply(env cldf.Environment, req *OwnershipRequest) (cldf.ChangesetOutput, error) { + var out cldf.ChangesetOutput + d, err := req.resolve(env) + if err != nil { + return out, err + } + _, err = operations.ExecuteOperation(env.OperationsBundle, operation.RenounceOwnership, d.deps, operation.OwnershipInput{ + ContractID: d.contractID, + IsProxy: req.Contract == ProxyContract, + }) + return out, err +} diff --git a/deployment/data-feeds/changeset/stellar/recover_tokens.go b/deployment/data-feeds/changeset/stellar/recover_tokens.go new file mode 100644 index 00000000000..2619ea53e19 --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/recover_tokens.go @@ -0,0 +1,60 @@ +package stellar + +import ( + "fmt" + + "github.com/Masterminds/semver/v3" + + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" + + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/stellar/operation" +) + +// RecoverTokensRequest recovers tokens accidentally sent to an already-deployed +// DataFeedsCache contract. +type RecoverTokensRequest struct { + ChainSel uint64 + Qualifier string + Version string + Token string + To string + Amount int64 +} + +var _ cldf.ChangeSetV2[*RecoverTokensRequest] = RecoverTokens{} + +// RecoverTokens recovers tokens accidentally sent to the cache. +type RecoverTokens struct{} + +func (RecoverTokens) VerifyPreconditions(env cldf.Environment, req *RecoverTokensRequest) error { + if err := verifyContractRef(env, req.ChainSel, CacheContract, req.Qualifier, req.Version); err != nil { + return err + } + if err := validateAddress(req.Token); err != nil { + return fmt.Errorf("token: %w", err) + } + if err := validateAddress(req.To); err != nil { + return fmt.Errorf("to: %w", err) + } + if req.Amount <= 0 { + return fmt.Errorf("amount must be positive") + } + return nil +} + +func (RecoverTokens) Apply(env cldf.Environment, req *RecoverTokensRequest) (cldf.ChangesetOutput, error) { + var out cldf.ChangesetOutput + version := semver.MustParse(req.Version) + d, _, err := resolveContractDeps(env, req.ChainSel, CacheContract, req.Qualifier, version) + if err != nil { + return out, err + } + _, err = operations.ExecuteOperation(env.OperationsBundle, operation.RecoverTokens, d.deps, operation.RecoverTokensInput{ + ContractID: d.contractID, + Token: req.Token, + To: req.To, + Amount: req.Amount, + }) + return out, err +} diff --git a/deployment/data-feeds/changeset/stellar/remove_feeds.go b/deployment/data-feeds/changeset/stellar/remove_feeds.go new file mode 100644 index 00000000000..40af6049681 --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/remove_feeds.go @@ -0,0 +1,63 @@ +package stellar + +import ( + "errors" + + "github.com/Masterminds/semver/v3" + + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" + + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/stellar/operation" +) + +// RemoveFeedConfigsRequest removes a batch of feed configs from an +// already-deployed DataFeedsCache contract. +type RemoveFeedConfigsRequest struct { + ChainSel uint64 + Qualifier string + Version string + Admin string + DataIDs []string +} + +var _ cldf.ChangeSetV2[*RemoveFeedConfigsRequest] = RemoveFeedConfigs{} + +// RemoveFeedConfigs removes feed configs from the cache. +type RemoveFeedConfigs struct{} + +func (RemoveFeedConfigs) VerifyPreconditions(env cldf.Environment, req *RemoveFeedConfigsRequest) error { + if err := verifyContractRef(env, req.ChainSel, CacheContract, req.Qualifier, req.Version); err != nil { + return err + } + if err := validateAddress(req.Admin); err != nil { + return err + } + if len(req.DataIDs) == 0 { + return errors.New("DataIDs cannot be empty") + } + if _, err := dataIDsToBytes(req.DataIDs); err != nil { + return err + } + return nil +} + +func (RemoveFeedConfigs) Apply(env cldf.Environment, req *RemoveFeedConfigsRequest) (cldf.ChangesetOutput, error) { + var out cldf.ChangesetOutput + version := semver.MustParse(req.Version) + d, _, err := resolveContractDeps(env, req.ChainSel, CacheContract, req.Qualifier, version) + if err != nil { + return out, err + } + + ids, err := dataIDsToBytes(req.DataIDs) + if err != nil { + return out, err + } + _, err = operations.ExecuteOperation(env.OperationsBundle, operation.RemoveFeedConfigs, d.deps, operation.RemoveFeedConfigsInput{ + ContractID: d.contractID, + Admin: req.Admin, + DataIDs: ids, + }) + return out, err +} diff --git a/deployment/data-feeds/changeset/stellar/report_encoding_test.go b/deployment/data-feeds/changeset/stellar/report_encoding_test.go new file mode 100644 index 00000000000..6213f6d8590 --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/report_encoding_test.go @@ -0,0 +1,71 @@ +package stellar + +import ( + "math/big" + "testing" + + "github.com/stellar/go-stellar-sdk/xdr" + "github.com/stretchr/testify/require" +) + +// TestBuildReportMetadataLayout pins buildReportMetadata's 64-byte on_report +// metadata header: [0:32) workflow_cid zero | [32:42) workflow_name | +// [42:62) workflow_owner | [62:64) report_id zero. +func TestBuildReportMetadataLayout(t *testing.T) { + var name [10]byte + for i := range name { + name[i] = byte(i + 1) // 0x01..0x0a, distinguishable from zero padding + } + var owner [20]byte + for i := range owner { + owner[i] = byte(i + 100) // 0x64..0x77 + } + + got := buildReportMetadata(name, owner) + require.Len(t, got, 64) + + want := make([]byte, 64) + copy(want[32:42], name[:]) + copy(want[42:62], owner[:]) + require.Equal(t, want, got) + + require.Equal(t, make([]byte, 32), got[0:32], "workflow_cid bytes must be zero") + require.Equal(t, name[:], got[32:42], "workflow_name occupies [32:42)") + require.Equal(t, owner[:], got[42:62], "workflow_owner occupies [42:62)") + require.Equal(t, []byte{0, 0}, got[62:64], "report_id bytes must be zero") +} + +// TestBuildReportScMapKeyOrder pins that buildReport's ScMap struct fields +// decode back in alphabetical key order (answer < data_id < timestamp), per +// scval.BuildStructScVal's sort.Strings over field names. +func TestBuildReportScMapKeyOrder(t *testing.T) { + var wireHi16 [16]byte + for i := range wireHi16 { + wireHi16[i] = byte(i + 1) + } + answer := big.NewInt(123456789) + const ts = uint64(1700000000) + + b := buildReport(t, wireHi16, answer, ts) + + var decoded xdr.ScVal + require.NoError(t, decoded.UnmarshalBinary(b)) + require.Equal(t, xdr.ScValTypeScvVec, decoded.Type) + require.NotNil(t, decoded.Vec) + vec := *decoded.Vec + require.Len(t, *vec, 1) + + entry := (*vec)[0] + require.Equal(t, xdr.ScValTypeScvMap, entry.Type) + require.NotNil(t, entry.Map) + scMap := *entry.Map + require.Len(t, *scMap, 3) + + keys := make([]string, len(*scMap)) + for i, e := range *scMap { + require.Equal(t, xdr.ScValTypeScvSymbol, e.Key.Type) + require.NotNil(t, e.Key.Sym) + keys[i] = string(*e.Key.Sym) + } + require.Equal(t, []string{"answer", "data_id", "timestamp"}, keys) +} diff --git a/deployment/data-feeds/changeset/stellar/set_proxy_cache.go b/deployment/data-feeds/changeset/stellar/set_proxy_cache.go new file mode 100644 index 00000000000..d57db3a5732 --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/set_proxy_cache.go @@ -0,0 +1,66 @@ +package stellar + +import ( + "fmt" + + "github.com/Masterminds/semver/v3" + + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" + + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/stellar/operation" +) + +// SetProxyCacheRequest points an already-deployed DataFeedsProxy contract at a +// (possibly different) cache contract. Qualifier resolves the proxy being +// acted on (ChainSel, ProxyContract, Version, Qualifier); CacheQualifier +// resolves the new cache target (ChainSel, CacheContract, Version, +// CacheQualifier) — both lookups share this request's Version: per this +// suite's convention, cross-contract lookups use the acting changeset's +// datastore Version, so a cache recorded under a different version needs a +// matching-version record (an optional CacheVersion field is a recorded +// follow-up if operators need split versions). +type SetProxyCacheRequest struct { + ChainSel uint64 + Qualifier string + Version string + CacheQualifier string +} + +var _ cldf.ChangeSetV2[*SetProxyCacheRequest] = SetProxyCache{} + +// SetProxyCache points the proxy at a cache contract. +type SetProxyCache struct{} + +func (SetProxyCache) VerifyPreconditions(env cldf.Environment, req *SetProxyCacheRequest) error { + if err := verifyContractRef(env, req.ChainSel, ProxyContract, req.Qualifier, req.Version); err != nil { + return err + } + if err := verifyContractRef(env, req.ChainSel, CacheContract, req.CacheQualifier, req.Version); err != nil { + return err + } + return nil +} + +func (SetProxyCache) Apply(env cldf.Environment, req *SetProxyCacheRequest) (cldf.ChangesetOutput, error) { + var out cldf.ChangesetOutput + version := semver.MustParse(req.Version) + + proxyDeps, _, err := resolveContractDeps(env, req.ChainSel, ProxyContract, req.Qualifier, version) + if err != nil { + return out, err + } + cacheRef, err := env.DataStore.Addresses().Get( + datastore.NewAddressRefKey(req.ChainSel, CacheContract, version, req.CacheQualifier), + ) + if err != nil { + return out, fmt.Errorf("cache address ref not found for qualifier %q: %w", req.CacheQualifier, err) + } + + _, err = operations.ExecuteOperation(env.OperationsBundle, operation.SetProxyCache, proxyDeps.deps, operation.SetProxyCacheInput{ + ContractID: proxyDeps.contractID, + Cache: cacheRef.Address, + }) + return out, err +} diff --git a/deployment/data-feeds/changeset/stellar/state.go b/deployment/data-feeds/changeset/stellar/state.go new file mode 100644 index 00000000000..461655f4d83 --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/state.go @@ -0,0 +1,45 @@ +package stellar + +import ( + "fmt" + + "github.com/Masterminds/semver/v3" + + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + cache "github.com/smartcontractkit/chainlink-stellar/bindings/contracts/data_feeds_cache" + proxy "github.com/smartcontractkit/chainlink-stellar/bindings/contracts/data_feeds_proxy" +) + +// loadContractClientDeps resolves the AddressRef for (chainSel, contractType, +// version, qualifier) and the chain's deploy/invoke deps (via +// resolveContractDeps, which chain ok-guards before touching the datastore +// or building deps). It is the shared lookup behind LoadCacheClient and +// LoadProxyClient. +func loadContractClientDeps(env cldf.Environment, chainSel uint64, contractType datastore.ContractType, qualifier, version string) (stellarApplyDeps, datastore.AddressRef, error) { + v, err := semver.NewVersion(version) + if err != nil { + return stellarApplyDeps{}, datastore.AddressRef{}, fmt.Errorf("invalid version %q: %w", version, err) + } + return resolveContractDeps(env, chainSel, contractType, qualifier, v) +} + +// LoadCacheClient resolves the CacheContract AddressRef from env.DataStore +// for (chainSel, qualifier, version) and returns a generated cache client +// bound to its contract ID, along with the resolved AddressRef. +func LoadCacheClient(env cldf.Environment, chainSel uint64, qualifier, version string) (*cache.DataFeedsCacheClient, datastore.AddressRef, error) { + d, ref, err := loadContractClientDeps(env, chainSel, CacheContract, qualifier, version) + if err != nil { + return nil, datastore.AddressRef{}, err + } + return cache.NewDataFeedsCacheClient(d.deps.Invoker, d.contractID), ref, nil +} + +// LoadProxyClient is LoadCacheClient's counterpart for ProxyContract. +func LoadProxyClient(env cldf.Environment, chainSel uint64, qualifier, version string) (*proxy.DataFeedsProxyClient, datastore.AddressRef, error) { + d, ref, err := loadContractClientDeps(env, chainSel, ProxyContract, qualifier, version) + if err != nil { + return nil, datastore.AddressRef{}, err + } + return proxy.NewDataFeedsProxyClient(d.deps.Invoker, d.contractID), ref, nil +} diff --git a/deployment/data-feeds/changeset/stellar/state_test.go b/deployment/data-feeds/changeset/stellar/state_test.go new file mode 100644 index 00000000000..219480f4602 --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/state_test.go @@ -0,0 +1,61 @@ +package stellar + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestLoadCacheClient(t *testing.T) { + env, _, _ := newTestEnv(t) + seedCacheRef(t, &env, testContractID, "test-cache", "1.0.0") + + client, ref, err := LoadCacheClient(env, testChainSel, "test-cache", "1.0.0") + require.NoError(t, err) + require.NotNil(t, client) + require.Equal(t, testContractID, ref.Address) + require.Equal(t, testContractID, client.ContractID()) +} + +func TestLoadCacheClient_MissingRef(t *testing.T) { + env, _, _ := newTestEnv(t) + seedCacheRef(t, &env, testContractID, "test-cache", "1.0.0") + + _, _, err := LoadCacheClient(env, testChainSel, "does-not-exist", "1.0.0") + require.Error(t, err) +} + +func TestLoadCacheClient_UnknownChain(t *testing.T) { + env, _, _ := newTestEnv(t) + seedCacheRef(t, &env, testContractID, "test-cache", "1.0.0") + + _, _, err := LoadCacheClient(env, 999999, "test-cache", "1.0.0") + require.Error(t, err) +} + +func TestLoadProxyClient(t *testing.T) { + env, _, _ := newTestEnv(t) + seedProxyRef(t, &env, testProxyAddress, "test-proxy", "1.0.0") + + client, ref, err := LoadProxyClient(env, testChainSel, "test-proxy", "1.0.0") + require.NoError(t, err) + require.NotNil(t, client) + require.Equal(t, testProxyAddress, ref.Address) + require.Equal(t, testProxyAddress, client.ContractID()) +} + +func TestLoadProxyClient_MissingRef(t *testing.T) { + env, _, _ := newTestEnv(t) + seedProxyRef(t, &env, testProxyAddress, "test-proxy", "1.0.0") + + _, _, err := LoadProxyClient(env, testChainSel, "does-not-exist", "1.0.0") + require.Error(t, err) +} + +func TestLoadProxyClient_UnknownChain(t *testing.T) { + env, _, _ := newTestEnv(t) + seedProxyRef(t, &env, testProxyAddress, "test-proxy", "1.0.0") + + _, _, err := LoadProxyClient(env, 999999, "test-proxy", "1.0.0") + require.Error(t, err) +} diff --git a/deployment/data-feeds/changeset/stellar/testdata/.gitignore b/deployment/data-feeds/changeset/stellar/testdata/.gitignore new file mode 100644 index 00000000000..391b5033818 --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/testdata/.gitignore @@ -0,0 +1,5 @@ +# The repo-root .gitignore ignores *.wasm as build artifacts. These two files are +# committed test fixtures (the e2e test deploys them to a local Soroban devnet), +# so re-include them here. See README.md for regeneration. +!data_feeds_cache.wasm +!data_feeds_proxy.wasm diff --git a/deployment/data-feeds/changeset/stellar/testdata/README.md b/deployment/data-feeds/changeset/stellar/testdata/README.md new file mode 100644 index 00000000000..9da7362e433 --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/testdata/README.md @@ -0,0 +1,32 @@ +# Stellar Data Feeds e2e test fixtures + +`data_feeds_cache.wasm` and `data_feeds_proxy.wasm` are the compiled Soroban +contracts under test by `e2e_test.go` (`TestStellarDataFeedsE2E`). They are the +byte-for-byte artifacts of `stellar contract build` on chainlink-stellar's +`contracts/data-feeds` workspace at PR #161/#162 content (Bound with explicit +discriminants; literal BytesN ABI). + +sha256: + +- `data_feeds_cache.wasm` = `e96c7dc85c4cf37acf8acf6bbefd85d5e888d9a9a5342e39151d6f0323c70cbc` +- `data_feeds_proxy.wasm` = `e3f4a4ae333d26218bdb7aca0870623d06bdc2a65ee40440c797cfedd3e456f1` + +## Regeneration + +These are committed binaries (the e2e test needs a real WASM to deploy to a +local CTF Soroban devnet). To regenerate after a contract change, rebuild from +the contract source repo and copy the artifacts back: + +```sh +cd /contracts/data-feeds +stellar contract build # stellar CLI 27.0.0 +cp target/wasm32v1-none/release/data_feeds_cache.wasm / +cp target/wasm32v1-none/release/data_feeds_proxy.wasm / +``` + +(Older stellar CLI versions output to `target/wasm32-unknown-unknown/release/` +instead — check which target triple your `stellar contract build` uses.) + +If the contract ABI changes, regenerate the Go bindings +(`chainlink-stellar/bindings/contracts/data_feeds_{cache,proxy}`) as well, or the +e2e test will fail to compile / decode. diff --git a/deployment/data-feeds/changeset/stellar/testdata/data_feeds_cache.wasm b/deployment/data-feeds/changeset/stellar/testdata/data_feeds_cache.wasm new file mode 100644 index 0000000000000000000000000000000000000000..cc53ad56d1bac6b3744db8f05cc9f2a29d452d9c GIT binary patch literal 21500 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�NRPz6VFk9fLZyyQ1dR+dlBD+|19>pzWX$& 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 H(bxU|6yro| literal 0 HcmV?d00001 diff --git a/deployment/data-feeds/changeset/stellar/testdata/data_feeds_proxy.wasm b/deployment/data-feeds/changeset/stellar/testdata/data_feeds_proxy.wasm new file mode 100644 index 0000000000000000000000000000000000000000..367e0d4bbe20cc48d1ff234e911feea1fb9ca5de GIT binary patch literal 9463 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 Tk8Sh+S0YlFWPHKf$yfY80MSFM literal 0 HcmV?d00001 diff --git a/deployment/data-feeds/changeset/stellar/testutil_test.go b/deployment/data-feeds/changeset/stellar/testutil_test.go new file mode 100644 index 00000000000..a01380251da --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/testutil_test.go @@ -0,0 +1,61 @@ +package stellar + +import ( + "context" + + protocolrpc "github.com/stellar/go-stellar-sdk/protocols/rpc" + "github.com/stellar/go-stellar-sdk/xdr" +) + +type invocation struct { + ContractID string + Function string + Args []xdr.ScVal +} + +type fakeInvoker struct { + calls []invocation +} + +func (f *fakeInvoker) InvokeContract(_ context.Context, contractID, fn string, args []xdr.ScVal) (*xdr.ScVal, error) { + f.calls = append(f.calls, invocation{contractID, fn, args}) + v := xdr.ScVal{Type: xdr.ScValTypeScvVoid} + return &v, nil +} + +func (f *fakeInvoker) SimulateContract(_ context.Context, contractID, fn string, args []xdr.ScVal) (*xdr.ScVal, error) { + f.calls = append(f.calls, invocation{contractID, fn, args}) + v := xdr.ScVal{Type: xdr.ScValTypeScvVoid} + return &v, nil +} + +func (f *fakeInvoker) GetEvents(_ context.Context, _ string, _ uint32, _ []string) ([]protocolrpc.EventInfo, error) { + return nil, nil +} + +type deployCall struct { + WasmPath string + Salt [32]byte + Args []xdr.ScVal +} + +type fakeDeployer struct { + deploys []deployCall + contractID string + wasmHash xdr.Hash + uploads []string // wasm paths passed to UploadContractWASM, in call order +} + +func (f *fakeDeployer) DeployContract(ctx context.Context, wasmPath string, salt [32]byte) (string, error) { + return f.DeployContractWithArgs(ctx, wasmPath, salt, nil) +} + +func (f *fakeDeployer) DeployContractWithArgs(_ context.Context, wasmPath string, salt [32]byte, args []xdr.ScVal) (string, error) { + f.deploys = append(f.deploys, deployCall{wasmPath, salt, args}) + return f.contractID, nil +} + +func (f *fakeDeployer) UploadContractWASM(_ context.Context, wasmPath string) (xdr.Hash, error) { + f.uploads = append(f.uploads, wasmPath) + return f.wasmHash, nil +} diff --git a/deployment/data-feeds/changeset/stellar/upgrade_cache.go b/deployment/data-feeds/changeset/stellar/upgrade_cache.go new file mode 100644 index 00000000000..59ba101ab1a --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/upgrade_cache.go @@ -0,0 +1,61 @@ +package stellar + +import ( + "fmt" + "os" + + "github.com/Masterminds/semver/v3" + + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" + + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/stellar/operation" +) + +// UpgradeCacheRequest points an already-deployed DataFeedsCache contract at a +// new WASM implementation. +type UpgradeCacheRequest struct { + ChainSel uint64 + Qualifier string + Version string + WasmPath string +} + +var _ cldf.ChangeSetV2[*UpgradeCacheRequest] = UpgradeCache{} + +// UpgradeCache uploads a new WASM blob and points the cache at it. Apply +// chains two operations: UploadWASM produces the code hash that UpgradeCache +// then applies on-chain. +type UpgradeCache struct{} + +func (UpgradeCache) VerifyPreconditions(env cldf.Environment, req *UpgradeCacheRequest) error { + if err := verifyContractRef(env, req.ChainSel, CacheContract, req.Qualifier, req.Version); err != nil { + return err + } + if _, err := os.Stat(req.WasmPath); err != nil { + return fmt.Errorf("wasm path: %w", err) + } + return nil +} + +func (UpgradeCache) Apply(env cldf.Environment, req *UpgradeCacheRequest) (cldf.ChangesetOutput, error) { + var out cldf.ChangesetOutput + version := semver.MustParse(req.Version) + d, _, err := resolveContractDeps(env, req.ChainSel, CacheContract, req.Qualifier, version) + if err != nil { + return out, err + } + + uploadReport, err := operations.ExecuteOperation(env.OperationsBundle, operation.UploadWASM, d.deps, operation.UploadWASMInput{ + WasmPath: req.WasmPath, + }) + if err != nil { + return out, err + } + + _, err = operations.ExecuteOperation(env.OperationsBundle, operation.UpgradeCache, d.deps, operation.UpgradeCacheInput{ + ContractID: d.contractID, + NewWasmHash: uploadReport.Output.WasmHash, + }) + return out, err +} diff --git a/deployment/data-feeds/changeset/stellar/validation.go b/deployment/data-feeds/changeset/stellar/validation.go new file mode 100644 index 00000000000..e3893a83887 --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/validation.go @@ -0,0 +1,66 @@ +package stellar + +import ( + "encoding/hex" + "fmt" + "math/big" + "strings" + + "github.com/stellar/go-stellar-sdk/strkey" +) + +// validateAddress accepts a Stellar account (G...) or contract (C...) strkey. +func validateAddress(s string) error { + if strkey.IsValidEd25519PublicKey(s) { + return nil + } + if _, err := strkey.Decode(strkey.VersionByteContract, s); err == nil { + return nil + } + return fmt.Errorf("%q is not a valid Stellar account or contract address", s) +} + +// dataIDsToBytes converts 0x-prefixed hex feed IDs to 16-byte arrays, +// left-justified with trailing zero padding — byte-identical to the Solana +// suite's dataIDtoBytes (data IDs are canonically left-aligned, e.g. +// 0x018e16c39e00032000000000000000000 shorthand drops trailing zeros). +func dataIDsToBytes(ids []string) ([][16]byte, error) { + out := make([][16]byte, 0, len(ids)) + for _, id := range ids { + v, ok := new(big.Int).SetString(id, 0) + if !ok { + return nil, fmt.Errorf("invalid data_id: %q", id) + } + if v.BitLen() > 128 { + return nil, fmt.Errorf("data_id too long: %q (%d bits)", id, v.BitLen()) + } + var b [16]byte + copy(b[:], v.Bytes()) + out = append(out, b) + } + return out, nil +} + +// workflowNameToBytes right-pads an ASCII workflow name into [10]byte. +func workflowNameToBytes(s string) ([10]byte, error) { + var out [10]byte + if len(s) > len(out) { + return out, fmt.Errorf("workflow name %q exceeds %d bytes", s, len(out)) + } + copy(out[:], s) + return out, nil +} + +// workflowOwnerToBytes decodes a 20-byte 0x-prefixed hex workflow owner. +func workflowOwnerToBytes(hexStr string) ([20]byte, error) { + var out [20]byte + b, err := hex.DecodeString(strings.TrimPrefix(hexStr, "0x")) + if err != nil { + return out, fmt.Errorf("invalid workflow owner %q: %w", hexStr, err) + } + if len(b) != len(out) { + return out, fmt.Errorf("workflow owner must be %d bytes, got %d", len(out), len(b)) + } + copy(out[:], b) + return out, nil +} diff --git a/deployment/data-feeds/changeset/stellar/validation_test.go b/deployment/data-feeds/changeset/stellar/validation_test.go new file mode 100644 index 00000000000..972f911e8ac --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/validation_test.go @@ -0,0 +1,75 @@ +package stellar + +import ( + "encoding/hex" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +// mustHex16 decodes s (<=32 hex chars) and left-justifies it into a [16]byte, +// matching the layout dataIDsToBytes is expected to produce. +func mustHex16(t *testing.T, s string) [16]byte { + t.Helper() + b, err := hex.DecodeString(s) + require.NoError(t, err) + require.LessOrEqual(t, len(b), 16) + var out [16]byte + copy(out[:], b) + return out +} + +func TestValidateAddress(t *testing.T) { + require.NoError(t, validateAddress("GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN7")) // account + require.NoError(t, validateAddress("CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA")) // contract + require.Error(t, validateAddress("not-a-key")) + require.Error(t, validateAddress("")) +} + +func TestDataIDsToBytes(t *testing.T) { + // Full-width (exactly 16-byte) input: exact round-trip check. NOTE: for a + // value this wide, left-justified copy(b[:], v.Bytes()) and + // right-justified v.FillBytes(b[:]) produce byte-identical output, so + // this case alone cannot catch a regression to FillBytes — see the short + // case below for that. + got, err := dataIDsToBytes([]string{"0x018e16c39e0003200000000000000000"}) + require.NoError(t, err) + require.Len(t, got, 1) + require.Equal(t, mustHex16(t, "018e16c39e0003200000000000000000"), got[0]) + + // Short (sub-16-byte) shorthand input: this is the discriminating case. + // Left-justified (copy), the nonzero prefix "18e16c39e0003200000" lands in + // the LEADING bytes b[0:10] with zero padding trailing at b[10:16]: + // 18 e1 6c 39 e0 00 32 00 00 00 00 00 00 00 00 00 + // Right-justified (FillBytes) would instead push the same nonzero bytes + // down to END at b[15], i.e.: + // 00 00 00 00 00 00 18 e1 6c 39 e0 00 32 00 00 00 + got, err = dataIDsToBytes([]string{"0x018e16c39e00032000000"}) + require.NoError(t, err) + require.Len(t, got, 1) + require.Equal(t, byte(0x18), got[0][0]) + require.Equal(t, mustHex16(t, "18e16c39e00032000000000000000000"), got[0]) + + // >128-bit input (17 bytes) must be rejected. + _, err = dataIDsToBytes([]string{"0x01" + strings.Repeat("00", 16)}) + require.Error(t, err) + + // Invalid hex string must be rejected. + _, err = dataIDsToBytes([]string{"zzz"}) + require.Error(t, err) +} + +func TestWorkflowFieldConversions(t *testing.T) { + n, err := workflowNameToBytes("abc") + require.NoError(t, err) + require.Equal(t, byte('a'), n[0]) + _, err = workflowNameToBytes("elevenchars!") + require.Error(t, err) + + o, err := workflowOwnerToBytes("0x0102030405060708090a0b0c0d0e0f1011121314") + require.NoError(t, err) + require.Equal(t, byte(0x14), o[19]) + _, err = workflowOwnerToBytes("0x01") + require.Error(t, err) +} diff --git a/deployment/go.mod b/deployment/go.mod index 15032859439..f9882e0d3a0 100644 --- a/deployment/go.mod +++ b/deployment/go.mod @@ -60,7 +60,7 @@ require ( github.com/smartcontractkit/chainlink-protos/orchestrator v0.11.0 github.com/smartcontractkit/chainlink-solana v1.3.1-0.20260605202330-b5a89c32fdc1 github.com/smartcontractkit/chainlink-solana/contracts v0.0.0-20260513123719-d347eaf314e1 - github.com/smartcontractkit/chainlink-stellar v0.0.3-0.20260721074545-ef8526aebfcf + github.com/smartcontractkit/chainlink-stellar v0.0.3-0.20260723224154-a5c793895953 github.com/smartcontractkit/chainlink-sui v0.0.0-20260714190119-005bb9a612c3 github.com/smartcontractkit/chainlink-sui/codec v0.0.0-20260720132736-e99278bfdc96 github.com/smartcontractkit/chainlink-sui/deployment v0.0.0-20260714190119-005bb9a612c3 @@ -93,6 +93,21 @@ require ( gotest.tools/v3 v3.5.2 ) +require ( + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect +) + require ( cloud.google.com/go/compute/metadata v0.9.0 // indirect cosmossdk.io/api v0.7.6 // indirect @@ -260,17 +275,6 @@ require ( github.com/go-openapi/jsonpointer v0.22.1 // indirect github.com/go-openapi/jsonreference v0.21.3 // indirect github.com/go-openapi/swag v0.25.4 // indirect - github.com/go-openapi/swag/cmdutils v0.25.4 // indirect - github.com/go-openapi/swag/conv v0.25.4 // indirect - github.com/go-openapi/swag/fileutils v0.25.4 // indirect - github.com/go-openapi/swag/jsonname v0.25.4 // indirect - github.com/go-openapi/swag/jsonutils v0.25.4 // indirect - github.com/go-openapi/swag/loading v0.25.4 // indirect - github.com/go-openapi/swag/mangling v0.25.4 // indirect - github.com/go-openapi/swag/netutils v0.25.4 // indirect - github.com/go-openapi/swag/stringutils v0.25.4 // indirect - github.com/go-openapi/swag/typeutils v0.25.4 // indirect - github.com/go-openapi/swag/yamlutils v0.25.4 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.30.3 // indirect @@ -458,7 +462,7 @@ require ( github.com/smartcontractkit/chainlink-protos/storage-service v0.3.0 // indirect github.com/smartcontractkit/chainlink-protos/svr v1.3.0 // indirect github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260716141634-c0cc05ed05d8 // indirect - github.com/smartcontractkit/chainlink-stellar/bindings v0.0.0-20260721074545-ef8526aebfcf // indirect + github.com/smartcontractkit/chainlink-stellar/bindings v0.0.0-20260723224154-a5c793895953 github.com/smartcontractkit/chainlink-testing-framework/parrot v0.6.2 // indirect github.com/smartcontractkit/chainlink-testing-framework/seth v1.51.5 // indirect github.com/smartcontractkit/chainlink-ton/cciplib v0.1.1-0.20260716214810-db5ecc877490 // indirect @@ -472,7 +476,7 @@ require ( github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/viper v1.21.0 // indirect - github.com/stellar/go-stellar-sdk v0.5.0 // indirect + github.com/stellar/go-stellar-sdk v0.5.0 github.com/stellar/go-xdr v0.0.0-20260423131911-a87d4d0789c3 // indirect github.com/stephenlacy/go-ethereum-hdwallet v0.0.0-20230913225845-a4fa94429863 // indirect github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect @@ -571,7 +575,6 @@ require ( sigs.k8s.io/kustomize/api v0.17.2 // indirect sigs.k8s.io/kustomize/kyaml v0.17.1 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/deployment/go.sum b/deployment/go.sum index 6bf68057548..bd25cfb028e 100644 --- a/deployment/go.sum +++ b/deployment/go.sum @@ -1480,10 +1480,10 @@ github.com/smartcontractkit/chainlink-solana v1.3.1-0.20260605202330-b5a89c32fdc github.com/smartcontractkit/chainlink-solana v1.3.1-0.20260605202330-b5a89c32fdc1/go.mod h1:wi1QdXqhSJnADt9YRaRtEWomqknLcrdkTS0JotupuOQ= github.com/smartcontractkit/chainlink-solana/contracts v0.0.0-20260513123719-d347eaf314e1 h1:/xvuNFI7DwOoTQnmAdYPDdY+sConn3RgZ2rMy/8AXlo= github.com/smartcontractkit/chainlink-solana/contracts v0.0.0-20260513123719-d347eaf314e1/go.mod h1:lQK+YvR9Ox0ft72k0se7DlA+kujVWyjFQXG3DLbEZ/4= -github.com/smartcontractkit/chainlink-stellar v0.0.3-0.20260721074545-ef8526aebfcf h1:AUZU1Lnnf7aOh9G5t+Sk0pjQklsT+QMg/an0Quhglns= -github.com/smartcontractkit/chainlink-stellar v0.0.3-0.20260721074545-ef8526aebfcf/go.mod h1:J8D0u8K/a2+3YuJx0F8nf4Fd7h1UZqeWGYszp7aCbdA= -github.com/smartcontractkit/chainlink-stellar/bindings v0.0.0-20260721074545-ef8526aebfcf h1:K0og2/DvEL1/rOc6TF4RuhHJ1fUEGdrZ2NMdYRvB/S8= -github.com/smartcontractkit/chainlink-stellar/bindings v0.0.0-20260721074545-ef8526aebfcf/go.mod h1:uZEMU0BN48tmRP/Hu+RdnM70g17pgAv4s/Nux7asr2U= +github.com/smartcontractkit/chainlink-stellar v0.0.3-0.20260723224154-a5c793895953 h1:n/Q0hHFQRxYEalqDEeg01osPd6SYVdrwSbu7Kp0VBdA= +github.com/smartcontractkit/chainlink-stellar v0.0.3-0.20260723224154-a5c793895953/go.mod h1:J8D0u8K/a2+3YuJx0F8nf4Fd7h1UZqeWGYszp7aCbdA= +github.com/smartcontractkit/chainlink-stellar/bindings v0.0.0-20260723224154-a5c793895953 h1:vMQkCPE3ClKukommFC52jKTwPwI5WbM8vo+DMvzG39Q= +github.com/smartcontractkit/chainlink-stellar/bindings v0.0.0-20260723224154-a5c793895953/go.mod h1:IYBZArxwOQRlcx0Udx/VfFjqJpAs4NrjKsBoNITKAMg= github.com/smartcontractkit/chainlink-sui v0.0.0-20260714190119-005bb9a612c3 h1:bI9pXgfGOhS+tPGjPkpos3nuGE/5Xfw8X0MQHi+0EcU= github.com/smartcontractkit/chainlink-sui v0.0.0-20260714190119-005bb9a612c3/go.mod h1:6FWUSAXA58d0c9AyOi/1zymX40/67czcDR1SGZt/BKg= github.com/smartcontractkit/chainlink-sui/codec v0.0.0-20260720132736-e99278bfdc96 h1:3R9f6pguDIjei+nKx5dUT9j6fX8urGmlZA7Ad3x9UBs= From 96d92200e080f22ac04be573dafff7690dccab1d Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Tue, 4 Aug 2026 19:27:29 +0100 Subject: [PATCH 02/19] Complete changeset coverage for reworked bindings - Bump chainlink-stellar pins to the reworked PR #162 head - Rebuild testdata WASM from the same contracts (matched pair) - Add SetFeedFrozen; generalize Upgrade and RecoverTokens to the proxy - Extend the e2e to all 26 cache and 14 proxy functions with optional decimals/description and FeedFrozen assertions - Trim comments to the Solana-suite register --- .../data-feeds/changeset/stellar/README.md | 17 ++- .../changeset/stellar/changeset_test.go | 92 +++++++++++-- .../changeset/stellar/configure_cache.go | 8 +- .../changeset/stellar/deploy_cache.go | 4 +- .../changeset/stellar/deploy_proxy.go | 13 +- .../data-feeds/changeset/stellar/deps.go | 17 +-- .../data-feeds/changeset/stellar/e2e_test.go | 127 +++++++++++++----- .../changeset/stellar/freeze_feeds.go | 67 +++++++++ .../changeset/stellar/operation/operation.go | 44 ++++-- .../changeset/stellar/recover_tokens.go | 14 +- .../changeset/stellar/set_proxy_cache.go | 9 +- .../data-feeds/changeset/stellar/state.go | 12 +- .../changeset/stellar/testdata/README.md | 9 +- .../stellar/testdata/data_feeds_cache.wasm | Bin 21500 -> 23100 bytes .../stellar/testdata/data_feeds_proxy.wasm | Bin 9463 -> 9503 bytes .../data-feeds/changeset/stellar/upgrade.go | 67 +++++++++ .../changeset/stellar/upgrade_cache.go | 61 --------- .../changeset/stellar/validation.go | 6 +- deployment/go.mod | 4 +- deployment/go.sum | 8 +- 20 files changed, 402 insertions(+), 177 deletions(-) create mode 100644 deployment/data-feeds/changeset/stellar/freeze_feeds.go create mode 100644 deployment/data-feeds/changeset/stellar/upgrade.go delete mode 100644 deployment/data-feeds/changeset/stellar/upgrade_cache.go diff --git a/deployment/data-feeds/changeset/stellar/README.md b/deployment/data-feeds/changeset/stellar/README.md index a11683b7e74..6ef8ea26b1a 100644 --- a/deployment/data-feeds/changeset/stellar/README.md +++ b/deployment/data-feeds/changeset/stellar/README.md @@ -12,9 +12,11 @@ are EVM/JD-specific and deliberately have no Stellar equivalent here). data-feeds/changeset/stellar/ ├── deploy_cache.go, deploy_proxy.go # instantiate contracts, record AddressRefs ├── configure_cache.go # SetFeedConfigs — descriptions + permissions -├── remove_feeds.go, feed_admin.go # remove feed configs, grant/revoke feed-admin +├── remove_feeds.go, freeze_feeds.go # remove feed configs, freeze/unfreeze feeds +├── feed_admin.go # grant/revoke feed-admin ├── ownership.go # transfer / accept / renounce (cache + proxy) -├── upgrade_cache.go, recover_tokens.go, set_proxy_cache.go +├── upgrade.go, recover_tokens.go # new WASM / token recovery (cache + proxy) +├── set_proxy_cache.go # repoint the proxy at a cache ├── deps.go, state.go # shared resolve-ref/build-deps + client loaders ├── validation.go, config.go # input validation, encoding helpers ├── operation/operation.go # one CLDF operation per on-chain call @@ -77,11 +79,12 @@ cp target/wasm32v1-none/release/data_feeds_{cache,proxy}.wasm \ #### Running the e2e test `TestStellarDataFeedsE2E` boots a local CTF Soroban devnet, deploys both -contracts, and asserts 35 exported function calls succeed plus 2 -(`recover_tokens` on each contract) fail with the documented `InvalidAction` -host error (self-transfer re-entry — see the test's `recover_tokens` -comments). It needs Docker and does not run in CI (`skipInCI` skips when -`CI=true`). +contracts, and invokes all 40 exported functions (26 cache + 14 proxy): 38 +succeed on-chain, and `recover_tokens` on each contract fails with the +documented `InvalidAction` host error (self-transfer re-entry — see the test's +`recover_tokens` comments). Reads on a frozen feed are additionally asserted +to fail with the cache's `FeedFrozen` error. It needs Docker and does not run +in CI (`skipInCI` skips when `CI=true`). ```sh go test ./data-feeds/changeset/stellar/ -run TestStellarDataFeedsE2E -v -timeout 25m diff --git a/deployment/data-feeds/changeset/stellar/changeset_test.go b/deployment/data-feeds/changeset/stellar/changeset_test.go index d5976d410bf..71450da9715 100644 --- a/deployment/data-feeds/changeset/stellar/changeset_test.go +++ b/deployment/data-feeds/changeset/stellar/changeset_test.go @@ -361,6 +361,42 @@ func TestRemoveFeedConfigsChangeset(t *testing.T) { require.Error(t, RemoveFeedConfigs{}.VerifyPreconditions(env, &badQualifier)) } +func TestSetFeedFrozenChangeset(t *testing.T) { + env, inv, _ := newTestEnv(t) + seedCacheRef(t, &env, testContractID, "test", "1.0.0") + + req := &SetFeedFrozenRequest{ + ChainSel: testChainSel, + Qualifier: "test", + Version: "1.0.0", + Admin: testAdmin, + DataIDs: []string{"0x018e16c39e00032000000"}, + Frozen: true, + } + require.NoError(t, SetFeedFrozen{}.VerifyPreconditions(env, req)) + + _, err := SetFeedFrozen{}.Apply(env, req) + require.NoError(t, err) + require.Len(t, inv.calls, 1) + require.Equal(t, "set_feed_frozen", inv.calls[0].Function) + require.Equal(t, testContractID, inv.calls[0].ContractID) + + // empty DataIDs must fail preconditions + empty := *req + empty.DataIDs = nil + require.Error(t, SetFeedFrozen{}.VerifyPreconditions(env, &empty)) + + // invalid admin address must fail preconditions + badAdmin := *req + badAdmin.Admin = "not-a-key" + require.Error(t, SetFeedFrozen{}.VerifyPreconditions(env, &badAdmin)) + + // missing cache ref must fail preconditions + badQualifier := *req + badQualifier.Qualifier = "does-not-exist" + require.Error(t, SetFeedFrozen{}.VerifyPreconditions(env, &badQualifier)) +} + func TestFeedAdminChangesets(t *testing.T) { env, inv, _ := newTestEnv(t) seedCacheRef(t, &env, testContractID, "test", "1.0.0") @@ -495,9 +531,12 @@ func TestOwnershipChangesets(t *testing.T) { require.Error(t, TransferOwnership{}.VerifyPreconditions(env, &badQualifier)) } -func TestUpgradeCacheChangeset(t *testing.T) { +func TestUpgradeChangeset(t *testing.T) { env, inv, dep := newTestEnv(t) - seedCacheRef(t, &env, testContractID, "test", "1.0.0") + seedContractRefs(t, &env, + contractRefSpec{CacheContract, testContractID, "test", "1.0.0"}, + contractRefSpec{ProxyContract, testProxyAddress, "test-proxy", "1.0.0"}, + ) var wantHash xdr.Hash for i := range wantHash { @@ -505,15 +544,16 @@ func TestUpgradeCacheChangeset(t *testing.T) { } dep.wasmHash = wantHash - req := &UpgradeCacheRequest{ + req := &UpgradeRequest{ ChainSel: testChainSel, Qualifier: "test", Version: "1.0.0", + Contract: CacheContract, WasmPath: writeDummyWasm(t, "new_cache.wasm"), } - require.NoError(t, UpgradeCache{}.VerifyPreconditions(env, req)) + require.NoError(t, Upgrade{}.VerifyPreconditions(env, req)) - _, err := UpgradeCache{}.Apply(env, req) + _, err := Upgrade{}.Apply(env, req) require.NoError(t, err) // the upload happened, against the requested wasm path @@ -530,25 +570,45 @@ func TestUpgradeCacheChangeset(t *testing.T) { require.NoError(t, err) require.Equal(t, [32]byte(wantHash), gotHash) + // proxy path targets the proxy ref + proxyReq := *req + proxyReq.Qualifier = "test-proxy" + proxyReq.Contract = ProxyContract + require.NoError(t, Upgrade{}.VerifyPreconditions(env, &proxyReq)) + _, err = Upgrade{}.Apply(env, &proxyReq) + require.NoError(t, err) + require.Len(t, inv.calls, 2) + require.Equal(t, "upgrade", inv.calls[1].Function) + require.Equal(t, testProxyAddress, inv.calls[1].ContractID) + + // unsupported contract type must fail preconditions + badContract := *req + badContract.Contract = "NotAContract" + require.Error(t, Upgrade{}.VerifyPreconditions(env, &badContract)) + // missing wasm path must fail preconditions badPath := *req badPath.WasmPath = "/does/not/exist.wasm" - require.Error(t, UpgradeCache{}.VerifyPreconditions(env, &badPath)) + require.Error(t, Upgrade{}.VerifyPreconditions(env, &badPath)) // missing cache ref must fail preconditions badQualifier := *req badQualifier.Qualifier = "does-not-exist" - require.Error(t, UpgradeCache{}.VerifyPreconditions(env, &badQualifier)) + require.Error(t, Upgrade{}.VerifyPreconditions(env, &badQualifier)) } func TestRecoverTokensChangeset(t *testing.T) { env, inv, _ := newTestEnv(t) - seedCacheRef(t, &env, testContractID, "test", "1.0.0") + seedContractRefs(t, &env, + contractRefSpec{CacheContract, testContractID, "test", "1.0.0"}, + contractRefSpec{ProxyContract, testProxyAddress, "test-proxy", "1.0.0"}, + ) req := &RecoverTokensRequest{ ChainSel: testChainSel, Qualifier: "test", Version: "1.0.0", + Contract: CacheContract, Token: testContractID, To: testAdmin, Amount: 1000, @@ -562,6 +622,22 @@ func TestRecoverTokensChangeset(t *testing.T) { require.Equal(t, testContractID, inv.calls[0].ContractID) require.Len(t, inv.calls[0].Args, 3) + // proxy path targets the proxy ref + proxyReq := *req + proxyReq.Qualifier = "test-proxy" + proxyReq.Contract = ProxyContract + require.NoError(t, RecoverTokens{}.VerifyPreconditions(env, &proxyReq)) + _, err = RecoverTokens{}.Apply(env, &proxyReq) + require.NoError(t, err) + require.Len(t, inv.calls, 2) + require.Equal(t, "recover_tokens", inv.calls[1].Function) + require.Equal(t, testProxyAddress, inv.calls[1].ContractID) + + // unsupported contract type must fail preconditions + badContract := *req + badContract.Contract = "NotAContract" + require.Error(t, RecoverTokens{}.VerifyPreconditions(env, &badContract)) + // bad token address must fail preconditions badToken := *req badToken.Token = "not-a-key" diff --git a/deployment/data-feeds/changeset/stellar/configure_cache.go b/deployment/data-feeds/changeset/stellar/configure_cache.go index 2fb4e45421c..21ca835588d 100644 --- a/deployment/data-feeds/changeset/stellar/configure_cache.go +++ b/deployment/data-feeds/changeset/stellar/configure_cache.go @@ -15,7 +15,7 @@ import ( // FeedPermission is the operator-facing shape of a workflow write-permission. // AllowedSender is the on-chain caller permitted to invoke on_report — the CRE -// forwarder contract. Mirrors EVM WorkflowMetadata{AllowedSender, ...}. +// forwarder contract. type FeedPermission struct { AllowedSender string // Stellar address (C... or G...) AllowedWorkflowOwner string // 20-byte hex @@ -23,7 +23,7 @@ type FeedPermission struct { } // SetFeedConfigsRequest sets descriptions + workflow permissions for a batch of -// feeds. Permissions apply to every feed in the batch (EVM-aligned semantics). +// feeds. Permissions apply to every feed in the batch. type SetFeedConfigsRequest struct { ChainSel uint64 Qualifier string @@ -36,8 +36,8 @@ type SetFeedConfigsRequest struct { var _ cldf.ChangeSetV2[*SetFeedConfigsRequest] = SetFeedConfigs{} -// SetFeedConfigs sets per-feed descriptions and shared workflow write-permissions -// on an already-deployed DataFeedsCache contract. +// SetFeedConfigs sets per-feed descriptions and workflow write-permissions on +// the cache. type SetFeedConfigs struct{} func (SetFeedConfigs) VerifyPreconditions(env cldf.Environment, req *SetFeedConfigsRequest) error { diff --git a/deployment/data-feeds/changeset/stellar/deploy_cache.go b/deployment/data-feeds/changeset/stellar/deploy_cache.go index b3456d02e70..647f72ed51b 100644 --- a/deployment/data-feeds/changeset/stellar/deploy_cache.go +++ b/deployment/data-feeds/changeset/stellar/deploy_cache.go @@ -14,9 +14,7 @@ import ( "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/stellar/operation" ) -// DeployCacheRequest configures a DataFeedsCache deployment. The real cache -// contract's __constructor(owner) takes a single argument — there is no -// RetentionTTLLedgers constructor input (that's a hardcoded on-chain constant). +// DeployCacheRequest configures a DataFeedsCache deployment. type DeployCacheRequest struct { ChainSel uint64 WasmPath string diff --git a/deployment/data-feeds/changeset/stellar/deploy_proxy.go b/deployment/data-feeds/changeset/stellar/deploy_proxy.go index dbae47623ac..12f108c86c6 100644 --- a/deployment/data-feeds/changeset/stellar/deploy_proxy.go +++ b/deployment/data-feeds/changeset/stellar/deploy_proxy.go @@ -14,15 +14,10 @@ import ( "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/stellar/operation" ) -// DeployProxyRequest configures a DataFeedsProxy deployment. The proxy's -// __constructor(owner, cache) resolves the cache address from the datastore -// by (ChainSel, CacheContract, Version, CacheQualifier) — the cache must -// already be deployed and recorded. The cache lookup reuses this request's -// own Version (there is no separate CacheVersion field): per this suite's -// convention, cross-contract lookups share the acting changeset's datastore -// Version, so a cache recorded under a different version needs a -// matching-version record (an optional CacheVersion field is a recorded -// follow-up if operators need split versions). +// DeployProxyRequest configures a DataFeedsProxy deployment. The cache passed +// to __constructor(owner, cache) is resolved from the datastore by (ChainSel, +// CacheContract, Version, CacheQualifier) — it must already be recorded. +// Cross-contract lookups share the request's Version (see README). type DeployProxyRequest struct { ChainSel uint64 WasmPath string diff --git a/deployment/data-feeds/changeset/stellar/deps.go b/deployment/data-feeds/changeset/stellar/deps.go index ab63c762f96..da8e6d48030 100644 --- a/deployment/data-feeds/changeset/stellar/deps.go +++ b/deployment/data-feeds/changeset/stellar/deps.go @@ -13,8 +13,8 @@ import ( "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/stellar/operation" ) -// newStellarDeps builds the deploy/invoke dependencies for a CLDF Stellar chain. -// Package-level var so unit tests can substitute fakes (the Invoker seam). +// newStellarDeps builds the deploy/invoke dependencies for a CLDF Stellar +// chain. Package-level var so unit tests can substitute fakes. var newStellarDeps = func(ch cldfstellar.Chain) (operation.StellarDeps, error) { d, err := stellardeploy.NewDeployerFromChain(ch) if err != nil { @@ -24,8 +24,7 @@ var newStellarDeps = func(ch cldfstellar.Chain) (operation.StellarDeps, error) { } // stellarApplyDeps bundles a resolved contract's address with the chain deps -// needed to invoke an operation against it — shared by every stellar -// changeset that acts on a single already-deployed contract. +// needed to invoke operations against it. type stellarApplyDeps struct { deps operation.StellarDeps contractID string @@ -33,9 +32,7 @@ type stellarApplyDeps struct { // verifyContractRef checks that chainSel names a known Stellar chain, // versionStr parses, and an AddressRef exists for (chainSel, contractType, -// version, qualifier). This is the chain/version/ref-existence skeleton -// shared by every changeset's VerifyPreconditions; only the extra checks -// differ. +// version, qualifier). func verifyContractRef(env cldf.Environment, chainSel uint64, contractType datastore.ContractType, qualifier, versionStr string) error { if _, ok := env.BlockChains.StellarChains()[chainSel]; !ok { return fmt.Errorf("stellar chain not found for chain selector %d", chainSel) @@ -53,11 +50,7 @@ func verifyContractRef(env cldf.Environment, chainSel uint64, contractType datas } // resolveContractDeps looks up the AddressRef for (chainSel, contractType, -// version, qualifier) and bundles it with the chain's deploy/invoke deps, -// returning the AddressRef itself alongside for callers that need more than -// just its Address (e.g. LoadCacheClient/LoadProxyClient). version must -// already be parsed (see semver.MustParse in callers that have validated -// req.Version in VerifyPreconditions, typically via verifyContractRef). +// version, qualifier) and bundles it with the chain's deploy/invoke deps. func resolveContractDeps(env cldf.Environment, chainSel uint64, contractType datastore.ContractType, qualifier string, version *semver.Version) (stellarApplyDeps, datastore.AddressRef, error) { var zero stellarApplyDeps var zeroRef datastore.AddressRef diff --git a/deployment/data-feeds/changeset/stellar/e2e_test.go b/deployment/data-feeds/changeset/stellar/e2e_test.go index cc6fd331ee2..e90296191b4 100644 --- a/deployment/data-feeds/changeset/stellar/e2e_test.go +++ b/deployment/data-feeds/changeset/stellar/e2e_test.go @@ -30,15 +30,15 @@ import ( // TestStellarDataFeedsE2E is the Definition-of-Done gate: it stands up a local // CTF Soroban devnet, deploys both Data Feeds contracts, and invokes EVERY -// exported function on-chain — the 23 cache fns and the 14 proxy fns — through +// exported function on-chain — the 26 cache fns and the 14 proxy fns — through // the real changesets/operations/bindings (no fakes). // // Function-coverage checklist. Outcome (a) = on-chain success; outcome (b) = a // documented, asserted, expected contract-domain/host error (never ignored). // -// DataFeedsCache (23): +// DataFeedsCache (26): // __constructor (a) DeployCache changeset -// upgrade (a) UpgradeCache changeset +// upgrade (a) Upgrade changeset // recover_tokens (b) RecoverTokens changeset — SAC transfer(from=self) // re-enters the contract => host trap Error(Context, // InvalidAction), asserted @@ -52,8 +52,15 @@ import ( // version (a) LoadCacheClient direct // type_and_version (a) LoadCacheClient direct // get_owner (a) LoadCacheClient direct -// decimals (a) LoadCacheClient direct -// description (a) LoadCacheClient direct +// decimals (a) LoadCacheClient direct — Some(0) while configured, +// None after remove_feed_configs +// description (a) LoadCacheClient direct — Some while configured, +// None after remove_feed_configs +// is_configured (a) LoadCacheClient direct — true, false after removal +// is_frozen (a) LoadCacheClient direct — false / true while frozen +// set_feed_frozen (a) SetFeedFrozen changeset — freeze/unfreeze +// round-trip; frozen decimals asserts the +// FeedFrozen error (b) // is_feed_admin (a) LoadCacheClient direct // has_permission (a) LoadCacheClient direct // get_feed_permissions (a) LoadCacheClient direct @@ -77,14 +84,10 @@ import ( // description (a) LoadProxyClient direct // latest_round (a) LoadProxyClient direct — reads the round stored via on_report // get_round (a) LoadProxyClient direct — reads the round stored via on_report -// upgrade (a) LoadProxyClient direct — fresh wasm hash (no changeset by design) -// recover_tokens (b) LoadProxyClient direct — SAC transfer(from=self) +// upgrade (a) Upgrade changeset +// recover_tokens (b) RecoverTokens changeset — SAC transfer(from=self) // re-enters the contract => host trap Error(Context, // InvalidAction), asserted -// -// The proxy deliberately has no upgrade/recover_tokens changeset (no operator -// need identified yet), so those two are driven through the generated proxy -// client directly. // e2eOnce guards the CTF container lifecycle for this suite (the provider // requires a *sync.Once; one test => one Once). @@ -95,8 +98,8 @@ const ( e2eVersion = "1.0.0" // e2eDataID is a canonical left-aligned feed id (BTC/USD-shaped). After // dataIDsToBytes left-justifies it into [16]byte, byte[7] is 0x00, which is - // outside the cache's decimals window [0x20,0x60], so cache.decimals returns 0 - // (the else branch). Non-zero id => a valid feed id (is_valid_id passes). + // outside the cache's decimals window [0x20,0x60], so cache.decimals returns + // Some(0) while configured. Non-zero id => a valid feed id (is_valid_id passes). e2eDataID = "0x018e16c39e00032000000" e2eDescription = "BTC/USD" e2eWorkflowName = "e2e" @@ -193,8 +196,40 @@ func TestStellarDataFeedsE2E(t *testing.T) { mustCall(t, "cache.version", func() error { _, e := cacheClient.Version(ctx); return e }) mustCall(t, "cache.type_and_version", func() error { _, e := cacheClient.TypeAndVersion(ctx); return e }) mustCall(t, "cache.get_owner", func() error { _, e := cacheClient.GetOwner(ctx); return e }) - mustCall(t, "cache.decimals", func() error { _, e := cacheClient.Decimals(ctx, did); return e }) - mustCall(t, "cache.description", func() error { _, e := cacheClient.Description(ctx, did); return e }) + mustCall(t, "cache.decimals", func() error { + dec, e := cacheClient.Decimals(ctx, did) + if e != nil { + return e + } + require.NotNil(t, dec, "decimals must be present for a configured feed") + require.Equal(t, uint32(0), *dec) + return nil + }) + mustCall(t, "cache.description", func() error { + desc, e := cacheClient.Description(ctx, did) + if e != nil { + return e + } + require.NotNil(t, desc, "description must be present for a configured feed") + require.Equal(t, e2eDescription, *desc) + return nil + }) + mustCall(t, "cache.is_configured", func() error { + configured, e := cacheClient.IsConfigured(ctx, did) + if e != nil { + return e + } + require.True(t, configured, "feed was configured via SetFeedConfigs") + return nil + }) + mustCall(t, "cache.is_frozen", func() error { + frozen, e := cacheClient.IsFrozen(ctx, did) + if e != nil { + return e + } + require.False(t, frozen, "feed starts unfrozen") + return nil + }) mustCall(t, "cache.is_feed_admin", func() error { _, e := cacheClient.IsFeedAdmin(ctx, deployer); return e }) mustCall(t, "cache.has_permission", func() error { _, e := cacheClient.HasPermission(ctx, did, deployer, [20]byte(ownerBytes), [10]byte(nameBytes)) @@ -226,6 +261,23 @@ func TestStellarDataFeedsE2E(t *testing.T) { return nil }) + // --- set_feed_frozen: freeze -> reads rejected -> unfreeze ------------- + env = apply(t, env, SetFeedFrozen{}, &SetFeedFrozenRequest{ + ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, Admin: deployer, + DataIDs: []string{e2eDataID}, Frozen: true, + }) + frozen, err := cacheClient.IsFrozen(ctx, did) + require.NoError(t, err) + require.True(t, frozen, "is_frozen must report the freeze") + _, frozenErr := cacheClient.Decimals(ctx, did) + require.Error(t, frozenErr, "decimals on a frozen feed must be rejected") + require.ErrorContains(t, frozenErr, "#109", "expected CacheError::FeedFrozen") + t.Logf("(b) cache.decimals while frozen error: %v", frozenErr) + env = apply(t, env, SetFeedFrozen{}, &SetFeedFrozenRequest{ + ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, Admin: deployer, + DataIDs: []string{e2eDataID}, Frozen: false, + }) + // --- proxy reads (read through to the cache's stored round) ----------- proxyClient, _, err := LoadProxyClient(env, sel, e2eQualifier, e2eVersion) require.NoError(t, err) @@ -239,24 +291,24 @@ func TestStellarDataFeedsE2E(t *testing.T) { mustCall(t, "proxy.latest_round", func() error { _, e := proxyClient.LatestRound(ctx, pdid); return e }) mustCall(t, "proxy.get_round", func() error { _, e := proxyClient.GetRound(ctx, pdid, 1); return e }) - // --- proxy upgrade + recover_tokens (no changesets by design) --------- - deps, err := newStellarDeps(ch) - require.NoError(t, err) - proxyHash, err := deps.Deploy.UploadContractWASM(ctx, proxyWasm) - require.NoError(t, err) - require.NoError(t, proxyClient.Upgrade(ctx, [32]byte(proxyHash)), - "proxy upgrade to a freshly uploaded wasm hash must succeed") + // --- proxy upgrade + recover_tokens ------------------------------------ + env = apply(t, env, Upgrade{}, &UpgradeRequest{ + ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, + Contract: ProxyContract, WasmPath: proxyWasm, + }) // recover_tokens invokes the SAC transfer(from=self, to, amount). We pass the // contract's own address as the token, so the transfer sub-call re-enters the // same contract; Soroban forbids contract re-entry and the host traps with // Error(Context, InvalidAction) ("Contract re-entry is not allowed"). This is - // the documented (b) outcome: recover_tokens is really invoked (its three args - // are decoded and the transfer is dispatched — the tooling's encode/invoke path - // is what's under test) and the trap is a contract/host-domain error, not a - // transport/XDR/auth-plumbing failure. Standing up a funded native-XLM SAC - // balance for a success path is disproportionate for this gate. - proxyRecoverErr := proxyClient.RecoverTokens(ctx, proxyClient.ContractID(), deployer, 1) + // the documented (b) outcome: recover_tokens is really invoked and the trap + // is a contract/host-domain error, not a transport/XDR/auth-plumbing failure. + // Standing up a funded native-XLM SAC balance for a success path is + // disproportionate for this gate. + proxyRecoverErr := applyErr(t, env, RecoverTokens{}, &RecoverTokensRequest{ + ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, + Contract: ProxyContract, Token: proxyClient.ContractID(), To: deployer, Amount: 1, + }) require.Error(t, proxyRecoverErr, "proxy recover_tokens must fail: self-transfer re-enters the contract") require.ErrorContains(t, proxyRecoverErr, "InvalidAction", "expected a contract re-entry host trap") t.Logf("(b) proxy.recover_tokens error: %v", proxyRecoverErr) @@ -275,8 +327,9 @@ func TestStellarDataFeedsE2E(t *testing.T) { }) // --- cache upgrade + recover + config/admin removal -------------------- - env = apply(t, env, UpgradeCache{}, &UpgradeCacheRequest{ - ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, WasmPath: cacheWasm, + env = apply(t, env, Upgrade{}, &UpgradeRequest{ + ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, + Contract: CacheContract, WasmPath: cacheWasm, }) // recover_tokens: same real-transfer semantics as the proxy. Token is the @@ -285,7 +338,7 @@ func TestStellarDataFeedsE2E(t *testing.T) { // while the deployer is still owner (ownership renounce happens last). cacheRecoverErr := applyErr(t, env, RecoverTokens{}, &RecoverTokensRequest{ ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, - Token: cacheAddr, To: deployer, Amount: 1, + Contract: CacheContract, Token: cacheAddr, To: deployer, Amount: 1, }) require.Error(t, cacheRecoverErr, "cache recover_tokens must fail: self-transfer re-enters the contract") require.ErrorContains(t, cacheRecoverErr, "InvalidAction", "expected a contract re-entry host trap") @@ -295,6 +348,18 @@ func TestStellarDataFeedsE2E(t *testing.T) { ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, Admin: deployer, DataIDs: []string{e2eDataID}, }) + + // The optional reads must decode None once the feed config is gone. + dec, err := cacheClient.Decimals(ctx, did) + require.NoError(t, err) + require.Nil(t, dec, "decimals must be absent after remove_feed_configs") + desc, err := cacheClient.Description(ctx, did) + require.NoError(t, err) + require.Nil(t, desc, "description must be absent after remove_feed_configs") + configured, err := cacheClient.IsConfigured(ctx, did) + require.NoError(t, err) + require.False(t, configured, "is_configured must be false after remove_feed_configs") + env = apply(t, env, RemoveFeedAdmin{}, &FeedAdminRequest{ ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, Admin: deployer, }) diff --git a/deployment/data-feeds/changeset/stellar/freeze_feeds.go b/deployment/data-feeds/changeset/stellar/freeze_feeds.go new file mode 100644 index 00000000000..ddc2170f5ab --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/freeze_feeds.go @@ -0,0 +1,67 @@ +package stellar + +import ( + "errors" + + "github.com/Masterminds/semver/v3" + + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" + + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/stellar/operation" +) + +// SetFeedFrozenRequest freezes or unfreezes a batch of feeds on an +// already-deployed DataFeedsCache contract. +type SetFeedFrozenRequest struct { + ChainSel uint64 + Qualifier string + Version string + Admin string + DataIDs []string + Frozen bool +} + +var _ cldf.ChangeSetV2[*SetFeedFrozenRequest] = SetFeedFrozen{} + +// SetFeedFrozen freezes or unfreezes feeds on the cache. Reads on a frozen +// feed are rejected with FeedFrozen; a feed with no recorded round cannot be +// frozen (NoFeedState). +type SetFeedFrozen struct{} + +func (SetFeedFrozen) VerifyPreconditions(env cldf.Environment, req *SetFeedFrozenRequest) error { + if err := verifyContractRef(env, req.ChainSel, CacheContract, req.Qualifier, req.Version); err != nil { + return err + } + if err := validateAddress(req.Admin); err != nil { + return err + } + if len(req.DataIDs) == 0 { + return errors.New("DataIDs cannot be empty") + } + if _, err := dataIDsToBytes(req.DataIDs); err != nil { + return err + } + return nil +} + +func (SetFeedFrozen) Apply(env cldf.Environment, req *SetFeedFrozenRequest) (cldf.ChangesetOutput, error) { + var out cldf.ChangesetOutput + version := semver.MustParse(req.Version) + d, _, err := resolveContractDeps(env, req.ChainSel, CacheContract, req.Qualifier, version) + if err != nil { + return out, err + } + + ids, err := dataIDsToBytes(req.DataIDs) + if err != nil { + return out, err + } + _, err = operations.ExecuteOperation(env.OperationsBundle, operation.SetFeedFrozen, d.deps, operation.SetFeedFrozenInput{ + ContractID: d.contractID, + Admin: req.Admin, + DataIDs: ids, + Frozen: req.Frozen, + }) + return out, err +} diff --git a/deployment/data-feeds/changeset/stellar/operation/operation.go b/deployment/data-feeds/changeset/stellar/operation/operation.go index 13211cb1487..1287af9051b 100644 --- a/deployment/data-feeds/changeset/stellar/operation/operation.go +++ b/deployment/data-feeds/changeset/stellar/operation/operation.go @@ -48,8 +48,8 @@ type DeployCacheInput struct { } // DeployCache uploads the cache WASM and instantiates it via CreateContractV2 -// with __constructor(owner). The cache constructor takes a single argument; -// the data-retention TTL is a hardcoded on-chain constant, not a ctor input. +// with __constructor(owner); the data-retention TTL is an on-chain constant, +// not a constructor input. var DeployCache = cldfops.NewOperation( "df-cache:deploy", Version1_0_0, "Deploys the DataFeedsCache Soroban contract", @@ -119,6 +119,22 @@ var RemoveFeedConfigs = cldfops.NewOperation( }, ) +type SetFeedFrozenInput struct { + ContractID string `json:"contract_id"` + Admin string `json:"admin"` + DataIDs [][16]byte `json:"data_ids"` + Frozen bool `json:"frozen"` +} + +var SetFeedFrozen = cldfops.NewOperation( + "df-cache:set-feed-frozen", Version1_0_0, + "Freezes or unfreezes feeds on the cache", + func(b cldfops.Bundle, d StellarDeps, in SetFeedFrozenInput) (Void, error) { + c := cache.NewDataFeedsCacheClient(d.Invoker, in.ContractID) + return Void{}, c.SetFeedFrozen(b.GetContext(), in.Admin, in.DataIDs, in.Frozen) + }, +) + type FeedAdminInput struct { ContractID string `json:"contract_id"` Admin string `json:"admin"` @@ -209,15 +225,20 @@ var UploadWASM = cldfops.NewOperation( }, ) -type UpgradeCacheInput struct { +type UpgradeContractInput struct { ContractID string `json:"contract_id"` + IsProxy bool `json:"is_proxy"` NewWasmHash [32]byte `json:"new_wasm_hash"` } -var UpgradeCache = cldfops.NewOperation( - "df-cache:upgrade", Version1_0_0, - "Points the cache at a new WASM implementation", - func(b cldfops.Bundle, d StellarDeps, in UpgradeCacheInput) (Void, error) { +var UpgradeContract = cldfops.NewOperation( + "df:upgrade", Version1_0_0, + "Points the contract at a new WASM implementation", + func(b cldfops.Bundle, d StellarDeps, in UpgradeContractInput) (Void, error) { + if in.IsProxy { + c := proxy.NewDataFeedsProxyClient(d.Invoker, in.ContractID) + return Void{}, c.Upgrade(b.GetContext(), in.NewWasmHash) + } c := cache.NewDataFeedsCacheClient(d.Invoker, in.ContractID) return Void{}, c.Upgrade(b.GetContext(), in.NewWasmHash) }, @@ -225,15 +246,20 @@ var UpgradeCache = cldfops.NewOperation( type RecoverTokensInput struct { ContractID string `json:"contract_id"` + IsProxy bool `json:"is_proxy"` Token string `json:"token"` To string `json:"to"` Amount int64 `json:"amount"` } var RecoverTokens = cldfops.NewOperation( - "df-cache:recover-tokens", Version1_0_0, - "Recovers tokens accidentally sent to the cache", + "df:recover-tokens", Version1_0_0, + "Recovers tokens accidentally sent to the contract", func(b cldfops.Bundle, d StellarDeps, in RecoverTokensInput) (Void, error) { + if in.IsProxy { + c := proxy.NewDataFeedsProxyClient(d.Invoker, in.ContractID) + return Void{}, c.RecoverTokens(b.GetContext(), in.Token, in.To, in.Amount) + } c := cache.NewDataFeedsCacheClient(d.Invoker, in.ContractID) return Void{}, c.RecoverTokens(b.GetContext(), in.Token, in.To, in.Amount) }, diff --git a/deployment/data-feeds/changeset/stellar/recover_tokens.go b/deployment/data-feeds/changeset/stellar/recover_tokens.go index 2619ea53e19..48e056bcd84 100644 --- a/deployment/data-feeds/changeset/stellar/recover_tokens.go +++ b/deployment/data-feeds/changeset/stellar/recover_tokens.go @@ -5,6 +5,7 @@ import ( "github.com/Masterminds/semver/v3" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/smartcontractkit/chainlink-deployments-framework/operations" @@ -12,11 +13,12 @@ import ( ) // RecoverTokensRequest recovers tokens accidentally sent to an already-deployed -// DataFeedsCache contract. +// cache or proxy contract. type RecoverTokensRequest struct { ChainSel uint64 Qualifier string Version string + Contract datastore.ContractType // CacheContract or ProxyContract Token string To string Amount int64 @@ -24,11 +26,14 @@ type RecoverTokensRequest struct { var _ cldf.ChangeSetV2[*RecoverTokensRequest] = RecoverTokens{} -// RecoverTokens recovers tokens accidentally sent to the cache. +// RecoverTokens recovers tokens accidentally sent to the cache or proxy. type RecoverTokens struct{} func (RecoverTokens) VerifyPreconditions(env cldf.Environment, req *RecoverTokensRequest) error { - if err := verifyContractRef(env, req.ChainSel, CacheContract, req.Qualifier, req.Version); err != nil { + if req.Contract != CacheContract && req.Contract != ProxyContract { + return fmt.Errorf("unsupported contract type %q: must be %q or %q", req.Contract, CacheContract, ProxyContract) + } + if err := verifyContractRef(env, req.ChainSel, req.Contract, req.Qualifier, req.Version); err != nil { return err } if err := validateAddress(req.Token); err != nil { @@ -46,12 +51,13 @@ func (RecoverTokens) VerifyPreconditions(env cldf.Environment, req *RecoverToken func (RecoverTokens) Apply(env cldf.Environment, req *RecoverTokensRequest) (cldf.ChangesetOutput, error) { var out cldf.ChangesetOutput version := semver.MustParse(req.Version) - d, _, err := resolveContractDeps(env, req.ChainSel, CacheContract, req.Qualifier, version) + d, _, err := resolveContractDeps(env, req.ChainSel, req.Contract, req.Qualifier, version) if err != nil { return out, err } _, err = operations.ExecuteOperation(env.OperationsBundle, operation.RecoverTokens, d.deps, operation.RecoverTokensInput{ ContractID: d.contractID, + IsProxy: req.Contract == ProxyContract, Token: req.Token, To: req.To, Amount: req.Amount, diff --git a/deployment/data-feeds/changeset/stellar/set_proxy_cache.go b/deployment/data-feeds/changeset/stellar/set_proxy_cache.go index d57db3a5732..3ebf01d12ad 100644 --- a/deployment/data-feeds/changeset/stellar/set_proxy_cache.go +++ b/deployment/data-feeds/changeset/stellar/set_proxy_cache.go @@ -14,13 +14,8 @@ import ( // SetProxyCacheRequest points an already-deployed DataFeedsProxy contract at a // (possibly different) cache contract. Qualifier resolves the proxy being -// acted on (ChainSel, ProxyContract, Version, Qualifier); CacheQualifier -// resolves the new cache target (ChainSel, CacheContract, Version, -// CacheQualifier) — both lookups share this request's Version: per this -// suite's convention, cross-contract lookups use the acting changeset's -// datastore Version, so a cache recorded under a different version needs a -// matching-version record (an optional CacheVersion field is a recorded -// follow-up if operators need split versions). +// acted on; CacheQualifier resolves the new cache target. Cross-contract +// lookups share the request's Version (see README). type SetProxyCacheRequest struct { ChainSel uint64 Qualifier string diff --git a/deployment/data-feeds/changeset/stellar/state.go b/deployment/data-feeds/changeset/stellar/state.go index 461655f4d83..94488fb3a32 100644 --- a/deployment/data-feeds/changeset/stellar/state.go +++ b/deployment/data-feeds/changeset/stellar/state.go @@ -11,11 +11,8 @@ import ( proxy "github.com/smartcontractkit/chainlink-stellar/bindings/contracts/data_feeds_proxy" ) -// loadContractClientDeps resolves the AddressRef for (chainSel, contractType, -// version, qualifier) and the chain's deploy/invoke deps (via -// resolveContractDeps, which chain ok-guards before touching the datastore -// or building deps). It is the shared lookup behind LoadCacheClient and -// LoadProxyClient. +// loadContractClientDeps parses version and resolves the contract's AddressRef +// plus chain deps — the shared lookup behind LoadCacheClient and LoadProxyClient. func loadContractClientDeps(env cldf.Environment, chainSel uint64, contractType datastore.ContractType, qualifier, version string) (stellarApplyDeps, datastore.AddressRef, error) { v, err := semver.NewVersion(version) if err != nil { @@ -24,9 +21,8 @@ func loadContractClientDeps(env cldf.Environment, chainSel uint64, contractType return resolveContractDeps(env, chainSel, contractType, qualifier, v) } -// LoadCacheClient resolves the CacheContract AddressRef from env.DataStore -// for (chainSel, qualifier, version) and returns a generated cache client -// bound to its contract ID, along with the resolved AddressRef. +// LoadCacheClient returns a generated cache client bound to the resolved +// CacheContract AddressRef for (chainSel, qualifier, version). func LoadCacheClient(env cldf.Environment, chainSel uint64, qualifier, version string) (*cache.DataFeedsCacheClient, datastore.AddressRef, error) { d, ref, err := loadContractClientDeps(env, chainSel, CacheContract, qualifier, version) if err != nil { diff --git a/deployment/data-feeds/changeset/stellar/testdata/README.md b/deployment/data-feeds/changeset/stellar/testdata/README.md index 9da7362e433..9946575c99d 100644 --- a/deployment/data-feeds/changeset/stellar/testdata/README.md +++ b/deployment/data-feeds/changeset/stellar/testdata/README.md @@ -3,13 +3,14 @@ `data_feeds_cache.wasm` and `data_feeds_proxy.wasm` are the compiled Soroban contracts under test by `e2e_test.go` (`TestStellarDataFeedsE2E`). They are the byte-for-byte artifacts of `stellar contract build` on chainlink-stellar's -`contracts/data-feeds` workspace at PR #161/#162 content (Bound with explicit -discriminants; literal BytesN ABI). +`contracts/data-feeds` workspace at commit `971e21ae` (PR #161/#162 heads; +adds `is_configured`/`is_frozen`/`set_feed_frozen` and optional +`decimals`/`description` returns). sha256: -- `data_feeds_cache.wasm` = `e96c7dc85c4cf37acf8acf6bbefd85d5e888d9a9a5342e39151d6f0323c70cbc` -- `data_feeds_proxy.wasm` = `e3f4a4ae333d26218bdb7aca0870623d06bdc2a65ee40440c797cfedd3e456f1` +- `data_feeds_cache.wasm` = `c494264eaaa2cf5e5a745c0022ac791ffa07d1beb24a1d2ac67fcb6d7f28ef0f` +- `data_feeds_proxy.wasm` = `f670dc316877fac6f6c594133f411af25254d4a305038eeade28d34b9382fe6a` ## Regeneration diff --git a/deployment/data-feeds/changeset/stellar/testdata/data_feeds_cache.wasm b/deployment/data-feeds/changeset/stellar/testdata/data_feeds_cache.wasm index cc53ad56d1bac6b3744db8f05cc9f2a29d452d9c..b7390ecfb8189e7969e719862ee7b59208dd51d4 100644 GIT binary patch delta 9063 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 Bu?+wK diff --git a/deployment/data-feeds/changeset/stellar/testdata/data_feeds_proxy.wasm b/deployment/data-feeds/changeset/stellar/testdata/data_feeds_proxy.wasm index 367e0d4bbe20cc48d1ff234e911feea1fb9ca5de..5cedfbb14dbeaf2e0325884d550ac893eb2ea8d6 100644 GIT binary patch delta 143 zcmezFIp1qTJQLFb`OS$;`&k%QOjcwMvu86TZsd@dyp=tSasFf;jyNqRm{Hga TP+|ZZagqUO2KVMhjxrel$$=>O delta 100 zcmbR5_1$wrJQLFoxy^}8`&k$#PgZ0Po7})Is?6Z@pRw7AC5V^Nv9I9(gDwMulmerp zLY4}H0)taaLqkJB^W-D!F^oNv6*=O>fl?e diff --git a/deployment/data-feeds/changeset/stellar/upgrade.go b/deployment/data-feeds/changeset/stellar/upgrade.go new file mode 100644 index 00000000000..e09d2ccd395 --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/upgrade.go @@ -0,0 +1,67 @@ +package stellar + +import ( + "fmt" + "os" + + "github.com/Masterminds/semver/v3" + + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" + + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/stellar/operation" +) + +// UpgradeRequest points an already-deployed cache or proxy contract at a new +// WASM implementation. +type UpgradeRequest struct { + ChainSel uint64 + Qualifier string + Version string + Contract datastore.ContractType // CacheContract or ProxyContract + WasmPath string +} + +var _ cldf.ChangeSetV2[*UpgradeRequest] = Upgrade{} + +// Upgrade uploads a new WASM blob and points the contract at it. Apply chains +// two operations: UploadWASM produces the code hash that UpgradeContract then +// applies on-chain. +type Upgrade struct{} + +func (Upgrade) VerifyPreconditions(env cldf.Environment, req *UpgradeRequest) error { + if req.Contract != CacheContract && req.Contract != ProxyContract { + return fmt.Errorf("unsupported contract type %q: must be %q or %q", req.Contract, CacheContract, ProxyContract) + } + if err := verifyContractRef(env, req.ChainSel, req.Contract, req.Qualifier, req.Version); err != nil { + return err + } + if _, err := os.Stat(req.WasmPath); err != nil { + return fmt.Errorf("wasm path: %w", err) + } + return nil +} + +func (Upgrade) Apply(env cldf.Environment, req *UpgradeRequest) (cldf.ChangesetOutput, error) { + var out cldf.ChangesetOutput + version := semver.MustParse(req.Version) + d, _, err := resolveContractDeps(env, req.ChainSel, req.Contract, req.Qualifier, version) + if err != nil { + return out, err + } + + uploadReport, err := operations.ExecuteOperation(env.OperationsBundle, operation.UploadWASM, d.deps, operation.UploadWASMInput{ + WasmPath: req.WasmPath, + }) + if err != nil { + return out, err + } + + _, err = operations.ExecuteOperation(env.OperationsBundle, operation.UpgradeContract, d.deps, operation.UpgradeContractInput{ + ContractID: d.contractID, + IsProxy: req.Contract == ProxyContract, + NewWasmHash: uploadReport.Output.WasmHash, + }) + return out, err +} diff --git a/deployment/data-feeds/changeset/stellar/upgrade_cache.go b/deployment/data-feeds/changeset/stellar/upgrade_cache.go deleted file mode 100644 index 59ba101ab1a..00000000000 --- a/deployment/data-feeds/changeset/stellar/upgrade_cache.go +++ /dev/null @@ -1,61 +0,0 @@ -package stellar - -import ( - "fmt" - "os" - - "github.com/Masterminds/semver/v3" - - cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" - "github.com/smartcontractkit/chainlink-deployments-framework/operations" - - "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/stellar/operation" -) - -// UpgradeCacheRequest points an already-deployed DataFeedsCache contract at a -// new WASM implementation. -type UpgradeCacheRequest struct { - ChainSel uint64 - Qualifier string - Version string - WasmPath string -} - -var _ cldf.ChangeSetV2[*UpgradeCacheRequest] = UpgradeCache{} - -// UpgradeCache uploads a new WASM blob and points the cache at it. Apply -// chains two operations: UploadWASM produces the code hash that UpgradeCache -// then applies on-chain. -type UpgradeCache struct{} - -func (UpgradeCache) VerifyPreconditions(env cldf.Environment, req *UpgradeCacheRequest) error { - if err := verifyContractRef(env, req.ChainSel, CacheContract, req.Qualifier, req.Version); err != nil { - return err - } - if _, err := os.Stat(req.WasmPath); err != nil { - return fmt.Errorf("wasm path: %w", err) - } - return nil -} - -func (UpgradeCache) Apply(env cldf.Environment, req *UpgradeCacheRequest) (cldf.ChangesetOutput, error) { - var out cldf.ChangesetOutput - version := semver.MustParse(req.Version) - d, _, err := resolveContractDeps(env, req.ChainSel, CacheContract, req.Qualifier, version) - if err != nil { - return out, err - } - - uploadReport, err := operations.ExecuteOperation(env.OperationsBundle, operation.UploadWASM, d.deps, operation.UploadWASMInput{ - WasmPath: req.WasmPath, - }) - if err != nil { - return out, err - } - - _, err = operations.ExecuteOperation(env.OperationsBundle, operation.UpgradeCache, d.deps, operation.UpgradeCacheInput{ - ContractID: d.contractID, - NewWasmHash: uploadReport.Output.WasmHash, - }) - return out, err -} diff --git a/deployment/data-feeds/changeset/stellar/validation.go b/deployment/data-feeds/changeset/stellar/validation.go index e3893a83887..0f33d53ba3a 100644 --- a/deployment/data-feeds/changeset/stellar/validation.go +++ b/deployment/data-feeds/changeset/stellar/validation.go @@ -20,10 +20,8 @@ func validateAddress(s string) error { return fmt.Errorf("%q is not a valid Stellar account or contract address", s) } -// dataIDsToBytes converts 0x-prefixed hex feed IDs to 16-byte arrays, -// left-justified with trailing zero padding — byte-identical to the Solana -// suite's dataIDtoBytes (data IDs are canonically left-aligned, e.g. -// 0x018e16c39e00032000000000000000000 shorthand drops trailing zeros). +// dataIDsToBytes converts 0x-prefixed hex feed IDs to [16]byte, left-justified +// with trailing zero padding (data IDs are canonically left-aligned). func dataIDsToBytes(ids []string) ([][16]byte, error) { out := make([][16]byte, 0, len(ids)) for _, id := range ids { diff --git a/deployment/go.mod b/deployment/go.mod index f9882e0d3a0..2e6bc8589f4 100644 --- a/deployment/go.mod +++ b/deployment/go.mod @@ -60,7 +60,7 @@ require ( github.com/smartcontractkit/chainlink-protos/orchestrator v0.11.0 github.com/smartcontractkit/chainlink-solana v1.3.1-0.20260605202330-b5a89c32fdc1 github.com/smartcontractkit/chainlink-solana/contracts v0.0.0-20260513123719-d347eaf314e1 - github.com/smartcontractkit/chainlink-stellar v0.0.3-0.20260723224154-a5c793895953 + github.com/smartcontractkit/chainlink-stellar v0.0.3-0.20260804133734-971e21aec752 github.com/smartcontractkit/chainlink-sui v0.0.0-20260714190119-005bb9a612c3 github.com/smartcontractkit/chainlink-sui/codec v0.0.0-20260720132736-e99278bfdc96 github.com/smartcontractkit/chainlink-sui/deployment v0.0.0-20260714190119-005bb9a612c3 @@ -462,7 +462,7 @@ require ( github.com/smartcontractkit/chainlink-protos/storage-service v0.3.0 // indirect github.com/smartcontractkit/chainlink-protos/svr v1.3.0 // indirect github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260716141634-c0cc05ed05d8 // indirect - github.com/smartcontractkit/chainlink-stellar/bindings v0.0.0-20260723224154-a5c793895953 + github.com/smartcontractkit/chainlink-stellar/bindings v0.0.0-20260804133734-971e21aec752 github.com/smartcontractkit/chainlink-testing-framework/parrot v0.6.2 // indirect github.com/smartcontractkit/chainlink-testing-framework/seth v1.51.5 // indirect github.com/smartcontractkit/chainlink-ton/cciplib v0.1.1-0.20260716214810-db5ecc877490 // indirect diff --git a/deployment/go.sum b/deployment/go.sum index bd25cfb028e..23986392020 100644 --- a/deployment/go.sum +++ b/deployment/go.sum @@ -1480,10 +1480,10 @@ github.com/smartcontractkit/chainlink-solana v1.3.1-0.20260605202330-b5a89c32fdc github.com/smartcontractkit/chainlink-solana v1.3.1-0.20260605202330-b5a89c32fdc1/go.mod h1:wi1QdXqhSJnADt9YRaRtEWomqknLcrdkTS0JotupuOQ= github.com/smartcontractkit/chainlink-solana/contracts v0.0.0-20260513123719-d347eaf314e1 h1:/xvuNFI7DwOoTQnmAdYPDdY+sConn3RgZ2rMy/8AXlo= github.com/smartcontractkit/chainlink-solana/contracts v0.0.0-20260513123719-d347eaf314e1/go.mod h1:lQK+YvR9Ox0ft72k0se7DlA+kujVWyjFQXG3DLbEZ/4= -github.com/smartcontractkit/chainlink-stellar v0.0.3-0.20260723224154-a5c793895953 h1:n/Q0hHFQRxYEalqDEeg01osPd6SYVdrwSbu7Kp0VBdA= -github.com/smartcontractkit/chainlink-stellar v0.0.3-0.20260723224154-a5c793895953/go.mod h1:J8D0u8K/a2+3YuJx0F8nf4Fd7h1UZqeWGYszp7aCbdA= -github.com/smartcontractkit/chainlink-stellar/bindings v0.0.0-20260723224154-a5c793895953 h1:vMQkCPE3ClKukommFC52jKTwPwI5WbM8vo+DMvzG39Q= -github.com/smartcontractkit/chainlink-stellar/bindings v0.0.0-20260723224154-a5c793895953/go.mod h1:IYBZArxwOQRlcx0Udx/VfFjqJpAs4NrjKsBoNITKAMg= +github.com/smartcontractkit/chainlink-stellar v0.0.3-0.20260804133734-971e21aec752 h1:b3cQr1oDv0S+sdjm3zy+ejmA0v3uTXPEF1IyCc7NQsw= +github.com/smartcontractkit/chainlink-stellar v0.0.3-0.20260804133734-971e21aec752/go.mod h1:J8D0u8K/a2+3YuJx0F8nf4Fd7h1UZqeWGYszp7aCbdA= +github.com/smartcontractkit/chainlink-stellar/bindings v0.0.0-20260804133734-971e21aec752 h1:kxAF1oaJMql/b4Pa3nX965cPFBfTM18I8cLlJnPgaSM= +github.com/smartcontractkit/chainlink-stellar/bindings v0.0.0-20260804133734-971e21aec752/go.mod h1:IYBZArxwOQRlcx0Udx/VfFjqJpAs4NrjKsBoNITKAMg= github.com/smartcontractkit/chainlink-sui v0.0.0-20260714190119-005bb9a612c3 h1:bI9pXgfGOhS+tPGjPkpos3nuGE/5Xfw8X0MQHi+0EcU= github.com/smartcontractkit/chainlink-sui v0.0.0-20260714190119-005bb9a612c3/go.mod h1:6FWUSAXA58d0c9AyOi/1zymX40/67czcDR1SGZt/BKg= github.com/smartcontractkit/chainlink-sui/codec v0.0.0-20260720132736-e99278bfdc96 h1:3R9f6pguDIjei+nKx5dUT9j6fX8urGmlZA7Ad3x9UBs= From d8db32c7a4f102bb5a69d513b57c27afc8ab1071 Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Tue, 4 Aug 2026 19:29:51 +0100 Subject: [PATCH 03/19] Fold resolve and contract checks into helpers - Add resolveDeps: parses the pre-validated version and drops the unused AddressRef return; use it at every single-contract Apply - Delete the per-request resolve methods in feed_admin and ownership - Add validateContract to replace three identical type-check blocks --- .../changeset/stellar/configure_cache.go | 5 +---- .../data-feeds/changeset/stellar/deps.go | 7 +++++++ .../changeset/stellar/feed_admin.go | 13 ++----------- .../changeset/stellar/freeze_feeds.go | 5 +---- .../data-feeds/changeset/stellar/ownership.go | 19 +++++-------------- .../changeset/stellar/recover_tokens.go | 9 +++------ .../changeset/stellar/remove_feeds.go | 5 +---- .../data-feeds/changeset/stellar/upgrade.go | 9 +++------ .../changeset/stellar/validation.go | 9 +++++++++ 9 files changed, 32 insertions(+), 49 deletions(-) diff --git a/deployment/data-feeds/changeset/stellar/configure_cache.go b/deployment/data-feeds/changeset/stellar/configure_cache.go index 21ca835588d..d4cd5aefc6c 100644 --- a/deployment/data-feeds/changeset/stellar/configure_cache.go +++ b/deployment/data-feeds/changeset/stellar/configure_cache.go @@ -4,8 +4,6 @@ import ( "errors" "fmt" - "github.com/Masterminds/semver/v3" - cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/smartcontractkit/chainlink-deployments-framework/operations" cache "github.com/smartcontractkit/chainlink-stellar/bindings/contracts/data_feeds_cache" @@ -91,8 +89,7 @@ func (req *SetFeedConfigsRequest) permissions() ([]cache.WorkflowPermission, err func (SetFeedConfigs) Apply(env cldf.Environment, req *SetFeedConfigsRequest) (cldf.ChangesetOutput, error) { var out cldf.ChangesetOutput - version := semver.MustParse(req.Version) - d, _, err := resolveContractDeps(env, req.ChainSel, CacheContract, req.Qualifier, version) + d, err := resolveDeps(env, req.ChainSel, CacheContract, req.Qualifier, req.Version) if err != nil { return out, err } diff --git a/deployment/data-feeds/changeset/stellar/deps.go b/deployment/data-feeds/changeset/stellar/deps.go index da8e6d48030..a01eeb871b7 100644 --- a/deployment/data-feeds/changeset/stellar/deps.go +++ b/deployment/data-feeds/changeset/stellar/deps.go @@ -49,6 +49,13 @@ func verifyContractRef(env cldf.Environment, chainSel uint64, contractType datas return nil } +// resolveDeps is resolveContractDeps for callers that don't need the +// AddressRef; version must already be validated by VerifyPreconditions. +func resolveDeps(env cldf.Environment, chainSel uint64, contractType datastore.ContractType, qualifier, version string) (stellarApplyDeps, error) { + d, _, err := resolveContractDeps(env, chainSel, contractType, qualifier, semver.MustParse(version)) + return d, err +} + // resolveContractDeps looks up the AddressRef for (chainSel, contractType, // version, qualifier) and bundles it with the chain's deploy/invoke deps. func resolveContractDeps(env cldf.Environment, chainSel uint64, contractType datastore.ContractType, qualifier string, version *semver.Version) (stellarApplyDeps, datastore.AddressRef, error) { diff --git a/deployment/data-feeds/changeset/stellar/feed_admin.go b/deployment/data-feeds/changeset/stellar/feed_admin.go index 4d9ae66e254..7c83aaf4c62 100644 --- a/deployment/data-feeds/changeset/stellar/feed_admin.go +++ b/deployment/data-feeds/changeset/stellar/feed_admin.go @@ -1,8 +1,6 @@ package stellar import ( - "github.com/Masterminds/semver/v3" - cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/smartcontractkit/chainlink-deployments-framework/operations" @@ -28,13 +26,6 @@ func (req *FeedAdminRequest) verifyPreconditions(env cldf.Environment) error { return nil } -// resolve looks up the cache contract's dependencies + address for req. -func (req *FeedAdminRequest) resolve(env cldf.Environment) (stellarApplyDeps, error) { - version := semver.MustParse(req.Version) - d, _, err := resolveContractDeps(env, req.ChainSel, CacheContract, req.Qualifier, version) - return d, err -} - var ( _ cldf.ChangeSetV2[*FeedAdminRequest] = AddFeedAdmin{} _ cldf.ChangeSetV2[*FeedAdminRequest] = RemoveFeedAdmin{} @@ -49,7 +40,7 @@ func (AddFeedAdmin) VerifyPreconditions(env cldf.Environment, req *FeedAdminRequ func (AddFeedAdmin) Apply(env cldf.Environment, req *FeedAdminRequest) (cldf.ChangesetOutput, error) { var out cldf.ChangesetOutput - d, err := req.resolve(env) + d, err := resolveDeps(env, req.ChainSel, CacheContract, req.Qualifier, req.Version) if err != nil { return out, err } @@ -69,7 +60,7 @@ func (RemoveFeedAdmin) VerifyPreconditions(env cldf.Environment, req *FeedAdminR func (RemoveFeedAdmin) Apply(env cldf.Environment, req *FeedAdminRequest) (cldf.ChangesetOutput, error) { var out cldf.ChangesetOutput - d, err := req.resolve(env) + d, err := resolveDeps(env, req.ChainSel, CacheContract, req.Qualifier, req.Version) if err != nil { return out, err } diff --git a/deployment/data-feeds/changeset/stellar/freeze_feeds.go b/deployment/data-feeds/changeset/stellar/freeze_feeds.go index ddc2170f5ab..6778c381946 100644 --- a/deployment/data-feeds/changeset/stellar/freeze_feeds.go +++ b/deployment/data-feeds/changeset/stellar/freeze_feeds.go @@ -3,8 +3,6 @@ package stellar import ( "errors" - "github.com/Masterminds/semver/v3" - cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/smartcontractkit/chainlink-deployments-framework/operations" @@ -47,8 +45,7 @@ func (SetFeedFrozen) VerifyPreconditions(env cldf.Environment, req *SetFeedFroze func (SetFeedFrozen) Apply(env cldf.Environment, req *SetFeedFrozenRequest) (cldf.ChangesetOutput, error) { var out cldf.ChangesetOutput - version := semver.MustParse(req.Version) - d, _, err := resolveContractDeps(env, req.ChainSel, CacheContract, req.Qualifier, version) + d, err := resolveDeps(env, req.ChainSel, CacheContract, req.Qualifier, req.Version) if err != nil { return out, err } diff --git a/deployment/data-feeds/changeset/stellar/ownership.go b/deployment/data-feeds/changeset/stellar/ownership.go index 46e76f8961b..5223eb54708 100644 --- a/deployment/data-feeds/changeset/stellar/ownership.go +++ b/deployment/data-feeds/changeset/stellar/ownership.go @@ -3,8 +3,6 @@ package stellar import ( "fmt" - "github.com/Masterminds/semver/v3" - "github.com/smartcontractkit/chainlink-deployments-framework/datastore" cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/smartcontractkit/chainlink-deployments-framework/operations" @@ -28,8 +26,8 @@ func (req *OwnershipRequest) verifyPreconditions(env cldf.Environment) error { if _, ok := env.BlockChains.StellarChains()[req.ChainSel]; !ok { return fmt.Errorf("stellar chain not found for chain selector %d", req.ChainSel) } - if req.Contract != CacheContract && req.Contract != ProxyContract { - return fmt.Errorf("unsupported contract type %q: must be %q or %q", req.Contract, CacheContract, ProxyContract) + if err := validateContract(req.Contract); err != nil { + return err } if err := verifyContractRef(env, req.ChainSel, req.Contract, req.Qualifier, req.Version); err != nil { return err @@ -37,13 +35,6 @@ func (req *OwnershipRequest) verifyPreconditions(env cldf.Environment) error { return nil } -// resolve looks up the target contract's dependencies + address for req. -func (req *OwnershipRequest) resolve(env cldf.Environment) (stellarApplyDeps, error) { - version := semver.MustParse(req.Version) - d, _, err := resolveContractDeps(env, req.ChainSel, req.Contract, req.Qualifier, version) - return d, err -} - var ( _ cldf.ChangeSetV2[*OwnershipRequest] = TransferOwnership{} _ cldf.ChangeSetV2[*OwnershipRequest] = AcceptOwnership{} @@ -68,7 +59,7 @@ func (TransferOwnership) VerifyPreconditions(env cldf.Environment, req *Ownershi func (TransferOwnership) Apply(env cldf.Environment, req *OwnershipRequest) (cldf.ChangesetOutput, error) { var out cldf.ChangesetOutput - d, err := req.resolve(env) + d, err := resolveDeps(env, req.ChainSel, req.Contract, req.Qualifier, req.Version) if err != nil { return out, err } @@ -91,7 +82,7 @@ func (AcceptOwnership) VerifyPreconditions(env cldf.Environment, req *OwnershipR func (AcceptOwnership) Apply(env cldf.Environment, req *OwnershipRequest) (cldf.ChangesetOutput, error) { var out cldf.ChangesetOutput - d, err := req.resolve(env) + d, err := resolveDeps(env, req.ChainSel, req.Contract, req.Qualifier, req.Version) if err != nil { return out, err } @@ -111,7 +102,7 @@ func (RenounceOwnership) VerifyPreconditions(env cldf.Environment, req *Ownershi func (RenounceOwnership) Apply(env cldf.Environment, req *OwnershipRequest) (cldf.ChangesetOutput, error) { var out cldf.ChangesetOutput - d, err := req.resolve(env) + d, err := resolveDeps(env, req.ChainSel, req.Contract, req.Qualifier, req.Version) if err != nil { return out, err } diff --git a/deployment/data-feeds/changeset/stellar/recover_tokens.go b/deployment/data-feeds/changeset/stellar/recover_tokens.go index 48e056bcd84..7a5f9be4c96 100644 --- a/deployment/data-feeds/changeset/stellar/recover_tokens.go +++ b/deployment/data-feeds/changeset/stellar/recover_tokens.go @@ -3,8 +3,6 @@ package stellar import ( "fmt" - "github.com/Masterminds/semver/v3" - "github.com/smartcontractkit/chainlink-deployments-framework/datastore" cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/smartcontractkit/chainlink-deployments-framework/operations" @@ -30,8 +28,8 @@ var _ cldf.ChangeSetV2[*RecoverTokensRequest] = RecoverTokens{} type RecoverTokens struct{} func (RecoverTokens) VerifyPreconditions(env cldf.Environment, req *RecoverTokensRequest) error { - if req.Contract != CacheContract && req.Contract != ProxyContract { - return fmt.Errorf("unsupported contract type %q: must be %q or %q", req.Contract, CacheContract, ProxyContract) + if err := validateContract(req.Contract); err != nil { + return err } if err := verifyContractRef(env, req.ChainSel, req.Contract, req.Qualifier, req.Version); err != nil { return err @@ -50,8 +48,7 @@ func (RecoverTokens) VerifyPreconditions(env cldf.Environment, req *RecoverToken func (RecoverTokens) Apply(env cldf.Environment, req *RecoverTokensRequest) (cldf.ChangesetOutput, error) { var out cldf.ChangesetOutput - version := semver.MustParse(req.Version) - d, _, err := resolveContractDeps(env, req.ChainSel, req.Contract, req.Qualifier, version) + d, err := resolveDeps(env, req.ChainSel, req.Contract, req.Qualifier, req.Version) if err != nil { return out, err } diff --git a/deployment/data-feeds/changeset/stellar/remove_feeds.go b/deployment/data-feeds/changeset/stellar/remove_feeds.go index 40af6049681..3a99fc05382 100644 --- a/deployment/data-feeds/changeset/stellar/remove_feeds.go +++ b/deployment/data-feeds/changeset/stellar/remove_feeds.go @@ -3,8 +3,6 @@ package stellar import ( "errors" - "github.com/Masterminds/semver/v3" - cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/smartcontractkit/chainlink-deployments-framework/operations" @@ -44,8 +42,7 @@ func (RemoveFeedConfigs) VerifyPreconditions(env cldf.Environment, req *RemoveFe func (RemoveFeedConfigs) Apply(env cldf.Environment, req *RemoveFeedConfigsRequest) (cldf.ChangesetOutput, error) { var out cldf.ChangesetOutput - version := semver.MustParse(req.Version) - d, _, err := resolveContractDeps(env, req.ChainSel, CacheContract, req.Qualifier, version) + d, err := resolveDeps(env, req.ChainSel, CacheContract, req.Qualifier, req.Version) if err != nil { return out, err } diff --git a/deployment/data-feeds/changeset/stellar/upgrade.go b/deployment/data-feeds/changeset/stellar/upgrade.go index e09d2ccd395..1765a693b81 100644 --- a/deployment/data-feeds/changeset/stellar/upgrade.go +++ b/deployment/data-feeds/changeset/stellar/upgrade.go @@ -4,8 +4,6 @@ import ( "fmt" "os" - "github.com/Masterminds/semver/v3" - "github.com/smartcontractkit/chainlink-deployments-framework/datastore" cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/smartcontractkit/chainlink-deployments-framework/operations" @@ -31,8 +29,8 @@ var _ cldf.ChangeSetV2[*UpgradeRequest] = Upgrade{} type Upgrade struct{} func (Upgrade) VerifyPreconditions(env cldf.Environment, req *UpgradeRequest) error { - if req.Contract != CacheContract && req.Contract != ProxyContract { - return fmt.Errorf("unsupported contract type %q: must be %q or %q", req.Contract, CacheContract, ProxyContract) + if err := validateContract(req.Contract); err != nil { + return err } if err := verifyContractRef(env, req.ChainSel, req.Contract, req.Qualifier, req.Version); err != nil { return err @@ -45,8 +43,7 @@ func (Upgrade) VerifyPreconditions(env cldf.Environment, req *UpgradeRequest) er func (Upgrade) Apply(env cldf.Environment, req *UpgradeRequest) (cldf.ChangesetOutput, error) { var out cldf.ChangesetOutput - version := semver.MustParse(req.Version) - d, _, err := resolveContractDeps(env, req.ChainSel, req.Contract, req.Qualifier, version) + d, err := resolveDeps(env, req.ChainSel, req.Contract, req.Qualifier, req.Version) if err != nil { return out, err } diff --git a/deployment/data-feeds/changeset/stellar/validation.go b/deployment/data-feeds/changeset/stellar/validation.go index 0f33d53ba3a..aa155a78721 100644 --- a/deployment/data-feeds/changeset/stellar/validation.go +++ b/deployment/data-feeds/changeset/stellar/validation.go @@ -6,9 +6,18 @@ import ( "math/big" "strings" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" "github.com/stellar/go-stellar-sdk/strkey" ) +// validateContract restricts a request's target to the two DF contract types. +func validateContract(t datastore.ContractType) error { + if t != CacheContract && t != ProxyContract { + return fmt.Errorf("unsupported contract type %q: must be %q or %q", t, CacheContract, ProxyContract) + } + return nil +} + // validateAddress accepts a Stellar account (G...) or contract (C...) strkey. func validateAddress(s string) error { if strkey.IsValidEd25519PublicKey(s) { From 4c3e7cafad7ace24ef33e442f6d51cfa1662b5ae Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Tue, 4 Aug 2026 19:34:35 +0100 Subject: [PATCH 04/19] Deduplicate the deploy flow into shared helpers - Add chainDeps, ownerOrSigner, and recordAddress to deps.go - DeployCache and DeployProxy now share the chain lookup, owner defaulting, and AddressRef recording they previously duplicated --- .../changeset/stellar/deploy_cache.go | 28 +++----------- .../changeset/stellar/deploy_proxy.go | 32 +++------------- .../data-feeds/changeset/stellar/deps.go | 38 +++++++++++++++++++ 3 files changed, 49 insertions(+), 49 deletions(-) diff --git a/deployment/data-feeds/changeset/stellar/deploy_cache.go b/deployment/data-feeds/changeset/stellar/deploy_cache.go index 647f72ed51b..f7fbfe86332 100644 --- a/deployment/data-feeds/changeset/stellar/deploy_cache.go +++ b/deployment/data-feeds/changeset/stellar/deploy_cache.go @@ -50,25 +50,16 @@ func (DeployCache) VerifyPreconditions(env cldf.Environment, req *DeployCacheReq func (DeployCache) Apply(env cldf.Environment, req *DeployCacheRequest) (cldf.ChangesetOutput, error) { var out cldf.ChangesetOutput - ch, ok := env.BlockChains.StellarChains()[req.ChainSel] - if !ok { - return out, fmt.Errorf("stellar chain not found for chain selector %d", req.ChainSel) + ch, deps, err := chainDeps(env, req.ChainSel) + if err != nil { + return out, err } - - deps, err := newStellarDeps(ch) + owner, err := ownerOrSigner(ch, req.Owner) if err != nil { return out, err } - owner := req.Owner - if owner == "" { - if ch.Signer == nil { - return out, fmt.Errorf("owner not set and chain has no signer") - } - owner = ch.Signer.Address() - } salt := stellardeploy.GenerateDeterministicSalt(owner, "data_feeds_cache-"+req.Qualifier) - report, err := operations.ExecuteOperation(env.OperationsBundle, operation.DeployCache, deps, operation.DeployCacheInput{ WasmPath: req.WasmPath, Salt: salt, @@ -77,14 +68,5 @@ func (DeployCache) Apply(env cldf.Environment, req *DeployCacheRequest) (cldf.Ch if err != nil { return out, err } - - out.DataStore = datastore.NewMemoryDataStore() - return out, out.DataStore.Addresses().Add(datastore.AddressRef{ - Address: report.Output.ContractID, - ChainSelector: req.ChainSel, - Type: CacheContract, - Version: semver.MustParse(req.Version), - Qualifier: req.Qualifier, - Labels: req.LabelSet, - }) + return recordAddress(report.Output.ContractID, req.ChainSel, CacheContract, req.Qualifier, req.Version, req.LabelSet) } diff --git a/deployment/data-feeds/changeset/stellar/deploy_proxy.go b/deployment/data-feeds/changeset/stellar/deploy_proxy.go index 12f108c86c6..1ba72de3913 100644 --- a/deployment/data-feeds/changeset/stellar/deploy_proxy.go +++ b/deployment/data-feeds/changeset/stellar/deploy_proxy.go @@ -54,33 +54,22 @@ func (DeployProxy) VerifyPreconditions(env cldf.Environment, req *DeployProxyReq func (DeployProxy) Apply(env cldf.Environment, req *DeployProxyRequest) (cldf.ChangesetOutput, error) { var out cldf.ChangesetOutput - ch, ok := env.BlockChains.StellarChains()[req.ChainSel] - if !ok { - return out, fmt.Errorf("stellar chain not found for chain selector %d", req.ChainSel) + ch, deps, err := chainDeps(env, req.ChainSel) + if err != nil { + return out, err } - - version := semver.MustParse(req.Version) cacheRef, err := env.DataStore.Addresses().Get( - datastore.NewAddressRefKey(req.ChainSel, CacheContract, version, req.CacheQualifier), + datastore.NewAddressRefKey(req.ChainSel, CacheContract, semver.MustParse(req.Version), req.CacheQualifier), ) if err != nil { return out, fmt.Errorf("cache address ref not found for qualifier %q: %w", req.CacheQualifier, err) } - - deps, err := newStellarDeps(ch) + owner, err := ownerOrSigner(ch, req.Owner) if err != nil { return out, err } - owner := req.Owner - if owner == "" { - if ch.Signer == nil { - return out, fmt.Errorf("owner not set and chain has no signer") - } - owner = ch.Signer.Address() - } salt := stellardeploy.GenerateDeterministicSalt(owner, "data_feeds_proxy-"+req.Qualifier) - report, err := operations.ExecuteOperation(env.OperationsBundle, operation.DeployProxy, deps, operation.DeployProxyInput{ WasmPath: req.WasmPath, Salt: salt, @@ -90,14 +79,5 @@ func (DeployProxy) Apply(env cldf.Environment, req *DeployProxyRequest) (cldf.Ch if err != nil { return out, err } - - out.DataStore = datastore.NewMemoryDataStore() - return out, out.DataStore.Addresses().Add(datastore.AddressRef{ - Address: report.Output.ContractID, - ChainSelector: req.ChainSel, - Type: ProxyContract, - Version: version, - Qualifier: req.Qualifier, - Labels: req.LabelSet, - }) + return recordAddress(report.Output.ContractID, req.ChainSel, ProxyContract, req.Qualifier, req.Version, req.LabelSet) } diff --git a/deployment/data-feeds/changeset/stellar/deps.go b/deployment/data-feeds/changeset/stellar/deps.go index a01eeb871b7..e793e49ca2b 100644 --- a/deployment/data-feeds/changeset/stellar/deps.go +++ b/deployment/data-feeds/changeset/stellar/deps.go @@ -1,6 +1,7 @@ package stellar import ( + "errors" "fmt" "github.com/Masterminds/semver/v3" @@ -23,6 +24,43 @@ var newStellarDeps = func(ch cldfstellar.Chain) (operation.StellarDeps, error) { return operation.StellarDeps{Deploy: d, Invoker: d}, nil } +// chainDeps resolves the CLDF Stellar chain for chainSel and builds its +// deploy/invoke deps. +func chainDeps(env cldf.Environment, chainSel uint64) (cldfstellar.Chain, operation.StellarDeps, error) { + ch, ok := env.BlockChains.StellarChains()[chainSel] + if !ok { + return cldfstellar.Chain{}, operation.StellarDeps{}, fmt.Errorf("stellar chain not found for chain selector %d", chainSel) + } + deps, err := newStellarDeps(ch) + return ch, deps, err +} + +// ownerOrSigner defaults an empty owner to the chain's deployer signer. +func ownerOrSigner(ch cldfstellar.Chain, owner string) (string, error) { + if owner != "" { + return owner, nil + } + if ch.Signer == nil { + return "", errors.New("owner not set and chain has no signer") + } + return ch.Signer.Address(), nil +} + +// recordAddress returns a ChangesetOutput whose datastore holds the deployed +// contract's AddressRef. +func recordAddress(address string, chainSel uint64, contractType datastore.ContractType, qualifier, version string, labels datastore.LabelSet) (cldf.ChangesetOutput, error) { + var out cldf.ChangesetOutput + out.DataStore = datastore.NewMemoryDataStore() + return out, out.DataStore.Addresses().Add(datastore.AddressRef{ + Address: address, + ChainSelector: chainSel, + Type: contractType, + Version: semver.MustParse(version), + Qualifier: qualifier, + Labels: labels, + }) +} + // stellarApplyDeps bundles a resolved contract's address with the chain deps // needed to invoke operations against it. type stellarApplyDeps struct { From 895219136e257df470b78f03418931850c66fe4b Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Tue, 4 Aug 2026 19:38:44 +0100 Subject: [PATCH 05/19] Drop unused test scaffolding - Remove fakeDeployer.DeployContract: not part of the SorobanContractDeployer interface and never called - Reuse the e2e's cache feed id for proxy reads instead of an identical pdid copy --- deployment/data-feeds/changeset/stellar/e2e_test.go | 9 ++++----- deployment/data-feeds/changeset/stellar/testutil_test.go | 4 ---- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/deployment/data-feeds/changeset/stellar/e2e_test.go b/deployment/data-feeds/changeset/stellar/e2e_test.go index e90296191b4..fa578ddf1df 100644 --- a/deployment/data-feeds/changeset/stellar/e2e_test.go +++ b/deployment/data-feeds/changeset/stellar/e2e_test.go @@ -281,15 +281,14 @@ func TestStellarDataFeedsE2E(t *testing.T) { // --- proxy reads (read through to the cache's stored round) ----------- proxyClient, _, err := LoadProxyClient(env, sel, e2eQualifier, e2eVersion) require.NoError(t, err) - pdid := [16]byte(ids[0]) mustCall(t, "proxy.version", func() error { _, e := proxyClient.Version(ctx); return e }) mustCall(t, "proxy.type_and_version", func() error { _, e := proxyClient.TypeAndVersion(ctx); return e }) mustCall(t, "proxy.get_owner", func() error { _, e := proxyClient.GetOwner(ctx); return e }) - mustCall(t, "proxy.decimals", func() error { _, e := proxyClient.Decimals(ctx, pdid); return e }) - mustCall(t, "proxy.description", func() error { _, e := proxyClient.Description(ctx, pdid); return e }) - mustCall(t, "proxy.latest_round", func() error { _, e := proxyClient.LatestRound(ctx, pdid); return e }) - mustCall(t, "proxy.get_round", func() error { _, e := proxyClient.GetRound(ctx, pdid, 1); return e }) + mustCall(t, "proxy.decimals", func() error { _, e := proxyClient.Decimals(ctx, did); return e }) + mustCall(t, "proxy.description", func() error { _, e := proxyClient.Description(ctx, did); return e }) + mustCall(t, "proxy.latest_round", func() error { _, e := proxyClient.LatestRound(ctx, did); return e }) + mustCall(t, "proxy.get_round", func() error { _, e := proxyClient.GetRound(ctx, did, 1); return e }) // --- proxy upgrade + recover_tokens ------------------------------------ env = apply(t, env, Upgrade{}, &UpgradeRequest{ diff --git a/deployment/data-feeds/changeset/stellar/testutil_test.go b/deployment/data-feeds/changeset/stellar/testutil_test.go index a01380251da..81f485dd18f 100644 --- a/deployment/data-feeds/changeset/stellar/testutil_test.go +++ b/deployment/data-feeds/changeset/stellar/testutil_test.go @@ -46,10 +46,6 @@ type fakeDeployer struct { uploads []string // wasm paths passed to UploadContractWASM, in call order } -func (f *fakeDeployer) DeployContract(ctx context.Context, wasmPath string, salt [32]byte) (string, error) { - return f.DeployContractWithArgs(ctx, wasmPath, salt, nil) -} - func (f *fakeDeployer) DeployContractWithArgs(_ context.Context, wasmPath string, salt [32]byte, args []xdr.ScVal) (string, error) { f.deploys = append(f.deploys, deployCall{wasmPath, salt, args}) return f.contractID, nil From e56b5cd367482814490c693e62923da6ade41425 Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Tue, 4 Aug 2026 19:42:42 +0100 Subject: [PATCH 06/19] Collapse client selection into a shared admin interface The five cache-or-proxy operations repeated the same IsProxy branch; contractAdmin captures the ownership/upgrade/recovery surface both generated clients share, and adminClient picks the right one. --- .../changeset/stellar/operation/operation.go | 53 ++++++++----------- 1 file changed, 23 insertions(+), 30 deletions(-) diff --git a/deployment/data-feeds/changeset/stellar/operation/operation.go b/deployment/data-feeds/changeset/stellar/operation/operation.go index 1287af9051b..9fdcf350d0e 100644 --- a/deployment/data-feeds/changeset/stellar/operation/operation.go +++ b/deployment/data-feeds/changeset/stellar/operation/operation.go @@ -33,6 +33,24 @@ type StellarDeps struct { Invoker bindings.Invoker } +// contractAdmin is the ownership/upgrade/recovery surface shared by the +// generated cache and proxy clients. +type contractAdmin interface { + TransferOwnership(ctx context.Context, newOwner string, liveUntilLedger uint32) error + AcceptOwnership(ctx context.Context) error + RenounceOwnership(ctx context.Context) error + Upgrade(ctx context.Context, newWasmHash [32]byte) error + RecoverTokens(ctx context.Context, token, to string, amount int64) error +} + +// adminClient returns the generated client selected by isProxy. +func adminClient(d StellarDeps, contractID string, isProxy bool) contractAdmin { + if isProxy { + return proxy.NewDataFeedsProxyClient(d.Invoker, contractID) + } + return cache.NewDataFeedsCacheClient(d.Invoker, contractID) +} + // Void is the output for operations that return no payload. type Void struct{} @@ -171,12 +189,7 @@ var TransferOwnership = cldfops.NewOperation( "df:transfer-ownership", Version1_0_0, "Begins two-step ownership transfer", func(b cldfops.Bundle, d StellarDeps, in OwnershipInput) (Void, error) { - if in.IsProxy { - c := proxy.NewDataFeedsProxyClient(d.Invoker, in.ContractID) - return Void{}, c.TransferOwnership(b.GetContext(), in.NewOwner, in.LiveUntilLedger) - } - c := cache.NewDataFeedsCacheClient(d.Invoker, in.ContractID) - return Void{}, c.TransferOwnership(b.GetContext(), in.NewOwner, in.LiveUntilLedger) + return Void{}, adminClient(d, in.ContractID, in.IsProxy).TransferOwnership(b.GetContext(), in.NewOwner, in.LiveUntilLedger) }, ) @@ -184,12 +197,7 @@ var AcceptOwnership = cldfops.NewOperation( "df:accept-ownership", Version1_0_0, "Accepts a pending ownership transfer (caller must be the pending owner)", func(b cldfops.Bundle, d StellarDeps, in OwnershipInput) (Void, error) { - if in.IsProxy { - c := proxy.NewDataFeedsProxyClient(d.Invoker, in.ContractID) - return Void{}, c.AcceptOwnership(b.GetContext()) - } - c := cache.NewDataFeedsCacheClient(d.Invoker, in.ContractID) - return Void{}, c.AcceptOwnership(b.GetContext()) + return Void{}, adminClient(d, in.ContractID, in.IsProxy).AcceptOwnership(b.GetContext()) }, ) @@ -197,12 +205,7 @@ var RenounceOwnership = cldfops.NewOperation( "df:renounce-ownership", Version1_0_0, "Renounces ownership permanently", func(b cldfops.Bundle, d StellarDeps, in OwnershipInput) (Void, error) { - if in.IsProxy { - c := proxy.NewDataFeedsProxyClient(d.Invoker, in.ContractID) - return Void{}, c.RenounceOwnership(b.GetContext()) - } - c := cache.NewDataFeedsCacheClient(d.Invoker, in.ContractID) - return Void{}, c.RenounceOwnership(b.GetContext()) + return Void{}, adminClient(d, in.ContractID, in.IsProxy).RenounceOwnership(b.GetContext()) }, ) @@ -235,12 +238,7 @@ var UpgradeContract = cldfops.NewOperation( "df:upgrade", Version1_0_0, "Points the contract at a new WASM implementation", func(b cldfops.Bundle, d StellarDeps, in UpgradeContractInput) (Void, error) { - if in.IsProxy { - c := proxy.NewDataFeedsProxyClient(d.Invoker, in.ContractID) - return Void{}, c.Upgrade(b.GetContext(), in.NewWasmHash) - } - c := cache.NewDataFeedsCacheClient(d.Invoker, in.ContractID) - return Void{}, c.Upgrade(b.GetContext(), in.NewWasmHash) + return Void{}, adminClient(d, in.ContractID, in.IsProxy).Upgrade(b.GetContext(), in.NewWasmHash) }, ) @@ -256,12 +254,7 @@ var RecoverTokens = cldfops.NewOperation( "df:recover-tokens", Version1_0_0, "Recovers tokens accidentally sent to the contract", func(b cldfops.Bundle, d StellarDeps, in RecoverTokensInput) (Void, error) { - if in.IsProxy { - c := proxy.NewDataFeedsProxyClient(d.Invoker, in.ContractID) - return Void{}, c.RecoverTokens(b.GetContext(), in.Token, in.To, in.Amount) - } - c := cache.NewDataFeedsCacheClient(d.Invoker, in.ContractID) - return Void{}, c.RecoverTokens(b.GetContext(), in.Token, in.To, in.Amount) + return Void{}, adminClient(d, in.ContractID, in.IsProxy).RecoverTokens(b.GetContext(), in.Token, in.To, in.Amount) }, ) From 6e001850db068559957a1f4dc20faefa7c9c3d06 Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Tue, 4 Aug 2026 19:47:13 +0100 Subject: [PATCH 07/19] Unify contract resolution on one string-version path - resolveContractDeps now parses the version itself and reuses chainDeps, removing the duplicated chain lookup and ref-error text - Delete state.go's loadContractClientDeps; the client loaders call resolveContractDeps directly --- .../data-feeds/changeset/stellar/deps.go | 30 ++++++++----------- .../changeset/stellar/set_proxy_cache.go | 6 ++-- .../data-feeds/changeset/stellar/state.go | 18 ++--------- 3 files changed, 16 insertions(+), 38 deletions(-) diff --git a/deployment/data-feeds/changeset/stellar/deps.go b/deployment/data-feeds/changeset/stellar/deps.go index e793e49ca2b..4e3ddcd6219 100644 --- a/deployment/data-feeds/changeset/stellar/deps.go +++ b/deployment/data-feeds/changeset/stellar/deps.go @@ -87,34 +87,28 @@ func verifyContractRef(env cldf.Environment, chainSel uint64, contractType datas return nil } -// resolveDeps is resolveContractDeps for callers that don't need the -// AddressRef; version must already be validated by VerifyPreconditions. +// resolveDeps is resolveContractDeps for callers that don't need the AddressRef. func resolveDeps(env cldf.Environment, chainSel uint64, contractType datastore.ContractType, qualifier, version string) (stellarApplyDeps, error) { - d, _, err := resolveContractDeps(env, chainSel, contractType, qualifier, semver.MustParse(version)) + d, _, err := resolveContractDeps(env, chainSel, contractType, qualifier, version) return d, err } // resolveContractDeps looks up the AddressRef for (chainSel, contractType, // version, qualifier) and bundles it with the chain's deploy/invoke deps. -func resolveContractDeps(env cldf.Environment, chainSel uint64, contractType datastore.ContractType, qualifier string, version *semver.Version) (stellarApplyDeps, datastore.AddressRef, error) { - var zero stellarApplyDeps - var zeroRef datastore.AddressRef - ch, ok := env.BlockChains.StellarChains()[chainSel] - if !ok { - return zero, zeroRef, fmt.Errorf("stellar chain not found for chain selector %d", chainSel) +func resolveContractDeps(env cldf.Environment, chainSel uint64, contractType datastore.ContractType, qualifier, version string) (stellarApplyDeps, datastore.AddressRef, error) { + v, err := semver.NewVersion(version) + if err != nil { + return stellarApplyDeps{}, datastore.AddressRef{}, fmt.Errorf("invalid version %q: %w", version, err) } - - ref, err := env.DataStore.Addresses().Get( - datastore.NewAddressRefKey(chainSel, contractType, version, qualifier), - ) + _, deps, err := chainDeps(env, chainSel) if err != nil { - return zero, zeroRef, fmt.Errorf("%s address ref not found for qualifier %q: %w", contractType, qualifier, err) + return stellarApplyDeps{}, datastore.AddressRef{}, err } - - deps, err := newStellarDeps(ch) + ref, err := env.DataStore.Addresses().Get( + datastore.NewAddressRefKey(chainSel, contractType, v, qualifier), + ) if err != nil { - return zero, zeroRef, err + return stellarApplyDeps{}, datastore.AddressRef{}, fmt.Errorf("%s address ref not found for qualifier %q: %w", contractType, qualifier, err) } - return stellarApplyDeps{deps: deps, contractID: ref.Address}, ref, nil } diff --git a/deployment/data-feeds/changeset/stellar/set_proxy_cache.go b/deployment/data-feeds/changeset/stellar/set_proxy_cache.go index 3ebf01d12ad..81339e2a14b 100644 --- a/deployment/data-feeds/changeset/stellar/set_proxy_cache.go +++ b/deployment/data-feeds/changeset/stellar/set_proxy_cache.go @@ -40,14 +40,12 @@ func (SetProxyCache) VerifyPreconditions(env cldf.Environment, req *SetProxyCach func (SetProxyCache) Apply(env cldf.Environment, req *SetProxyCacheRequest) (cldf.ChangesetOutput, error) { var out cldf.ChangesetOutput - version := semver.MustParse(req.Version) - - proxyDeps, _, err := resolveContractDeps(env, req.ChainSel, ProxyContract, req.Qualifier, version) + proxyDeps, err := resolveDeps(env, req.ChainSel, ProxyContract, req.Qualifier, req.Version) if err != nil { return out, err } cacheRef, err := env.DataStore.Addresses().Get( - datastore.NewAddressRefKey(req.ChainSel, CacheContract, version, req.CacheQualifier), + datastore.NewAddressRefKey(req.ChainSel, CacheContract, semver.MustParse(req.Version), req.CacheQualifier), ) if err != nil { return out, fmt.Errorf("cache address ref not found for qualifier %q: %w", req.CacheQualifier, err) diff --git a/deployment/data-feeds/changeset/stellar/state.go b/deployment/data-feeds/changeset/stellar/state.go index 94488fb3a32..d9c5bc20036 100644 --- a/deployment/data-feeds/changeset/stellar/state.go +++ b/deployment/data-feeds/changeset/stellar/state.go @@ -1,30 +1,16 @@ package stellar import ( - "fmt" - - "github.com/Masterminds/semver/v3" - "github.com/smartcontractkit/chainlink-deployments-framework/datastore" cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" cache "github.com/smartcontractkit/chainlink-stellar/bindings/contracts/data_feeds_cache" proxy "github.com/smartcontractkit/chainlink-stellar/bindings/contracts/data_feeds_proxy" ) -// loadContractClientDeps parses version and resolves the contract's AddressRef -// plus chain deps — the shared lookup behind LoadCacheClient and LoadProxyClient. -func loadContractClientDeps(env cldf.Environment, chainSel uint64, contractType datastore.ContractType, qualifier, version string) (stellarApplyDeps, datastore.AddressRef, error) { - v, err := semver.NewVersion(version) - if err != nil { - return stellarApplyDeps{}, datastore.AddressRef{}, fmt.Errorf("invalid version %q: %w", version, err) - } - return resolveContractDeps(env, chainSel, contractType, qualifier, v) -} - // LoadCacheClient returns a generated cache client bound to the resolved // CacheContract AddressRef for (chainSel, qualifier, version). func LoadCacheClient(env cldf.Environment, chainSel uint64, qualifier, version string) (*cache.DataFeedsCacheClient, datastore.AddressRef, error) { - d, ref, err := loadContractClientDeps(env, chainSel, CacheContract, qualifier, version) + d, ref, err := resolveContractDeps(env, chainSel, CacheContract, qualifier, version) if err != nil { return nil, datastore.AddressRef{}, err } @@ -33,7 +19,7 @@ func LoadCacheClient(env cldf.Environment, chainSel uint64, qualifier, version s // LoadProxyClient is LoadCacheClient's counterpart for ProxyContract. func LoadProxyClient(env cldf.Environment, chainSel uint64, qualifier, version string) (*proxy.DataFeedsProxyClient, datastore.AddressRef, error) { - d, ref, err := loadContractClientDeps(env, chainSel, ProxyContract, qualifier, version) + d, ref, err := resolveContractDeps(env, chainSel, ProxyContract, qualifier, version) if err != nil { return nil, datastore.AddressRef{}, err } From 3861a5d8ae0904809e8363011d9420d47b5e0b12 Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Tue, 4 Aug 2026 19:50:31 +0100 Subject: [PATCH 08/19] Fix stale package name in the suite README --- deployment/data-feeds/changeset/stellar/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/deployment/data-feeds/changeset/stellar/README.md b/deployment/data-feeds/changeset/stellar/README.md index 6ef8ea26b1a..6ed3c909332 100644 --- a/deployment/data-feeds/changeset/stellar/README.md +++ b/deployment/data-feeds/changeset/stellar/README.md @@ -25,14 +25,14 @@ data-feeds/changeset/stellar/ Each changeset's `Apply` resolves a `datastore.AddressRef` (chain selector + `ContractType` + version + qualifier) to a `stellarApplyDeps` bundle — the -contract ID plus the `stellardeps.StellarDeps` (Deploy + Invoker) needed to +contract ID plus the `operation.StellarDeps` (Deploy + Invoker) needed to call it — then executes one or more `operation.*` calls from `operation/operation.go` through `env.OperationsBundle`. Operations are thin: each wraps exactly one generated-binding call (`chainlink-stellar/bindings/contracts/data_feeds_{cache,proxy}`) or one -`stellardeps.Deploy` call. `deps.go`'s `verifyContractRef` / -`resolveContractDeps` hold the chain-exists + version-parse + ref-exists + -build-deps skeleton shared by every changeset in this package. +deployer call. `deps.go`'s `verifyContractRef` / `resolveContractDeps` hold +the chain-exists + version-parse + ref-exists + build-deps skeleton shared by +every changeset in this package. #### Permissions model — no standalone "set forwarder" From 3df63ab2101d23ee47f407911ad02ae0614b9ccf Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Tue, 4 Aug 2026 19:55:38 +0100 Subject: [PATCH 09/19] Use errors.New for static precondition messages --- deployment/data-feeds/changeset/stellar/deploy_proxy.go | 3 ++- deployment/data-feeds/changeset/stellar/ownership.go | 3 ++- deployment/data-feeds/changeset/stellar/recover_tokens.go | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/deployment/data-feeds/changeset/stellar/deploy_proxy.go b/deployment/data-feeds/changeset/stellar/deploy_proxy.go index 1ba72de3913..43dc6b4f276 100644 --- a/deployment/data-feeds/changeset/stellar/deploy_proxy.go +++ b/deployment/data-feeds/changeset/stellar/deploy_proxy.go @@ -1,6 +1,7 @@ package stellar import ( + "errors" "fmt" "os" @@ -47,7 +48,7 @@ func (DeployProxy) VerifyPreconditions(env cldf.Environment, req *DeployProxyReq } } if req.CacheQualifier == "" { - return fmt.Errorf("cache qualifier must be set") + return errors.New("cache qualifier must be set") } return nil } diff --git a/deployment/data-feeds/changeset/stellar/ownership.go b/deployment/data-feeds/changeset/stellar/ownership.go index 5223eb54708..c724300ee6e 100644 --- a/deployment/data-feeds/changeset/stellar/ownership.go +++ b/deployment/data-feeds/changeset/stellar/ownership.go @@ -1,6 +1,7 @@ package stellar import ( + "errors" "fmt" "github.com/smartcontractkit/chainlink-deployments-framework/datastore" @@ -52,7 +53,7 @@ func (TransferOwnership) VerifyPreconditions(env cldf.Environment, req *Ownershi return fmt.Errorf("new owner: %w", err) } if req.LiveUntilLedger == 0 { - return fmt.Errorf("LiveUntilLedger must be nonzero") + return errors.New("LiveUntilLedger must be nonzero") } return nil } diff --git a/deployment/data-feeds/changeset/stellar/recover_tokens.go b/deployment/data-feeds/changeset/stellar/recover_tokens.go index 7a5f9be4c96..4c451458301 100644 --- a/deployment/data-feeds/changeset/stellar/recover_tokens.go +++ b/deployment/data-feeds/changeset/stellar/recover_tokens.go @@ -1,6 +1,7 @@ package stellar import ( + "errors" "fmt" "github.com/smartcontractkit/chainlink-deployments-framework/datastore" @@ -41,7 +42,7 @@ func (RecoverTokens) VerifyPreconditions(env cldf.Environment, req *RecoverToken return fmt.Errorf("to: %w", err) } if req.Amount <= 0 { - return fmt.Errorf("amount must be positive") + return errors.New("amount must be positive") } return nil } From 39166591c8143928eaac4992f7ab20a729294d6a Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Wed, 5 Aug 2026 12:37:50 +0100 Subject: [PATCH 10/19] Restructure into one file per changeset action - Merge deploy_cache.go and deploy_proxy.go into deploy.go - Rename configure_cache, remove_feeds, and freeze_feeds to match their changeset names (set_feed_configs, remove_feed_configs, set_feed_frozen) - Split changeset_test.go into per-action test files, mirroring the Aptos suite; shared fakes, env builder, and datastore seeding move to testutil_test.go --- .../data-feeds/changeset/stellar/README.md | 10 +- .../changeset/stellar/changeset_test.go | 693 ------------------ .../stellar/{deploy_proxy.go => deploy.go} | 57 ++ .../changeset/stellar/deploy_cache.go | 72 -- .../changeset/stellar/deploy_test.go | 141 ++++ .../changeset/stellar/feed_admin_test.go | 45 ++ .../changeset/stellar/ownership_test.go | 100 +++ .../changeset/stellar/recover_tokens_test.go | 69 ++ ...remove_feeds.go => remove_feed_configs.go} | 0 .../stellar/remove_feed_configs_test.go | 42 ++ ...configure_cache.go => set_feed_configs.go} | 0 .../stellar/set_feed_configs_test.go | 75 ++ .../{freeze_feeds.go => set_feed_frozen.go} | 0 .../changeset/stellar/set_feed_frozen_test.go | 43 ++ .../changeset/stellar/set_proxy_cache_test.go | 40 + .../changeset/stellar/testutil_test.go | 125 ++++ .../changeset/stellar/upgrade_test.go | 76 ++ 17 files changed, 820 insertions(+), 768 deletions(-) delete mode 100644 deployment/data-feeds/changeset/stellar/changeset_test.go rename deployment/data-feeds/changeset/stellar/{deploy_proxy.go => deploy.go} (60%) delete mode 100644 deployment/data-feeds/changeset/stellar/deploy_cache.go create mode 100644 deployment/data-feeds/changeset/stellar/deploy_test.go create mode 100644 deployment/data-feeds/changeset/stellar/feed_admin_test.go create mode 100644 deployment/data-feeds/changeset/stellar/ownership_test.go create mode 100644 deployment/data-feeds/changeset/stellar/recover_tokens_test.go rename deployment/data-feeds/changeset/stellar/{remove_feeds.go => remove_feed_configs.go} (100%) create mode 100644 deployment/data-feeds/changeset/stellar/remove_feed_configs_test.go rename deployment/data-feeds/changeset/stellar/{configure_cache.go => set_feed_configs.go} (100%) create mode 100644 deployment/data-feeds/changeset/stellar/set_feed_configs_test.go rename deployment/data-feeds/changeset/stellar/{freeze_feeds.go => set_feed_frozen.go} (100%) create mode 100644 deployment/data-feeds/changeset/stellar/set_feed_frozen_test.go create mode 100644 deployment/data-feeds/changeset/stellar/set_proxy_cache_test.go create mode 100644 deployment/data-feeds/changeset/stellar/upgrade_test.go diff --git a/deployment/data-feeds/changeset/stellar/README.md b/deployment/data-feeds/changeset/stellar/README.md index 6ed3c909332..8106c8f30e0 100644 --- a/deployment/data-feeds/changeset/stellar/README.md +++ b/deployment/data-feeds/changeset/stellar/README.md @@ -10,9 +10,10 @@ are EVM/JD-specific and deliberately have no Stellar equivalent here). ``` data-feeds/changeset/stellar/ -├── deploy_cache.go, deploy_proxy.go # instantiate contracts, record AddressRefs -├── configure_cache.go # SetFeedConfigs — descriptions + permissions -├── remove_feeds.go, freeze_feeds.go # remove feed configs, freeze/unfreeze feeds +├── deploy.go # DeployCache / DeployProxy — instantiate + record AddressRefs +├── set_feed_configs.go # descriptions + workflow permissions +├── remove_feed_configs.go # remove feed configs +├── set_feed_frozen.go # freeze/unfreeze feeds ├── feed_admin.go # grant/revoke feed-admin ├── ownership.go # transfer / accept / renounce (cache + proxy) ├── upgrade.go, recover_tokens.go # new WASM / token recovery (cache + proxy) @@ -23,6 +24,9 @@ data-feeds/changeset/stellar/ └── testdata/ # e2e WASM fixtures — see testdata/README.md ``` +Every action file has a matching `_test.go`; shared test scaffolding (fake +deps, env builder, datastore seeding) lives in `testutil_test.go`. + Each changeset's `Apply` resolves a `datastore.AddressRef` (chain selector + `ContractType` + version + qualifier) to a `stellarApplyDeps` bundle — the contract ID plus the `operation.StellarDeps` (Deploy + Invoker) needed to diff --git a/deployment/data-feeds/changeset/stellar/changeset_test.go b/deployment/data-feeds/changeset/stellar/changeset_test.go deleted file mode 100644 index 71450da9715..00000000000 --- a/deployment/data-feeds/changeset/stellar/changeset_test.go +++ /dev/null @@ -1,693 +0,0 @@ -package stellar - -import ( - "context" - "os" - "path/filepath" - "testing" - - "github.com/Masterminds/semver/v3" - chainsel "github.com/smartcontractkit/chain-selectors" - "github.com/stellar/go-stellar-sdk/keypair" - "github.com/stellar/go-stellar-sdk/xdr" - "github.com/stretchr/testify/require" - - cldfchain "github.com/smartcontractkit/chainlink-deployments-framework/chain" - cldfstellar "github.com/smartcontractkit/chainlink-deployments-framework/chain/stellar" - "github.com/smartcontractkit/chainlink-deployments-framework/datastore" - cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" - "github.com/smartcontractkit/chainlink-deployments-framework/offchain/ocr" - cldflogger "github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger" - "github.com/smartcontractkit/chainlink-stellar/bindings/scval" - stellardeploy "github.com/smartcontractkit/chainlink-stellar/deployment" - - "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/stellar/operation" -) - -const testContractID = "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA" - -// testChainSel is a real Stellar chain selector (from chain-selectors) so -// that ChainMetadata methods (Family, NetworkType, ...) resolve correctly. -var testChainSel = chainsel.STELLAR_TESTNET.Selector - -// newTestEnv returns an Environment with one fake Stellar chain and swapped-in fake deps. -// It swaps the package-level newStellarDeps seam to hand back fakeInvoker/fakeDeployer -// instead of talking to a real Soroban RPC endpoint, and restores the seam on cleanup. -func newTestEnv(t *testing.T) (cldf.Environment, *fakeInvoker, *fakeDeployer) { - t.Helper() - - kp, err := keypair.Random() - require.NoError(t, err) - - chain := cldfstellar.Chain{ - ChainMetadata: cldfstellar.ChainMetadata{Selector: testChainSel}, - Signer: cldfstellar.NewStellarKeypairSigner(kp), - } - - invoker := &fakeInvoker{} - deployer := &fakeDeployer{contractID: testContractID} - - origDeps := newStellarDeps - t.Cleanup(func() { newStellarDeps = origDeps }) - newStellarDeps = func(_ cldfstellar.Chain) (operation.StellarDeps, error) { - return operation.StellarDeps{Deploy: deployer, Invoker: invoker}, nil - } - - blockChains := cldfchain.NewBlockChains(map[uint64]cldfchain.BlockChain{ - testChainSel: chain, - }) - - env := cldf.NewEnvironment( - "test", - cldflogger.Test(t), - nil, // ExistingAddresses: unused by these changesets, DataStore is authoritative - datastore.NewMemoryDataStore().Seal(), - nil, - nil, - func() context.Context { return context.Background() }, - ocr.OCRSecrets{}, - blockChains, - ) - - return *env, invoker, deployer -} - -// contractRefSpec describes one AddressRef to seed via seedContractRefs. -type contractRefSpec struct { - contractType datastore.ContractType - address string - qualifier string - version string -} - -// seedContractRefs pre-seeds env.DataStore with the given AddressRefs (all in -// one MemoryDataStore) so changesets can resolve contracts by -// (ChainSel, Type, Version, Qualifier). Replaces any refs seeded by a prior -// call on the same env — call once per test with every ref the test needs -// (e.g. both a proxy and a cache ref for SetProxyCache). -func seedContractRefs(t *testing.T, env *cldf.Environment, refs ...contractRefSpec) { - t.Helper() - ds := datastore.NewMemoryDataStore() - for _, r := range refs { - require.NoError(t, ds.Addresses().Add(datastore.AddressRef{ - Address: r.address, - ChainSelector: testChainSel, - Type: r.contractType, - Version: semver.MustParse(r.version), - Qualifier: r.qualifier, - })) - } - env.DataStore = ds.Seal() -} - -// seedCacheRef pre-seeds env.DataStore with a single cache AddressRef so -// config changesets (SetFeedConfigs, RemoveFeedConfigs, Add/RemoveFeedAdmin) -// can resolve the cache contract by (ChainSel, CacheContract, Version, Qualifier). -func seedCacheRef(t *testing.T, env *cldf.Environment, address, qualifier, version string) { - t.Helper() - seedContractRefs(t, env, contractRefSpec{CacheContract, address, qualifier, version}) -} - -// seedProxyRef pre-seeds env.DataStore with a single proxy AddressRef so -// changesets can resolve the proxy contract by -// (ChainSel, ProxyContract, Version, Qualifier). -func seedProxyRef(t *testing.T, env *cldf.Environment, address, qualifier, version string) { - t.Helper() - seedContractRefs(t, env, contractRefSpec{ProxyContract, address, qualifier, version}) -} - -// writeDummyWasm writes a placeholder wasm file under t.TempDir() so -// VerifyPreconditions' filesystem check (os.Stat) has a real path to find. -// The contents are never read — deployment is faked via fakeDeployer. -func writeDummyWasm(t *testing.T, name string) string { - t.Helper() - path := filepath.Join(t.TempDir(), name) - require.NoError(t, os.WriteFile(path, []byte("dummy wasm"), 0o600)) - return path -} - -func TestDeployCacheChangeset(t *testing.T) { - env, _, dep := newTestEnv(t) - - req := &DeployCacheRequest{ - ChainSel: testChainSel, - WasmPath: writeDummyWasm(t, "data_feeds_cache.wasm"), - Qualifier: "test-cache", - Version: "1.0.0", - } - - require.NoError(t, DeployCache{}.VerifyPreconditions(env, req)) - - out, err := DeployCache{}.Apply(env, req) - require.NoError(t, err) - require.NotNil(t, out.DataStore) - - require.Len(t, dep.deploys, 1) - require.Len(t, dep.deploys[0].Args, 1) // owner ctor arg - - owner := env.BlockChains.StellarChains()[testChainSel].Signer.Address() - wantSalt := stellardeploy.GenerateDeterministicSalt(owner, "data_feeds_cache-"+req.Qualifier) - require.Equal(t, wantSalt, dep.deploys[0].Salt) - - key := datastore.NewAddressRefKey(testChainSel, CacheContract, semver.MustParse("1.0.0"), "test-cache") - ref, err := out.DataStore.Addresses().Get(key) - require.NoError(t, err) - require.Equal(t, testContractID, ref.Address) - - // invalid explicit Owner must fail preconditions - badOwner := *req - badOwner.Owner = "not-a-key" - require.Error(t, DeployCache{}.VerifyPreconditions(env, &badOwner)) -} - -func TestDeployCacheChangeset_MissingChain(t *testing.T) { - env, _, _ := newTestEnv(t) - - req := &DeployCacheRequest{ - ChainSel: 999999, - WasmPath: writeDummyWasm(t, "data_feeds_cache.wasm"), - Qualifier: "test-cache", - Version: "1.0.0", - } - - require.Error(t, DeployCache{}.VerifyPreconditions(env, req)) -} - -// TestDeployCacheChangeset_ExplicitOwner pins that an explicit req.Owner -// seeds the deploy salt directly. deploy_cache.go's Apply only falls back to -// ch.Signer.Address() when req.Owner is empty; when it's set, the salt is -// GenerateDeterministicSalt(req.Owner, ...) -- not the chain signer's address. -func TestDeployCacheChangeset_ExplicitOwner(t *testing.T) { - env, _, dep := newTestEnv(t) - - req := &DeployCacheRequest{ - ChainSel: testChainSel, - WasmPath: writeDummyWasm(t, "data_feeds_cache.wasm"), - Owner: testAdmin, - Qualifier: "test-cache-owner", - Version: "1.0.0", - } - - require.NoError(t, DeployCache{}.VerifyPreconditions(env, req)) - - out, err := DeployCache{}.Apply(env, req) - require.NoError(t, err) - require.NotNil(t, out.DataStore) - - require.Len(t, dep.deploys, 1) - require.Len(t, dep.deploys[0].Args, 1) // owner ctor arg - - wantSalt := stellardeploy.GenerateDeterministicSalt(testAdmin, "data_feeds_cache-"+req.Qualifier) - require.Equal(t, wantSalt, dep.deploys[0].Salt) -} - -func TestDeployProxyChangeset(t *testing.T) { - env, _, dep := newTestEnv(t) - - // Seed the datastore with a cache AddressRef so DeployProxy can resolve it. - seedDS := datastore.NewMemoryDataStore() - require.NoError(t, seedDS.Addresses().Add(datastore.AddressRef{ - Address: "CCACHEFAKE0000000000000000000000000000000000000000000000", - ChainSelector: testChainSel, - Type: CacheContract, - Version: semver.MustParse("1.0.0"), - Qualifier: "test-cache", - })) - env.DataStore = seedDS.Seal() - - req := &DeployProxyRequest{ - ChainSel: testChainSel, - WasmPath: writeDummyWasm(t, "data_feeds_proxy.wasm"), - CacheQualifier: "test-cache", - Qualifier: "test-proxy", - Version: "1.0.0", - } - - require.NoError(t, DeployProxy{}.VerifyPreconditions(env, req)) - - out, err := DeployProxy{}.Apply(env, req) - require.NoError(t, err) - require.NotNil(t, out.DataStore) - - require.Len(t, dep.deploys, 1) - require.Len(t, dep.deploys[0].Args, 2) // owner + cache ctor args - - owner := env.BlockChains.StellarChains()[testChainSel].Signer.Address() - wantSalt := stellardeploy.GenerateDeterministicSalt(owner, "data_feeds_proxy-"+req.Qualifier) - require.Equal(t, wantSalt, dep.deploys[0].Salt) - - key := datastore.NewAddressRefKey(testChainSel, ProxyContract, semver.MustParse("1.0.0"), "test-proxy") - ref, err := out.DataStore.Addresses().Get(key) - require.NoError(t, err) - require.Equal(t, testContractID, ref.Address) -} - -func TestDeployProxyChangeset_MissingCache(t *testing.T) { - env, _, _ := newTestEnv(t) - - req := &DeployProxyRequest{ - ChainSel: testChainSel, - WasmPath: writeDummyWasm(t, "data_feeds_proxy.wasm"), - CacheQualifier: "does-not-exist", - Qualifier: "test-proxy", - Version: "1.0.0", - } - - require.Error(t, DeployProxy{}.VerifyPreconditions(env, req)) -} - -const testAdmin = "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN7" - -func TestSetFeedConfigsChangeset(t *testing.T) { - env, inv, _ := newTestEnv(t) - seedCacheRef(t, &env, testContractID, "test", "1.0.0") - - req := &SetFeedConfigsRequest{ - ChainSel: testChainSel, - Qualifier: "test", - Version: "1.0.0", - Admin: testAdmin, - DataIDs: []string{"0x018e16c39e00032000000"}, - Descriptions: []string{"BTC/USD"}, - Permissions: []FeedPermission{{ - AllowedSender: testContractID, // the forwarder - AllowedWorkflowOwner: "0x0102030405060708090a0b0c0d0e0f1011121314", - AllowedWorkflowName: "abc", - }}, - } - require.NoError(t, SetFeedConfigs{}.VerifyPreconditions(env, req)) - - _, err := SetFeedConfigs{}.Apply(env, req) - require.NoError(t, err) - require.Len(t, inv.calls, 1) - require.Equal(t, "set_feed_configs", inv.calls[0].Function) - require.Equal(t, testContractID, inv.calls[0].ContractID) - - // Permissions apply to every feed in the batch: a single permission set - // with two DataIDs must still pass length validation. - multi := *req - multi.DataIDs = []string{"0x01", "0x02"} - multi.Descriptions = []string{"BTC/USD", "ETH/USD"} - require.NoError(t, SetFeedConfigs{}.VerifyPreconditions(env, &multi)) - - // mismatched lengths must fail preconditions - bad := *req - bad.Descriptions = nil - require.Error(t, SetFeedConfigs{}.VerifyPreconditions(env, &bad)) - - // empty DataIDs must fail preconditions - empty := *req - empty.DataIDs = nil - empty.Descriptions = nil - require.Error(t, SetFeedConfigs{}.VerifyPreconditions(env, &empty)) - - // invalid admin address must fail preconditions - badAdmin := *req - badAdmin.Admin = "not-a-key" - require.Error(t, SetFeedConfigs{}.VerifyPreconditions(env, &badAdmin)) - - // missing cache ref must fail preconditions - badQualifier := *req - badQualifier.Qualifier = "does-not-exist" - require.Error(t, SetFeedConfigs{}.VerifyPreconditions(env, &badQualifier)) - - // invalid permission fields must fail preconditions - badPerm := *req - badPerm.Permissions = []FeedPermission{{ - AllowedSender: "not-a-key", - AllowedWorkflowOwner: "0x0102030405060708090a0b0c0d0e0f1011121314", - AllowedWorkflowName: "abc", - }} - require.Error(t, SetFeedConfigs{}.VerifyPreconditions(env, &badPerm)) - - // empty Permissions must fail preconditions - emptyPerm := *req - emptyPerm.Permissions = nil - require.Error(t, SetFeedConfigs{}.VerifyPreconditions(env, &emptyPerm)) -} - -func TestRemoveFeedConfigsChangeset(t *testing.T) { - env, inv, _ := newTestEnv(t) - seedCacheRef(t, &env, testContractID, "test", "1.0.0") - - req := &RemoveFeedConfigsRequest{ - ChainSel: testChainSel, - Qualifier: "test", - Version: "1.0.0", - Admin: testAdmin, - DataIDs: []string{"0x018e16c39e00032000000"}, - } - require.NoError(t, RemoveFeedConfigs{}.VerifyPreconditions(env, req)) - - _, err := RemoveFeedConfigs{}.Apply(env, req) - require.NoError(t, err) - require.Len(t, inv.calls, 1) - require.Equal(t, "remove_feed_configs", inv.calls[0].Function) - require.Equal(t, testContractID, inv.calls[0].ContractID) - - // empty DataIDs must fail preconditions - empty := *req - empty.DataIDs = nil - require.Error(t, RemoveFeedConfigs{}.VerifyPreconditions(env, &empty)) - - // invalid admin address must fail preconditions - badAdmin := *req - badAdmin.Admin = "not-a-key" - require.Error(t, RemoveFeedConfigs{}.VerifyPreconditions(env, &badAdmin)) - - // missing cache ref must fail preconditions - badQualifier := *req - badQualifier.Qualifier = "does-not-exist" - require.Error(t, RemoveFeedConfigs{}.VerifyPreconditions(env, &badQualifier)) -} - -func TestSetFeedFrozenChangeset(t *testing.T) { - env, inv, _ := newTestEnv(t) - seedCacheRef(t, &env, testContractID, "test", "1.0.0") - - req := &SetFeedFrozenRequest{ - ChainSel: testChainSel, - Qualifier: "test", - Version: "1.0.0", - Admin: testAdmin, - DataIDs: []string{"0x018e16c39e00032000000"}, - Frozen: true, - } - require.NoError(t, SetFeedFrozen{}.VerifyPreconditions(env, req)) - - _, err := SetFeedFrozen{}.Apply(env, req) - require.NoError(t, err) - require.Len(t, inv.calls, 1) - require.Equal(t, "set_feed_frozen", inv.calls[0].Function) - require.Equal(t, testContractID, inv.calls[0].ContractID) - - // empty DataIDs must fail preconditions - empty := *req - empty.DataIDs = nil - require.Error(t, SetFeedFrozen{}.VerifyPreconditions(env, &empty)) - - // invalid admin address must fail preconditions - badAdmin := *req - badAdmin.Admin = "not-a-key" - require.Error(t, SetFeedFrozen{}.VerifyPreconditions(env, &badAdmin)) - - // missing cache ref must fail preconditions - badQualifier := *req - badQualifier.Qualifier = "does-not-exist" - require.Error(t, SetFeedFrozen{}.VerifyPreconditions(env, &badQualifier)) -} - -func TestFeedAdminChangesets(t *testing.T) { - env, inv, _ := newTestEnv(t) - seedCacheRef(t, &env, testContractID, "test", "1.0.0") - - req := &FeedAdminRequest{ - ChainSel: testChainSel, - Qualifier: "test", - Version: "1.0.0", - Admin: testAdmin, - } - - require.NoError(t, AddFeedAdmin{}.VerifyPreconditions(env, req)) - _, err := AddFeedAdmin{}.Apply(env, req) - require.NoError(t, err) - require.Len(t, inv.calls, 1) - require.Equal(t, "add_feed_admin", inv.calls[0].Function) - require.Equal(t, testContractID, inv.calls[0].ContractID) - - require.NoError(t, RemoveFeedAdmin{}.VerifyPreconditions(env, req)) - _, err = RemoveFeedAdmin{}.Apply(env, req) - require.NoError(t, err) - require.Len(t, inv.calls, 2) - require.Equal(t, "remove_feed_admin", inv.calls[1].Function) - require.Equal(t, testContractID, inv.calls[1].ContractID) - - // invalid admin address must fail preconditions for both changesets - badAdmin := *req - badAdmin.Admin = "not-a-key" - require.Error(t, AddFeedAdmin{}.VerifyPreconditions(env, &badAdmin)) - require.Error(t, RemoveFeedAdmin{}.VerifyPreconditions(env, &badAdmin)) - - // missing cache ref must fail preconditions for both changesets - badQualifier := *req - badQualifier.Qualifier = "does-not-exist" - require.Error(t, AddFeedAdmin{}.VerifyPreconditions(env, &badQualifier)) - require.Error(t, RemoveFeedAdmin{}.VerifyPreconditions(env, &badQualifier)) -} - -// testProxyAddress is a second, distinct contract address used wherever a -// test needs to tell the proxy and cache refs apart (e.g. asserting Apply -// invoked the proxy, not the cache). -const testProxyAddress = "CBPROXY00000000000000000000000000000000000000000000000AA" - -func TestOwnershipChangesets(t *testing.T) { - env, inv, _ := newTestEnv(t) - seedContractRefs(t, &env, - contractRefSpec{CacheContract, testContractID, "test-cache", "1.0.0"}, - contractRefSpec{ProxyContract, testProxyAddress, "test-proxy", "1.0.0"}, - ) - - cacheReq := &OwnershipRequest{ - ChainSel: testChainSel, - Qualifier: "test-cache", - Version: "1.0.0", - Contract: CacheContract, - NewOwner: testAdmin, - LiveUntilLedger: 100, - } - - require.NoError(t, TransferOwnership{}.VerifyPreconditions(env, cacheReq)) - _, err := TransferOwnership{}.Apply(env, cacheReq) - require.NoError(t, err) - require.Len(t, inv.calls, 1) - require.Equal(t, "transfer_ownership", inv.calls[0].Function) - require.Equal(t, testContractID, inv.calls[0].ContractID) - require.Len(t, inv.calls[0].Args, 2) - - require.NoError(t, AcceptOwnership{}.VerifyPreconditions(env, cacheReq)) - _, err = AcceptOwnership{}.Apply(env, cacheReq) - require.NoError(t, err) - require.Len(t, inv.calls, 2) - require.Equal(t, "accept_ownership", inv.calls[1].Function) - require.Equal(t, testContractID, inv.calls[1].ContractID) - require.Empty(t, inv.calls[1].Args) - - require.NoError(t, RenounceOwnership{}.VerifyPreconditions(env, cacheReq)) - _, err = RenounceOwnership{}.Apply(env, cacheReq) - require.NoError(t, err) - require.Len(t, inv.calls, 3) - require.Equal(t, "renounce_ownership", inv.calls[2].Function) - require.Equal(t, testContractID, inv.calls[2].ContractID) - - // same three ops against the proxy: IsProxy selects the proxy client and - // the resolved contractID is the proxy's address, not the cache's. - proxyReq := *cacheReq - proxyReq.Qualifier = "test-proxy" - proxyReq.Contract = ProxyContract - - require.NoError(t, TransferOwnership{}.VerifyPreconditions(env, &proxyReq)) - _, err = TransferOwnership{}.Apply(env, &proxyReq) - require.NoError(t, err) - require.Equal(t, "transfer_ownership", inv.calls[3].Function) - require.Equal(t, testProxyAddress, inv.calls[3].ContractID) - - require.NoError(t, AcceptOwnership{}.VerifyPreconditions(env, &proxyReq)) - _, err = AcceptOwnership{}.Apply(env, &proxyReq) - require.NoError(t, err) - require.Equal(t, "accept_ownership", inv.calls[4].Function) - require.Equal(t, testProxyAddress, inv.calls[4].ContractID) - - require.NoError(t, RenounceOwnership{}.VerifyPreconditions(env, &proxyReq)) - _, err = RenounceOwnership{}.Apply(env, &proxyReq) - require.NoError(t, err) - require.Equal(t, "renounce_ownership", inv.calls[5].Function) - require.Equal(t, testProxyAddress, inv.calls[5].ContractID) - - // unknown Contract type must fail preconditions for all three changesets - badContract := *cacheReq - badContract.Contract = datastore.ContractType("SomethingElse") - require.Error(t, TransferOwnership{}.VerifyPreconditions(env, &badContract)) - require.Error(t, AcceptOwnership{}.VerifyPreconditions(env, &badContract)) - require.Error(t, RenounceOwnership{}.VerifyPreconditions(env, &badContract)) - - // TransferOwnership requires a valid NewOwner and nonzero LiveUntilLedger; - // Accept/Renounce don't touch those fields so the same request stays valid. - missingOwner := *cacheReq - missingOwner.NewOwner = "" - require.Error(t, TransferOwnership{}.VerifyPreconditions(env, &missingOwner)) - require.NoError(t, AcceptOwnership{}.VerifyPreconditions(env, &missingOwner)) - - badOwner := *cacheReq - badOwner.NewOwner = "not-a-key" - require.Error(t, TransferOwnership{}.VerifyPreconditions(env, &badOwner)) - - zeroLedger := *cacheReq - zeroLedger.LiveUntilLedger = 0 - require.Error(t, TransferOwnership{}.VerifyPreconditions(env, &zeroLedger)) - - // missing address ref must fail preconditions - badQualifier := *cacheReq - badQualifier.Qualifier = "does-not-exist" - require.Error(t, TransferOwnership{}.VerifyPreconditions(env, &badQualifier)) -} - -func TestUpgradeChangeset(t *testing.T) { - env, inv, dep := newTestEnv(t) - seedContractRefs(t, &env, - contractRefSpec{CacheContract, testContractID, "test", "1.0.0"}, - contractRefSpec{ProxyContract, testProxyAddress, "test-proxy", "1.0.0"}, - ) - - var wantHash xdr.Hash - for i := range wantHash { - wantHash[i] = byte(i + 1) - } - dep.wasmHash = wantHash - - req := &UpgradeRequest{ - ChainSel: testChainSel, - Qualifier: "test", - Version: "1.0.0", - Contract: CacheContract, - WasmPath: writeDummyWasm(t, "new_cache.wasm"), - } - require.NoError(t, Upgrade{}.VerifyPreconditions(env, req)) - - _, err := Upgrade{}.Apply(env, req) - require.NoError(t, err) - - // the upload happened, against the requested wasm path - require.Len(t, dep.uploads, 1) - require.Equal(t, req.WasmPath, dep.uploads[0]) - - // the upgrade invocation carried the fake's uploaded hash through - require.Len(t, inv.calls, 1) - require.Equal(t, "upgrade", inv.calls[0].Function) - require.Equal(t, testContractID, inv.calls[0].ContractID) - require.Len(t, inv.calls[0].Args, 1) - - gotHash, err := scval.Bytes32FromScVal(inv.calls[0].Args[0]) - require.NoError(t, err) - require.Equal(t, [32]byte(wantHash), gotHash) - - // proxy path targets the proxy ref - proxyReq := *req - proxyReq.Qualifier = "test-proxy" - proxyReq.Contract = ProxyContract - require.NoError(t, Upgrade{}.VerifyPreconditions(env, &proxyReq)) - _, err = Upgrade{}.Apply(env, &proxyReq) - require.NoError(t, err) - require.Len(t, inv.calls, 2) - require.Equal(t, "upgrade", inv.calls[1].Function) - require.Equal(t, testProxyAddress, inv.calls[1].ContractID) - - // unsupported contract type must fail preconditions - badContract := *req - badContract.Contract = "NotAContract" - require.Error(t, Upgrade{}.VerifyPreconditions(env, &badContract)) - - // missing wasm path must fail preconditions - badPath := *req - badPath.WasmPath = "/does/not/exist.wasm" - require.Error(t, Upgrade{}.VerifyPreconditions(env, &badPath)) - - // missing cache ref must fail preconditions - badQualifier := *req - badQualifier.Qualifier = "does-not-exist" - require.Error(t, Upgrade{}.VerifyPreconditions(env, &badQualifier)) -} - -func TestRecoverTokensChangeset(t *testing.T) { - env, inv, _ := newTestEnv(t) - seedContractRefs(t, &env, - contractRefSpec{CacheContract, testContractID, "test", "1.0.0"}, - contractRefSpec{ProxyContract, testProxyAddress, "test-proxy", "1.0.0"}, - ) - - req := &RecoverTokensRequest{ - ChainSel: testChainSel, - Qualifier: "test", - Version: "1.0.0", - Contract: CacheContract, - Token: testContractID, - To: testAdmin, - Amount: 1000, - } - require.NoError(t, RecoverTokens{}.VerifyPreconditions(env, req)) - - _, err := RecoverTokens{}.Apply(env, req) - require.NoError(t, err) - require.Len(t, inv.calls, 1) - require.Equal(t, "recover_tokens", inv.calls[0].Function) - require.Equal(t, testContractID, inv.calls[0].ContractID) - require.Len(t, inv.calls[0].Args, 3) - - // proxy path targets the proxy ref - proxyReq := *req - proxyReq.Qualifier = "test-proxy" - proxyReq.Contract = ProxyContract - require.NoError(t, RecoverTokens{}.VerifyPreconditions(env, &proxyReq)) - _, err = RecoverTokens{}.Apply(env, &proxyReq) - require.NoError(t, err) - require.Len(t, inv.calls, 2) - require.Equal(t, "recover_tokens", inv.calls[1].Function) - require.Equal(t, testProxyAddress, inv.calls[1].ContractID) - - // unsupported contract type must fail preconditions - badContract := *req - badContract.Contract = "NotAContract" - require.Error(t, RecoverTokens{}.VerifyPreconditions(env, &badContract)) - - // bad token address must fail preconditions - badToken := *req - badToken.Token = "not-a-key" - require.Error(t, RecoverTokens{}.VerifyPreconditions(env, &badToken)) - - // bad recipient address must fail preconditions - badTo := *req - badTo.To = "not-a-key" - require.Error(t, RecoverTokens{}.VerifyPreconditions(env, &badTo)) - - // non-positive amount must fail preconditions - badAmount := *req - badAmount.Amount = 0 - require.Error(t, RecoverTokens{}.VerifyPreconditions(env, &badAmount)) - - // missing cache ref must fail preconditions - badQualifier := *req - badQualifier.Qualifier = "does-not-exist" - require.Error(t, RecoverTokens{}.VerifyPreconditions(env, &badQualifier)) -} - -func TestSetProxyCacheChangeset(t *testing.T) { - env, inv, _ := newTestEnv(t) - seedContractRefs(t, &env, - contractRefSpec{ProxyContract, testProxyAddress, "test-proxy", "1.0.0"}, - contractRefSpec{CacheContract, testContractID, "test-cache", "1.0.0"}, - ) - - req := &SetProxyCacheRequest{ - ChainSel: testChainSel, - Qualifier: "test-proxy", - Version: "1.0.0", - CacheQualifier: "test-cache", - } - require.NoError(t, SetProxyCache{}.VerifyPreconditions(env, req)) - - _, err := SetProxyCache{}.Apply(env, req) - require.NoError(t, err) - require.Len(t, inv.calls, 1) - require.Equal(t, "set_cache", inv.calls[0].Function) - require.Equal(t, testProxyAddress, inv.calls[0].ContractID) - require.Len(t, inv.calls[0].Args, 1) - - // missing cache ref must fail preconditions (proxy ref alone isn't enough) - proxyOnly, _, _ := newTestEnv(t) - seedProxyRef(t, &proxyOnly, testProxyAddress, "test-proxy", "1.0.0") - require.Error(t, SetProxyCache{}.VerifyPreconditions(proxyOnly, req)) - - // missing proxy ref must fail preconditions (cache ref alone isn't enough) - cacheOnly, _, _ := newTestEnv(t) - seedCacheRef(t, &cacheOnly, testContractID, "test-cache", "1.0.0") - require.Error(t, SetProxyCache{}.VerifyPreconditions(cacheOnly, req)) -} diff --git a/deployment/data-feeds/changeset/stellar/deploy_proxy.go b/deployment/data-feeds/changeset/stellar/deploy.go similarity index 60% rename from deployment/data-feeds/changeset/stellar/deploy_proxy.go rename to deployment/data-feeds/changeset/stellar/deploy.go index 43dc6b4f276..ce848ceacaa 100644 --- a/deployment/data-feeds/changeset/stellar/deploy_proxy.go +++ b/deployment/data-feeds/changeset/stellar/deploy.go @@ -15,6 +15,63 @@ import ( "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/stellar/operation" ) +// DeployCacheRequest configures a DataFeedsCache deployment. +type DeployCacheRequest struct { + ChainSel uint64 + WasmPath string + Owner string // defaults to the chain's deployer address when empty + Qualifier string + Version string + LabelSet datastore.LabelSet +} + +var _ cldf.ChangeSetV2[*DeployCacheRequest] = DeployCache{} + +// DeployCache uploads and instantiates the DataFeedsCache contract and records +// its address in the datastore under CacheContract. +type DeployCache struct{} + +func (DeployCache) VerifyPreconditions(env cldf.Environment, req *DeployCacheRequest) error { + if _, ok := env.BlockChains.StellarChains()[req.ChainSel]; !ok { + return fmt.Errorf("stellar chain not found for chain selector %d", req.ChainSel) + } + if _, err := semver.NewVersion(req.Version); err != nil { + return fmt.Errorf("invalid version %q: %w", req.Version, err) + } + if _, err := os.Stat(req.WasmPath); err != nil { + return fmt.Errorf("wasm path: %w", err) + } + if req.Owner != "" { + if err := validateAddress(req.Owner); err != nil { + return err + } + } + return nil +} + +func (DeployCache) Apply(env cldf.Environment, req *DeployCacheRequest) (cldf.ChangesetOutput, error) { + var out cldf.ChangesetOutput + ch, deps, err := chainDeps(env, req.ChainSel) + if err != nil { + return out, err + } + owner, err := ownerOrSigner(ch, req.Owner) + if err != nil { + return out, err + } + + salt := stellardeploy.GenerateDeterministicSalt(owner, "data_feeds_cache-"+req.Qualifier) + report, err := operations.ExecuteOperation(env.OperationsBundle, operation.DeployCache, deps, operation.DeployCacheInput{ + WasmPath: req.WasmPath, + Salt: salt, + Owner: owner, + }) + if err != nil { + return out, err + } + return recordAddress(report.Output.ContractID, req.ChainSel, CacheContract, req.Qualifier, req.Version, req.LabelSet) +} + // DeployProxyRequest configures a DataFeedsProxy deployment. The cache passed // to __constructor(owner, cache) is resolved from the datastore by (ChainSel, // CacheContract, Version, CacheQualifier) — it must already be recorded. diff --git a/deployment/data-feeds/changeset/stellar/deploy_cache.go b/deployment/data-feeds/changeset/stellar/deploy_cache.go deleted file mode 100644 index f7fbfe86332..00000000000 --- a/deployment/data-feeds/changeset/stellar/deploy_cache.go +++ /dev/null @@ -1,72 +0,0 @@ -package stellar - -import ( - "fmt" - "os" - - "github.com/Masterminds/semver/v3" - - "github.com/smartcontractkit/chainlink-deployments-framework/datastore" - cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" - "github.com/smartcontractkit/chainlink-deployments-framework/operations" - stellardeploy "github.com/smartcontractkit/chainlink-stellar/deployment" - - "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/stellar/operation" -) - -// DeployCacheRequest configures a DataFeedsCache deployment. -type DeployCacheRequest struct { - ChainSel uint64 - WasmPath string - Owner string // defaults to the chain's deployer address when empty - Qualifier string - Version string - LabelSet datastore.LabelSet -} - -var _ cldf.ChangeSetV2[*DeployCacheRequest] = DeployCache{} - -// DeployCache uploads and instantiates the DataFeedsCache contract and records -// its address in the datastore under CacheContract. -type DeployCache struct{} - -func (DeployCache) VerifyPreconditions(env cldf.Environment, req *DeployCacheRequest) error { - if _, ok := env.BlockChains.StellarChains()[req.ChainSel]; !ok { - return fmt.Errorf("stellar chain not found for chain selector %d", req.ChainSel) - } - if _, err := semver.NewVersion(req.Version); err != nil { - return fmt.Errorf("invalid version %q: %w", req.Version, err) - } - if _, err := os.Stat(req.WasmPath); err != nil { - return fmt.Errorf("wasm path: %w", err) - } - if req.Owner != "" { - if err := validateAddress(req.Owner); err != nil { - return err - } - } - return nil -} - -func (DeployCache) Apply(env cldf.Environment, req *DeployCacheRequest) (cldf.ChangesetOutput, error) { - var out cldf.ChangesetOutput - ch, deps, err := chainDeps(env, req.ChainSel) - if err != nil { - return out, err - } - owner, err := ownerOrSigner(ch, req.Owner) - if err != nil { - return out, err - } - - salt := stellardeploy.GenerateDeterministicSalt(owner, "data_feeds_cache-"+req.Qualifier) - report, err := operations.ExecuteOperation(env.OperationsBundle, operation.DeployCache, deps, operation.DeployCacheInput{ - WasmPath: req.WasmPath, - Salt: salt, - Owner: owner, - }) - if err != nil { - return out, err - } - return recordAddress(report.Output.ContractID, req.ChainSel, CacheContract, req.Qualifier, req.Version, req.LabelSet) -} diff --git a/deployment/data-feeds/changeset/stellar/deploy_test.go b/deployment/data-feeds/changeset/stellar/deploy_test.go new file mode 100644 index 00000000000..fe9d3bcdab3 --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/deploy_test.go @@ -0,0 +1,141 @@ +package stellar + +import ( + "testing" + + "github.com/Masterminds/semver/v3" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + stellardeploy "github.com/smartcontractkit/chainlink-stellar/deployment" +) + +func TestDeployCacheChangeset(t *testing.T) { + env, _, dep := newTestEnv(t) + + req := &DeployCacheRequest{ + ChainSel: testChainSel, + WasmPath: writeDummyWasm(t, "data_feeds_cache.wasm"), + Qualifier: "test-cache", + Version: "1.0.0", + } + + require.NoError(t, DeployCache{}.VerifyPreconditions(env, req)) + + out, err := DeployCache{}.Apply(env, req) + require.NoError(t, err) + require.NotNil(t, out.DataStore) + + require.Len(t, dep.deploys, 1) + require.Len(t, dep.deploys[0].Args, 1) // owner ctor arg + + owner := env.BlockChains.StellarChains()[testChainSel].Signer.Address() + wantSalt := stellardeploy.GenerateDeterministicSalt(owner, "data_feeds_cache-"+req.Qualifier) + require.Equal(t, wantSalt, dep.deploys[0].Salt) + + key := datastore.NewAddressRefKey(testChainSel, CacheContract, semver.MustParse("1.0.0"), "test-cache") + ref, err := out.DataStore.Addresses().Get(key) + require.NoError(t, err) + require.Equal(t, testContractID, ref.Address) + + // invalid explicit Owner must fail preconditions + badOwner := *req + badOwner.Owner = "not-a-key" + require.Error(t, DeployCache{}.VerifyPreconditions(env, &badOwner)) +} + +func TestDeployCacheChangeset_MissingChain(t *testing.T) { + env, _, _ := newTestEnv(t) + + req := &DeployCacheRequest{ + ChainSel: 999999, + WasmPath: writeDummyWasm(t, "data_feeds_cache.wasm"), + Qualifier: "test-cache", + Version: "1.0.0", + } + + require.Error(t, DeployCache{}.VerifyPreconditions(env, req)) +} + +// TestDeployCacheChangeset_ExplicitOwner pins that an explicit req.Owner +// seeds the deploy salt directly. deploy.go's DeployCache.Apply only falls back to +// ch.Signer.Address() when req.Owner is empty; when it's set, the salt is +// GenerateDeterministicSalt(req.Owner, ...) -- not the chain signer's address. +func TestDeployCacheChangeset_ExplicitOwner(t *testing.T) { + env, _, dep := newTestEnv(t) + + req := &DeployCacheRequest{ + ChainSel: testChainSel, + WasmPath: writeDummyWasm(t, "data_feeds_cache.wasm"), + Owner: testAdmin, + Qualifier: "test-cache-owner", + Version: "1.0.0", + } + + require.NoError(t, DeployCache{}.VerifyPreconditions(env, req)) + + out, err := DeployCache{}.Apply(env, req) + require.NoError(t, err) + require.NotNil(t, out.DataStore) + + require.Len(t, dep.deploys, 1) + require.Len(t, dep.deploys[0].Args, 1) // owner ctor arg + + wantSalt := stellardeploy.GenerateDeterministicSalt(testAdmin, "data_feeds_cache-"+req.Qualifier) + require.Equal(t, wantSalt, dep.deploys[0].Salt) +} + +func TestDeployProxyChangeset(t *testing.T) { + env, _, dep := newTestEnv(t) + + // Seed the datastore with a cache AddressRef so DeployProxy can resolve it. + seedDS := datastore.NewMemoryDataStore() + require.NoError(t, seedDS.Addresses().Add(datastore.AddressRef{ + Address: "CCACHEFAKE0000000000000000000000000000000000000000000000", + ChainSelector: testChainSel, + Type: CacheContract, + Version: semver.MustParse("1.0.0"), + Qualifier: "test-cache", + })) + env.DataStore = seedDS.Seal() + + req := &DeployProxyRequest{ + ChainSel: testChainSel, + WasmPath: writeDummyWasm(t, "data_feeds_proxy.wasm"), + CacheQualifier: "test-cache", + Qualifier: "test-proxy", + Version: "1.0.0", + } + + require.NoError(t, DeployProxy{}.VerifyPreconditions(env, req)) + + out, err := DeployProxy{}.Apply(env, req) + require.NoError(t, err) + require.NotNil(t, out.DataStore) + + require.Len(t, dep.deploys, 1) + require.Len(t, dep.deploys[0].Args, 2) // owner + cache ctor args + + owner := env.BlockChains.StellarChains()[testChainSel].Signer.Address() + wantSalt := stellardeploy.GenerateDeterministicSalt(owner, "data_feeds_proxy-"+req.Qualifier) + require.Equal(t, wantSalt, dep.deploys[0].Salt) + + key := datastore.NewAddressRefKey(testChainSel, ProxyContract, semver.MustParse("1.0.0"), "test-proxy") + ref, err := out.DataStore.Addresses().Get(key) + require.NoError(t, err) + require.Equal(t, testContractID, ref.Address) +} + +func TestDeployProxyChangeset_MissingCache(t *testing.T) { + env, _, _ := newTestEnv(t) + + req := &DeployProxyRequest{ + ChainSel: testChainSel, + WasmPath: writeDummyWasm(t, "data_feeds_proxy.wasm"), + CacheQualifier: "does-not-exist", + Qualifier: "test-proxy", + Version: "1.0.0", + } + + require.Error(t, DeployProxy{}.VerifyPreconditions(env, req)) +} diff --git a/deployment/data-feeds/changeset/stellar/feed_admin_test.go b/deployment/data-feeds/changeset/stellar/feed_admin_test.go new file mode 100644 index 00000000000..d33f28a152e --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/feed_admin_test.go @@ -0,0 +1,45 @@ +package stellar + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestFeedAdminChangesets(t *testing.T) { + env, inv, _ := newTestEnv(t) + seedCacheRef(t, &env, testContractID, "test", "1.0.0") + + req := &FeedAdminRequest{ + ChainSel: testChainSel, + Qualifier: "test", + Version: "1.0.0", + Admin: testAdmin, + } + + require.NoError(t, AddFeedAdmin{}.VerifyPreconditions(env, req)) + _, err := AddFeedAdmin{}.Apply(env, req) + require.NoError(t, err) + require.Len(t, inv.calls, 1) + require.Equal(t, "add_feed_admin", inv.calls[0].Function) + require.Equal(t, testContractID, inv.calls[0].ContractID) + + require.NoError(t, RemoveFeedAdmin{}.VerifyPreconditions(env, req)) + _, err = RemoveFeedAdmin{}.Apply(env, req) + require.NoError(t, err) + require.Len(t, inv.calls, 2) + require.Equal(t, "remove_feed_admin", inv.calls[1].Function) + require.Equal(t, testContractID, inv.calls[1].ContractID) + + // invalid admin address must fail preconditions for both changesets + badAdmin := *req + badAdmin.Admin = "not-a-key" + require.Error(t, AddFeedAdmin{}.VerifyPreconditions(env, &badAdmin)) + require.Error(t, RemoveFeedAdmin{}.VerifyPreconditions(env, &badAdmin)) + + // missing cache ref must fail preconditions for both changesets + badQualifier := *req + badQualifier.Qualifier = "does-not-exist" + require.Error(t, AddFeedAdmin{}.VerifyPreconditions(env, &badQualifier)) + require.Error(t, RemoveFeedAdmin{}.VerifyPreconditions(env, &badQualifier)) +} diff --git a/deployment/data-feeds/changeset/stellar/ownership_test.go b/deployment/data-feeds/changeset/stellar/ownership_test.go new file mode 100644 index 00000000000..146f8a1421b --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/ownership_test.go @@ -0,0 +1,100 @@ +package stellar + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" +) + +func TestOwnershipChangesets(t *testing.T) { + env, inv, _ := newTestEnv(t) + seedContractRefs(t, &env, + contractRefSpec{CacheContract, testContractID, "test-cache", "1.0.0"}, + contractRefSpec{ProxyContract, testProxyAddress, "test-proxy", "1.0.0"}, + ) + + cacheReq := &OwnershipRequest{ + ChainSel: testChainSel, + Qualifier: "test-cache", + Version: "1.0.0", + Contract: CacheContract, + NewOwner: testAdmin, + LiveUntilLedger: 100, + } + + require.NoError(t, TransferOwnership{}.VerifyPreconditions(env, cacheReq)) + _, err := TransferOwnership{}.Apply(env, cacheReq) + require.NoError(t, err) + require.Len(t, inv.calls, 1) + require.Equal(t, "transfer_ownership", inv.calls[0].Function) + require.Equal(t, testContractID, inv.calls[0].ContractID) + require.Len(t, inv.calls[0].Args, 2) + + require.NoError(t, AcceptOwnership{}.VerifyPreconditions(env, cacheReq)) + _, err = AcceptOwnership{}.Apply(env, cacheReq) + require.NoError(t, err) + require.Len(t, inv.calls, 2) + require.Equal(t, "accept_ownership", inv.calls[1].Function) + require.Equal(t, testContractID, inv.calls[1].ContractID) + require.Empty(t, inv.calls[1].Args) + + require.NoError(t, RenounceOwnership{}.VerifyPreconditions(env, cacheReq)) + _, err = RenounceOwnership{}.Apply(env, cacheReq) + require.NoError(t, err) + require.Len(t, inv.calls, 3) + require.Equal(t, "renounce_ownership", inv.calls[2].Function) + require.Equal(t, testContractID, inv.calls[2].ContractID) + + // same three ops against the proxy: IsProxy selects the proxy client and + // the resolved contractID is the proxy's address, not the cache's. + proxyReq := *cacheReq + proxyReq.Qualifier = "test-proxy" + proxyReq.Contract = ProxyContract + + require.NoError(t, TransferOwnership{}.VerifyPreconditions(env, &proxyReq)) + _, err = TransferOwnership{}.Apply(env, &proxyReq) + require.NoError(t, err) + require.Equal(t, "transfer_ownership", inv.calls[3].Function) + require.Equal(t, testProxyAddress, inv.calls[3].ContractID) + + require.NoError(t, AcceptOwnership{}.VerifyPreconditions(env, &proxyReq)) + _, err = AcceptOwnership{}.Apply(env, &proxyReq) + require.NoError(t, err) + require.Equal(t, "accept_ownership", inv.calls[4].Function) + require.Equal(t, testProxyAddress, inv.calls[4].ContractID) + + require.NoError(t, RenounceOwnership{}.VerifyPreconditions(env, &proxyReq)) + _, err = RenounceOwnership{}.Apply(env, &proxyReq) + require.NoError(t, err) + require.Equal(t, "renounce_ownership", inv.calls[5].Function) + require.Equal(t, testProxyAddress, inv.calls[5].ContractID) + + // unknown Contract type must fail preconditions for all three changesets + badContract := *cacheReq + badContract.Contract = datastore.ContractType("SomethingElse") + require.Error(t, TransferOwnership{}.VerifyPreconditions(env, &badContract)) + require.Error(t, AcceptOwnership{}.VerifyPreconditions(env, &badContract)) + require.Error(t, RenounceOwnership{}.VerifyPreconditions(env, &badContract)) + + // TransferOwnership requires a valid NewOwner and nonzero LiveUntilLedger; + // Accept/Renounce don't touch those fields so the same request stays valid. + missingOwner := *cacheReq + missingOwner.NewOwner = "" + require.Error(t, TransferOwnership{}.VerifyPreconditions(env, &missingOwner)) + require.NoError(t, AcceptOwnership{}.VerifyPreconditions(env, &missingOwner)) + + badOwner := *cacheReq + badOwner.NewOwner = "not-a-key" + require.Error(t, TransferOwnership{}.VerifyPreconditions(env, &badOwner)) + + zeroLedger := *cacheReq + zeroLedger.LiveUntilLedger = 0 + require.Error(t, TransferOwnership{}.VerifyPreconditions(env, &zeroLedger)) + + // missing address ref must fail preconditions + badQualifier := *cacheReq + badQualifier.Qualifier = "does-not-exist" + require.Error(t, TransferOwnership{}.VerifyPreconditions(env, &badQualifier)) +} diff --git a/deployment/data-feeds/changeset/stellar/recover_tokens_test.go b/deployment/data-feeds/changeset/stellar/recover_tokens_test.go new file mode 100644 index 00000000000..22f7c9121cc --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/recover_tokens_test.go @@ -0,0 +1,69 @@ +package stellar + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRecoverTokensChangeset(t *testing.T) { + env, inv, _ := newTestEnv(t) + seedContractRefs(t, &env, + contractRefSpec{CacheContract, testContractID, "test", "1.0.0"}, + contractRefSpec{ProxyContract, testProxyAddress, "test-proxy", "1.0.0"}, + ) + + req := &RecoverTokensRequest{ + ChainSel: testChainSel, + Qualifier: "test", + Version: "1.0.0", + Contract: CacheContract, + Token: testContractID, + To: testAdmin, + Amount: 1000, + } + require.NoError(t, RecoverTokens{}.VerifyPreconditions(env, req)) + + _, err := RecoverTokens{}.Apply(env, req) + require.NoError(t, err) + require.Len(t, inv.calls, 1) + require.Equal(t, "recover_tokens", inv.calls[0].Function) + require.Equal(t, testContractID, inv.calls[0].ContractID) + require.Len(t, inv.calls[0].Args, 3) + + // proxy path targets the proxy ref + proxyReq := *req + proxyReq.Qualifier = "test-proxy" + proxyReq.Contract = ProxyContract + require.NoError(t, RecoverTokens{}.VerifyPreconditions(env, &proxyReq)) + _, err = RecoverTokens{}.Apply(env, &proxyReq) + require.NoError(t, err) + require.Len(t, inv.calls, 2) + require.Equal(t, "recover_tokens", inv.calls[1].Function) + require.Equal(t, testProxyAddress, inv.calls[1].ContractID) + + // unsupported contract type must fail preconditions + badContract := *req + badContract.Contract = "NotAContract" + require.Error(t, RecoverTokens{}.VerifyPreconditions(env, &badContract)) + + // bad token address must fail preconditions + badToken := *req + badToken.Token = "not-a-key" + require.Error(t, RecoverTokens{}.VerifyPreconditions(env, &badToken)) + + // bad recipient address must fail preconditions + badTo := *req + badTo.To = "not-a-key" + require.Error(t, RecoverTokens{}.VerifyPreconditions(env, &badTo)) + + // non-positive amount must fail preconditions + badAmount := *req + badAmount.Amount = 0 + require.Error(t, RecoverTokens{}.VerifyPreconditions(env, &badAmount)) + + // missing cache ref must fail preconditions + badQualifier := *req + badQualifier.Qualifier = "does-not-exist" + require.Error(t, RecoverTokens{}.VerifyPreconditions(env, &badQualifier)) +} diff --git a/deployment/data-feeds/changeset/stellar/remove_feeds.go b/deployment/data-feeds/changeset/stellar/remove_feed_configs.go similarity index 100% rename from deployment/data-feeds/changeset/stellar/remove_feeds.go rename to deployment/data-feeds/changeset/stellar/remove_feed_configs.go diff --git a/deployment/data-feeds/changeset/stellar/remove_feed_configs_test.go b/deployment/data-feeds/changeset/stellar/remove_feed_configs_test.go new file mode 100644 index 00000000000..245e2deefe6 --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/remove_feed_configs_test.go @@ -0,0 +1,42 @@ +package stellar + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRemoveFeedConfigsChangeset(t *testing.T) { + env, inv, _ := newTestEnv(t) + seedCacheRef(t, &env, testContractID, "test", "1.0.0") + + req := &RemoveFeedConfigsRequest{ + ChainSel: testChainSel, + Qualifier: "test", + Version: "1.0.0", + Admin: testAdmin, + DataIDs: []string{"0x018e16c39e00032000000"}, + } + require.NoError(t, RemoveFeedConfigs{}.VerifyPreconditions(env, req)) + + _, err := RemoveFeedConfigs{}.Apply(env, req) + require.NoError(t, err) + require.Len(t, inv.calls, 1) + require.Equal(t, "remove_feed_configs", inv.calls[0].Function) + require.Equal(t, testContractID, inv.calls[0].ContractID) + + // empty DataIDs must fail preconditions + empty := *req + empty.DataIDs = nil + require.Error(t, RemoveFeedConfigs{}.VerifyPreconditions(env, &empty)) + + // invalid admin address must fail preconditions + badAdmin := *req + badAdmin.Admin = "not-a-key" + require.Error(t, RemoveFeedConfigs{}.VerifyPreconditions(env, &badAdmin)) + + // missing cache ref must fail preconditions + badQualifier := *req + badQualifier.Qualifier = "does-not-exist" + require.Error(t, RemoveFeedConfigs{}.VerifyPreconditions(env, &badQualifier)) +} diff --git a/deployment/data-feeds/changeset/stellar/configure_cache.go b/deployment/data-feeds/changeset/stellar/set_feed_configs.go similarity index 100% rename from deployment/data-feeds/changeset/stellar/configure_cache.go rename to deployment/data-feeds/changeset/stellar/set_feed_configs.go diff --git a/deployment/data-feeds/changeset/stellar/set_feed_configs_test.go b/deployment/data-feeds/changeset/stellar/set_feed_configs_test.go new file mode 100644 index 00000000000..fa06b3e74d1 --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/set_feed_configs_test.go @@ -0,0 +1,75 @@ +package stellar + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSetFeedConfigsChangeset(t *testing.T) { + env, inv, _ := newTestEnv(t) + seedCacheRef(t, &env, testContractID, "test", "1.0.0") + + req := &SetFeedConfigsRequest{ + ChainSel: testChainSel, + Qualifier: "test", + Version: "1.0.0", + Admin: testAdmin, + DataIDs: []string{"0x018e16c39e00032000000"}, + Descriptions: []string{"BTC/USD"}, + Permissions: []FeedPermission{{ + AllowedSender: testContractID, // the forwarder + AllowedWorkflowOwner: "0x0102030405060708090a0b0c0d0e0f1011121314", + AllowedWorkflowName: "abc", + }}, + } + require.NoError(t, SetFeedConfigs{}.VerifyPreconditions(env, req)) + + _, err := SetFeedConfigs{}.Apply(env, req) + require.NoError(t, err) + require.Len(t, inv.calls, 1) + require.Equal(t, "set_feed_configs", inv.calls[0].Function) + require.Equal(t, testContractID, inv.calls[0].ContractID) + + // Permissions apply to every feed in the batch: a single permission set + // with two DataIDs must still pass length validation. + multi := *req + multi.DataIDs = []string{"0x01", "0x02"} + multi.Descriptions = []string{"BTC/USD", "ETH/USD"} + require.NoError(t, SetFeedConfigs{}.VerifyPreconditions(env, &multi)) + + // mismatched lengths must fail preconditions + bad := *req + bad.Descriptions = nil + require.Error(t, SetFeedConfigs{}.VerifyPreconditions(env, &bad)) + + // empty DataIDs must fail preconditions + empty := *req + empty.DataIDs = nil + empty.Descriptions = nil + require.Error(t, SetFeedConfigs{}.VerifyPreconditions(env, &empty)) + + // invalid admin address must fail preconditions + badAdmin := *req + badAdmin.Admin = "not-a-key" + require.Error(t, SetFeedConfigs{}.VerifyPreconditions(env, &badAdmin)) + + // missing cache ref must fail preconditions + badQualifier := *req + badQualifier.Qualifier = "does-not-exist" + require.Error(t, SetFeedConfigs{}.VerifyPreconditions(env, &badQualifier)) + + // invalid permission fields must fail preconditions + badPerm := *req + badPerm.Permissions = []FeedPermission{{ + AllowedSender: "not-a-key", + AllowedWorkflowOwner: "0x0102030405060708090a0b0c0d0e0f1011121314", + AllowedWorkflowName: "abc", + }} + require.Error(t, SetFeedConfigs{}.VerifyPreconditions(env, &badPerm)) + + // empty Permissions must fail preconditions + emptyPerm := *req + emptyPerm.Permissions = nil + require.Error(t, SetFeedConfigs{}.VerifyPreconditions(env, &emptyPerm)) +} diff --git a/deployment/data-feeds/changeset/stellar/freeze_feeds.go b/deployment/data-feeds/changeset/stellar/set_feed_frozen.go similarity index 100% rename from deployment/data-feeds/changeset/stellar/freeze_feeds.go rename to deployment/data-feeds/changeset/stellar/set_feed_frozen.go diff --git a/deployment/data-feeds/changeset/stellar/set_feed_frozen_test.go b/deployment/data-feeds/changeset/stellar/set_feed_frozen_test.go new file mode 100644 index 00000000000..c47240e8137 --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/set_feed_frozen_test.go @@ -0,0 +1,43 @@ +package stellar + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSetFeedFrozenChangeset(t *testing.T) { + env, inv, _ := newTestEnv(t) + seedCacheRef(t, &env, testContractID, "test", "1.0.0") + + req := &SetFeedFrozenRequest{ + ChainSel: testChainSel, + Qualifier: "test", + Version: "1.0.0", + Admin: testAdmin, + DataIDs: []string{"0x018e16c39e00032000000"}, + Frozen: true, + } + require.NoError(t, SetFeedFrozen{}.VerifyPreconditions(env, req)) + + _, err := SetFeedFrozen{}.Apply(env, req) + require.NoError(t, err) + require.Len(t, inv.calls, 1) + require.Equal(t, "set_feed_frozen", inv.calls[0].Function) + require.Equal(t, testContractID, inv.calls[0].ContractID) + + // empty DataIDs must fail preconditions + empty := *req + empty.DataIDs = nil + require.Error(t, SetFeedFrozen{}.VerifyPreconditions(env, &empty)) + + // invalid admin address must fail preconditions + badAdmin := *req + badAdmin.Admin = "not-a-key" + require.Error(t, SetFeedFrozen{}.VerifyPreconditions(env, &badAdmin)) + + // missing cache ref must fail preconditions + badQualifier := *req + badQualifier.Qualifier = "does-not-exist" + require.Error(t, SetFeedFrozen{}.VerifyPreconditions(env, &badQualifier)) +} diff --git a/deployment/data-feeds/changeset/stellar/set_proxy_cache_test.go b/deployment/data-feeds/changeset/stellar/set_proxy_cache_test.go new file mode 100644 index 00000000000..2ff1e334a87 --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/set_proxy_cache_test.go @@ -0,0 +1,40 @@ +package stellar + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSetProxyCacheChangeset(t *testing.T) { + env, inv, _ := newTestEnv(t) + seedContractRefs(t, &env, + contractRefSpec{ProxyContract, testProxyAddress, "test-proxy", "1.0.0"}, + contractRefSpec{CacheContract, testContractID, "test-cache", "1.0.0"}, + ) + + req := &SetProxyCacheRequest{ + ChainSel: testChainSel, + Qualifier: "test-proxy", + Version: "1.0.0", + CacheQualifier: "test-cache", + } + require.NoError(t, SetProxyCache{}.VerifyPreconditions(env, req)) + + _, err := SetProxyCache{}.Apply(env, req) + require.NoError(t, err) + require.Len(t, inv.calls, 1) + require.Equal(t, "set_cache", inv.calls[0].Function) + require.Equal(t, testProxyAddress, inv.calls[0].ContractID) + require.Len(t, inv.calls[0].Args, 1) + + // missing cache ref must fail preconditions (proxy ref alone isn't enough) + proxyOnly, _, _ := newTestEnv(t) + seedProxyRef(t, &proxyOnly, testProxyAddress, "test-proxy", "1.0.0") + require.Error(t, SetProxyCache{}.VerifyPreconditions(proxyOnly, req)) + + // missing proxy ref must fail preconditions (cache ref alone isn't enough) + cacheOnly, _, _ := newTestEnv(t) + seedCacheRef(t, &cacheOnly, testContractID, "test-cache", "1.0.0") + require.Error(t, SetProxyCache{}.VerifyPreconditions(cacheOnly, req)) +} diff --git a/deployment/data-feeds/changeset/stellar/testutil_test.go b/deployment/data-feeds/changeset/stellar/testutil_test.go index 81f485dd18f..4e603f3cadd 100644 --- a/deployment/data-feeds/changeset/stellar/testutil_test.go +++ b/deployment/data-feeds/changeset/stellar/testutil_test.go @@ -2,11 +2,136 @@ package stellar import ( "context" + "os" + "path/filepath" + "testing" + "github.com/Masterminds/semver/v3" + chainsel "github.com/smartcontractkit/chain-selectors" + "github.com/stellar/go-stellar-sdk/keypair" protocolrpc "github.com/stellar/go-stellar-sdk/protocols/rpc" "github.com/stellar/go-stellar-sdk/xdr" + "github.com/stretchr/testify/require" + + cldfchain "github.com/smartcontractkit/chainlink-deployments-framework/chain" + cldfstellar "github.com/smartcontractkit/chainlink-deployments-framework/chain/stellar" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink-deployments-framework/offchain/ocr" + cldflogger "github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger" + + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/stellar/operation" +) + +const ( + testContractID = "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA" + testAdmin = "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN7" + // testProxyAddress is a second, distinct contract address used wherever a + // test needs to tell the proxy and cache refs apart (e.g. asserting Apply + // invoked the proxy, not the cache). + testProxyAddress = "CBPROXY00000000000000000000000000000000000000000000000AA" ) +// testChainSel is a real Stellar chain selector (from chain-selectors) so +// that ChainMetadata methods (Family, NetworkType, ...) resolve correctly. +var testChainSel = chainsel.STELLAR_TESTNET.Selector + +// newTestEnv returns an Environment with one fake Stellar chain and swapped-in fake deps. +// It swaps the package-level newStellarDeps seam to hand back fakeInvoker/fakeDeployer +// instead of talking to a real Soroban RPC endpoint, and restores the seam on cleanup. +func newTestEnv(t *testing.T) (cldf.Environment, *fakeInvoker, *fakeDeployer) { + t.Helper() + + kp, err := keypair.Random() + require.NoError(t, err) + + chain := cldfstellar.Chain{ + ChainMetadata: cldfstellar.ChainMetadata{Selector: testChainSel}, + Signer: cldfstellar.NewStellarKeypairSigner(kp), + } + + invoker := &fakeInvoker{} + deployer := &fakeDeployer{contractID: testContractID} + + origDeps := newStellarDeps + t.Cleanup(func() { newStellarDeps = origDeps }) + newStellarDeps = func(_ cldfstellar.Chain) (operation.StellarDeps, error) { + return operation.StellarDeps{Deploy: deployer, Invoker: invoker}, nil + } + + blockChains := cldfchain.NewBlockChains(map[uint64]cldfchain.BlockChain{ + testChainSel: chain, + }) + + env := cldf.NewEnvironment( + "test", + cldflogger.Test(t), + nil, // ExistingAddresses: unused by these changesets, DataStore is authoritative + datastore.NewMemoryDataStore().Seal(), + nil, + nil, + func() context.Context { return context.Background() }, + ocr.OCRSecrets{}, + blockChains, + ) + + return *env, invoker, deployer +} + +// contractRefSpec describes one AddressRef to seed via seedContractRefs. +type contractRefSpec struct { + contractType datastore.ContractType + address string + qualifier string + version string +} + +// seedContractRefs pre-seeds env.DataStore with the given AddressRefs (all in +// one MemoryDataStore) so changesets can resolve contracts by +// (ChainSel, Type, Version, Qualifier). Replaces any refs seeded by a prior +// call on the same env — call once per test with every ref the test needs +// (e.g. both a proxy and a cache ref for SetProxyCache). +func seedContractRefs(t *testing.T, env *cldf.Environment, refs ...contractRefSpec) { + t.Helper() + ds := datastore.NewMemoryDataStore() + for _, r := range refs { + require.NoError(t, ds.Addresses().Add(datastore.AddressRef{ + Address: r.address, + ChainSelector: testChainSel, + Type: r.contractType, + Version: semver.MustParse(r.version), + Qualifier: r.qualifier, + })) + } + env.DataStore = ds.Seal() +} + +// seedCacheRef pre-seeds env.DataStore with a single cache AddressRef so +// config changesets (SetFeedConfigs, RemoveFeedConfigs, Add/RemoveFeedAdmin) +// can resolve the cache contract by (ChainSel, CacheContract, Version, Qualifier). +func seedCacheRef(t *testing.T, env *cldf.Environment, address, qualifier, version string) { + t.Helper() + seedContractRefs(t, env, contractRefSpec{CacheContract, address, qualifier, version}) +} + +// seedProxyRef pre-seeds env.DataStore with a single proxy AddressRef so +// changesets can resolve the proxy contract by +// (ChainSel, ProxyContract, Version, Qualifier). +func seedProxyRef(t *testing.T, env *cldf.Environment, address, qualifier, version string) { + t.Helper() + seedContractRefs(t, env, contractRefSpec{ProxyContract, address, qualifier, version}) +} + +// writeDummyWasm writes a placeholder wasm file under t.TempDir() so +// VerifyPreconditions' filesystem check (os.Stat) has a real path to find. +// The contents are never read — deployment is faked via fakeDeployer. +func writeDummyWasm(t *testing.T, name string) string { + t.Helper() + path := filepath.Join(t.TempDir(), name) + require.NoError(t, os.WriteFile(path, []byte("dummy wasm"), 0o600)) + return path +} + type invocation struct { ContractID string Function string diff --git a/deployment/data-feeds/changeset/stellar/upgrade_test.go b/deployment/data-feeds/changeset/stellar/upgrade_test.go new file mode 100644 index 00000000000..e528791ebf0 --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/upgrade_test.go @@ -0,0 +1,76 @@ +package stellar + +import ( + "testing" + + "github.com/stellar/go-stellar-sdk/xdr" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-stellar/bindings/scval" +) + +func TestUpgradeChangeset(t *testing.T) { + env, inv, dep := newTestEnv(t) + seedContractRefs(t, &env, + contractRefSpec{CacheContract, testContractID, "test", "1.0.0"}, + contractRefSpec{ProxyContract, testProxyAddress, "test-proxy", "1.0.0"}, + ) + + var wantHash xdr.Hash + for i := range wantHash { + wantHash[i] = byte(i + 1) + } + dep.wasmHash = wantHash + + req := &UpgradeRequest{ + ChainSel: testChainSel, + Qualifier: "test", + Version: "1.0.0", + Contract: CacheContract, + WasmPath: writeDummyWasm(t, "new_cache.wasm"), + } + require.NoError(t, Upgrade{}.VerifyPreconditions(env, req)) + + _, err := Upgrade{}.Apply(env, req) + require.NoError(t, err) + + // the upload happened, against the requested wasm path + require.Len(t, dep.uploads, 1) + require.Equal(t, req.WasmPath, dep.uploads[0]) + + // the upgrade invocation carried the fake's uploaded hash through + require.Len(t, inv.calls, 1) + require.Equal(t, "upgrade", inv.calls[0].Function) + require.Equal(t, testContractID, inv.calls[0].ContractID) + require.Len(t, inv.calls[0].Args, 1) + + gotHash, err := scval.Bytes32FromScVal(inv.calls[0].Args[0]) + require.NoError(t, err) + require.Equal(t, [32]byte(wantHash), gotHash) + + // proxy path targets the proxy ref + proxyReq := *req + proxyReq.Qualifier = "test-proxy" + proxyReq.Contract = ProxyContract + require.NoError(t, Upgrade{}.VerifyPreconditions(env, &proxyReq)) + _, err = Upgrade{}.Apply(env, &proxyReq) + require.NoError(t, err) + require.Len(t, inv.calls, 2) + require.Equal(t, "upgrade", inv.calls[1].Function) + require.Equal(t, testProxyAddress, inv.calls[1].ContractID) + + // unsupported contract type must fail preconditions + badContract := *req + badContract.Contract = "NotAContract" + require.Error(t, Upgrade{}.VerifyPreconditions(env, &badContract)) + + // missing wasm path must fail preconditions + badPath := *req + badPath.WasmPath = "/does/not/exist.wasm" + require.Error(t, Upgrade{}.VerifyPreconditions(env, &badPath)) + + // missing cache ref must fail preconditions + badQualifier := *req + badQualifier.Qualifier = "does-not-exist" + require.Error(t, Upgrade{}.VerifyPreconditions(env, &badQualifier)) +} From c0a741fd84f2ea7ede3f3235c87d91d4c9a4a884 Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Wed, 5 Aug 2026 12:45:48 +0100 Subject: [PATCH 11/19] Bake operations into their changeset files - Move each CLDF operation from operation/operation.go into the action file that executes it, mirroring the Aptos layout; the operation package is deleted - Operation vars and inputs are unexported; shared machinery (StellarDeps, contractAdmin, adminClient) moves to deps.go - Drop the package README --- .../data-feeds/changeset/stellar/README.md | 120 -------- .../data-feeds/changeset/stellar/deploy.go | 60 +++- .../data-feeds/changeset/stellar/deps.go | 57 +++- .../changeset/stellar/feed_admin.go | 30 +- .../changeset/stellar/operation/operation.go | 273 ------------------ .../data-feeds/changeset/stellar/ownership.go | 40 ++- .../changeset/stellar/recover_tokens.go | 20 +- .../changeset/stellar/remove_feed_configs.go | 20 +- .../changeset/stellar/set_feed_configs.go | 19 +- .../changeset/stellar/set_feed_frozen.go | 21 +- .../changeset/stellar/set_proxy_cache.go | 19 +- .../changeset/stellar/testutil_test.go | 6 +- .../data-feeds/changeset/stellar/upgrade.go | 39 ++- 13 files changed, 287 insertions(+), 437 deletions(-) delete mode 100644 deployment/data-feeds/changeset/stellar/README.md delete mode 100644 deployment/data-feeds/changeset/stellar/operation/operation.go diff --git a/deployment/data-feeds/changeset/stellar/README.md b/deployment/data-feeds/changeset/stellar/README.md deleted file mode 100644 index 8106c8f30e0..00000000000 --- a/deployment/data-feeds/changeset/stellar/README.md +++ /dev/null @@ -1,120 +0,0 @@ -### Stellar Data Feeds Changesets - -CLDF `ChangeSetV2` suite for the Soroban `DataFeedsCache` and `DataFeedsProxy` -contracts. Style follows the Solana suite (`ChangeSetV2` + operations); -functional coverage mirrors the EVM Data Feeds suite (deploy / configure / -admin / remove / ownership / proxy — `jd_*`, `migrate_feeds`, `import_address` -are EVM/JD-specific and deliberately have no Stellar equivalent here). - -#### Package layout - -``` -data-feeds/changeset/stellar/ -├── deploy.go # DeployCache / DeployProxy — instantiate + record AddressRefs -├── set_feed_configs.go # descriptions + workflow permissions -├── remove_feed_configs.go # remove feed configs -├── set_feed_frozen.go # freeze/unfreeze feeds -├── feed_admin.go # grant/revoke feed-admin -├── ownership.go # transfer / accept / renounce (cache + proxy) -├── upgrade.go, recover_tokens.go # new WASM / token recovery (cache + proxy) -├── set_proxy_cache.go # repoint the proxy at a cache -├── deps.go, state.go # shared resolve-ref/build-deps + client loaders -├── validation.go, config.go # input validation, encoding helpers -├── operation/operation.go # one CLDF operation per on-chain call -└── testdata/ # e2e WASM fixtures — see testdata/README.md -``` - -Every action file has a matching `_test.go`; shared test scaffolding (fake -deps, env builder, datastore seeding) lives in `testutil_test.go`. - -Each changeset's `Apply` resolves a `datastore.AddressRef` (chain selector + -`ContractType` + version + qualifier) to a `stellarApplyDeps` bundle — the -contract ID plus the `operation.StellarDeps` (Deploy + Invoker) needed to -call it — then executes one or more `operation.*` calls from -`operation/operation.go` through `env.OperationsBundle`. Operations are thin: -each wraps exactly one generated-binding call -(`chainlink-stellar/bindings/contracts/data_feeds_{cache,proxy}`) or one -deployer call. `deps.go`'s `verifyContractRef` / `resolveContractDeps` hold -the chain-exists + version-parse + ref-exists + build-deps skeleton shared by -every changeset in this package. - -#### Permissions model — no standalone "set forwarder" - -There is no `SetForwarder`-style changeset here. A feed's writer allowlist is -part of its `FeedConfig` and is set via `SetFeedConfigs` -(`configure_cache.go`): each `FeedPermission` carries an `AllowedSender` — -the CRE forwarder contract address — plus `AllowedWorkflowOwner` and -`AllowedWorkflowName`, checked on-chain by `on_report`. This mirrors EVM's -`WorkflowMetadata{AllowedSender, AllowedWorkflowOwner, AllowedWorkflowName}` -gate. To point feeds at a new forwarder, re-run `SetFeedConfigs` with updated -`Permissions` — there's no separate on-chain "forwarder" slot to update. - -#### Cross-contract version coupling - -`DeployProxy` and `SetProxyCache` both resolve their cache `AddressRef` using -the *proxy's own* `req.Version`, not a separate cache-version field. This -suite's convention is that cross-contract datastore lookups share the acting -changeset's `Version` — so a cache recorded under a different version needs a -matching-version record before a proxy at that version can find it. An -optional `CacheVersion` field on these requests (to let the two diverge) is a -recorded follow-up, not implemented here. - -#### Regenerating testdata WASM + bindings - -The e2e test (`e2e_test.go`) deploys real WASM to a local devnet; the fixtures -in `testdata/` and the generated Go bindings in -`chainlink-stellar/bindings/contracts/data_feeds_{cache,proxy}` are a -**matched pair** — a contract ABI change requires regenerating both together, -or the e2e test will fail to compile or decode. See `testdata/README.md` for -checksums and the exact regeneration steps; in short: - -```sh -# 1. rebuild WASM from the contract source repo -cd /contracts/data-feeds -stellar contract build # stellar CLI 27.0.0 -cp target/wasm32v1-none/release/data_feeds_{cache,proxy}.wasm \ - /deployment/data-feeds/changeset/stellar/testdata/ -# (older stellar CLIs output to target/wasm32-unknown-unknown/release/ instead) - -# 2. regenerate bindings from the new WASM, in chainlink-stellar -./scripts/gen_bindings.sh -``` - -#### Running the e2e test - -`TestStellarDataFeedsE2E` boots a local CTF Soroban devnet, deploys both -contracts, and invokes all 40 exported functions (26 cache + 14 proxy): 38 -succeed on-chain, and `recover_tokens` on each contract fails with the -documented `InvalidAction` host error (self-transfer re-entry — see the test's -`recover_tokens` comments). Reads on a frozen feed are additionally asserted -to fail with the cache's `FeedFrozen` error. It needs Docker and does not run -in CI (`skipInCI` skips when `CI=true`). - -```sh -go test ./data-feeds/changeset/stellar/ -run TestStellarDataFeedsE2E -v -timeout 25m -``` - -The default path boots `stellar/quickstart:future` via CLDF's -`NewCTFChainProvider`, hardcoded by the CTF framework to `linux/amd64` — on a -native arm64 host without amd64 emulation this fails (`exec format error`). -Override with `STELLAR_E2E_RPC_URL` / `STELLAR_E2E_FRIENDBOT_URL` to attach to -a quickstart container started natively instead: - -```sh -docker run -d -p 8000:8000 stellar/quickstart:latest \ - --local --enable-soroban-rpc --protocol-version 26 -STELLAR_E2E_RPC_URL=http://127.0.0.1:8000/rpc \ -STELLAR_E2E_FRIENDBOT_URL=http://127.0.0.1:8000/friendbot \ -go test ./data-feeds/changeset/stellar/ -run TestStellarDataFeedsE2E -v -timeout 25m -``` - -See the doc comment on `newE2EChain` for why protocol 26 (not quickstart's -default 25) is required. - -#### Out of scope - -- **Durable-pipeline registration** in `chainlink-deployments` — follow-up - work, coordinate with PLEX-2923. -- **MCMS** (multi-chain multisig) integration — PLEX-owned. -- **Forwarder / timelock deployment** — PLEX-owned; this package only wires - an already-deployed forwarder's address into `FeedPermission.AllowedSender`. diff --git a/deployment/data-feeds/changeset/stellar/deploy.go b/deployment/data-feeds/changeset/stellar/deploy.go index ce848ceacaa..7c56e979f05 100644 --- a/deployment/data-feeds/changeset/stellar/deploy.go +++ b/deployment/data-feeds/changeset/stellar/deploy.go @@ -6,13 +6,13 @@ import ( "os" "github.com/Masterminds/semver/v3" + "github.com/stellar/go-stellar-sdk/xdr" "github.com/smartcontractkit/chainlink-deployments-framework/datastore" cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/smartcontractkit/chainlink-deployments-framework/operations" + "github.com/smartcontractkit/chainlink-stellar/bindings/scval" stellardeploy "github.com/smartcontractkit/chainlink-stellar/deployment" - - "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/stellar/operation" ) // DeployCacheRequest configures a DataFeedsCache deployment. @@ -61,7 +61,7 @@ func (DeployCache) Apply(env cldf.Environment, req *DeployCacheRequest) (cldf.Ch } salt := stellardeploy.GenerateDeterministicSalt(owner, "data_feeds_cache-"+req.Qualifier) - report, err := operations.ExecuteOperation(env.OperationsBundle, operation.DeployCache, deps, operation.DeployCacheInput{ + report, err := operations.ExecuteOperation(env.OperationsBundle, deployCacheOp, deps, deployCacheInput{ WasmPath: req.WasmPath, Salt: salt, Owner: owner, @@ -128,7 +128,7 @@ func (DeployProxy) Apply(env cldf.Environment, req *DeployProxyRequest) (cldf.Ch } salt := stellardeploy.GenerateDeterministicSalt(owner, "data_feeds_proxy-"+req.Qualifier) - report, err := operations.ExecuteOperation(env.OperationsBundle, operation.DeployProxy, deps, operation.DeployProxyInput{ + report, err := operations.ExecuteOperation(env.OperationsBundle, deployProxyOp, deps, deployProxyInput{ WasmPath: req.WasmPath, Salt: salt, Owner: owner, @@ -139,3 +139,55 @@ func (DeployProxy) Apply(env cldf.Environment, req *DeployProxyRequest) (cldf.Ch } return recordAddress(report.Output.ContractID, req.ChainSel, ProxyContract, req.Qualifier, req.Version, req.LabelSet) } + +type deployOutput struct { + ContractID string `json:"contract_id"` +} + +type deployCacheInput struct { + WasmPath string `json:"wasm_path"` + Salt [32]byte `json:"salt"` + Owner string `json:"owner"` +} + +// deployCacheOp uploads the cache WASM and instantiates it via CreateContractV2 +// with __constructor(owner); the data-retention TTL is an on-chain constant, +// not a constructor input. +var deployCacheOp = operations.NewOperation( + "df-cache:deploy", opVersion, + "Deploys the DataFeedsCache Soroban contract", + func(b operations.Bundle, d StellarDeps, in deployCacheInput) (deployOutput, error) { + args := []xdr.ScVal{ + scval.AddressToScVal(in.Owner), + } + cid, err := d.Deploy.DeployContractWithArgs(b.GetContext(), in.WasmPath, in.Salt, args) + if err != nil { + return deployOutput{}, err + } + return deployOutput{ContractID: cid}, nil + }, +) + +type deployProxyInput struct { + WasmPath string `json:"wasm_path"` + Salt [32]byte `json:"salt"` + Owner string `json:"owner"` + Cache string `json:"cache"` +} + +// deployProxyOp instantiates the proxy via __constructor(owner, cache). +var deployProxyOp = operations.NewOperation( + "df-proxy:deploy", opVersion, + "Deploys the DataFeedsProxy Soroban contract", + func(b operations.Bundle, d StellarDeps, in deployProxyInput) (deployOutput, error) { + args := []xdr.ScVal{ + scval.AddressToScVal(in.Owner), + scval.AddressToScVal(in.Cache), + } + cid, err := d.Deploy.DeployContractWithArgs(b.GetContext(), in.WasmPath, in.Salt, args) + if err != nil { + return deployOutput{}, err + } + return deployOutput{ContractID: cid}, nil + }, +) diff --git a/deployment/data-feeds/changeset/stellar/deps.go b/deployment/data-feeds/changeset/stellar/deps.go index 4e3ddcd6219..7719cade875 100644 --- a/deployment/data-feeds/changeset/stellar/deps.go +++ b/deployment/data-feeds/changeset/stellar/deps.go @@ -1,35 +1,76 @@ package stellar import ( + "context" "errors" "fmt" "github.com/Masterminds/semver/v3" + "github.com/stellar/go-stellar-sdk/xdr" cldfstellar "github.com/smartcontractkit/chainlink-deployments-framework/chain/stellar" "github.com/smartcontractkit/chainlink-deployments-framework/datastore" cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink-stellar/bindings" + cache "github.com/smartcontractkit/chainlink-stellar/bindings/contracts/data_feeds_cache" + proxy "github.com/smartcontractkit/chainlink-stellar/bindings/contracts/data_feeds_proxy" stellardeploy "github.com/smartcontractkit/chainlink-stellar/deployment" - - "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/stellar/operation" ) +// StellarDeps bundles deploy-time and runtime chain I/O for the operations. +// The same *deployment.Deployer satisfies both fields. +type StellarDeps struct { + Deploy SorobanContractDeployer + Invoker bindings.Invoker +} + +// SorobanContractDeployer is the deploy-time surface the operations need from +// chainlink-stellar's concrete deployment.Deployer, defined locally so the +// upstream package does not have to export a widened interface. +type SorobanContractDeployer interface { + DeployContractWithArgs(ctx context.Context, wasmPath string, salt [32]byte, ctorArgs []xdr.ScVal) (string, error) + UploadContractWASM(ctx context.Context, wasmPath string) (xdr.Hash, error) +} + +// void is the output for operations that return no payload. +type void struct{} + +var opVersion = semver.MustParse("1.0.0") + +// contractAdmin is the ownership/upgrade/recovery surface shared by the +// generated cache and proxy clients. +type contractAdmin interface { + TransferOwnership(ctx context.Context, newOwner string, liveUntilLedger uint32) error + AcceptOwnership(ctx context.Context) error + RenounceOwnership(ctx context.Context) error + Upgrade(ctx context.Context, newWasmHash [32]byte) error + RecoverTokens(ctx context.Context, token, to string, amount int64) error +} + +// adminClient returns the generated client selected by isProxy. +func adminClient(d StellarDeps, contractID string, isProxy bool) contractAdmin { + if isProxy { + return proxy.NewDataFeedsProxyClient(d.Invoker, contractID) + } + return cache.NewDataFeedsCacheClient(d.Invoker, contractID) +} + // newStellarDeps builds the deploy/invoke dependencies for a CLDF Stellar // chain. Package-level var so unit tests can substitute fakes. -var newStellarDeps = func(ch cldfstellar.Chain) (operation.StellarDeps, error) { +var newStellarDeps = func(ch cldfstellar.Chain) (StellarDeps, error) { d, err := stellardeploy.NewDeployerFromChain(ch) if err != nil { - return operation.StellarDeps{}, err + return StellarDeps{}, err } - return operation.StellarDeps{Deploy: d, Invoker: d}, nil + return StellarDeps{Deploy: d, Invoker: d}, nil } // chainDeps resolves the CLDF Stellar chain for chainSel and builds its // deploy/invoke deps. -func chainDeps(env cldf.Environment, chainSel uint64) (cldfstellar.Chain, operation.StellarDeps, error) { +func chainDeps(env cldf.Environment, chainSel uint64) (cldfstellar.Chain, StellarDeps, error) { ch, ok := env.BlockChains.StellarChains()[chainSel] if !ok { - return cldfstellar.Chain{}, operation.StellarDeps{}, fmt.Errorf("stellar chain not found for chain selector %d", chainSel) + return cldfstellar.Chain{}, StellarDeps{}, fmt.Errorf("stellar chain not found for chain selector %d", chainSel) } deps, err := newStellarDeps(ch) return ch, deps, err @@ -64,7 +105,7 @@ func recordAddress(address string, chainSel uint64, contractType datastore.Contr // stellarApplyDeps bundles a resolved contract's address with the chain deps // needed to invoke operations against it. type stellarApplyDeps struct { - deps operation.StellarDeps + deps StellarDeps contractID string } diff --git a/deployment/data-feeds/changeset/stellar/feed_admin.go b/deployment/data-feeds/changeset/stellar/feed_admin.go index 7c83aaf4c62..a17cc95cd7f 100644 --- a/deployment/data-feeds/changeset/stellar/feed_admin.go +++ b/deployment/data-feeds/changeset/stellar/feed_admin.go @@ -3,8 +3,7 @@ package stellar import ( cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/smartcontractkit/chainlink-deployments-framework/operations" - - "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/stellar/operation" + cache "github.com/smartcontractkit/chainlink-stellar/bindings/contracts/data_feeds_cache" ) // FeedAdminRequest grants or revokes feed-admin rights on an already-deployed @@ -44,7 +43,7 @@ func (AddFeedAdmin) Apply(env cldf.Environment, req *FeedAdminRequest) (cldf.Cha if err != nil { return out, err } - _, err = operations.ExecuteOperation(env.OperationsBundle, operation.AddFeedAdmin, d.deps, operation.FeedAdminInput{ + _, err = operations.ExecuteOperation(env.OperationsBundle, addFeedAdminOp, d.deps, feedAdminInput{ ContractID: d.contractID, Admin: req.Admin, }) @@ -64,9 +63,32 @@ func (RemoveFeedAdmin) Apply(env cldf.Environment, req *FeedAdminRequest) (cldf. if err != nil { return out, err } - _, err = operations.ExecuteOperation(env.OperationsBundle, operation.RemoveFeedAdmin, d.deps, operation.FeedAdminInput{ + _, err = operations.ExecuteOperation(env.OperationsBundle, removeFeedAdminOp, d.deps, feedAdminInput{ ContractID: d.contractID, Admin: req.Admin, }) return out, err } + +type feedAdminInput struct { + ContractID string `json:"contract_id"` + Admin string `json:"admin"` +} + +var addFeedAdminOp = operations.NewOperation( + "df-cache:add-feed-admin", opVersion, + "Grants feed-admin rights on the cache", + func(b operations.Bundle, d StellarDeps, in feedAdminInput) (void, error) { + c := cache.NewDataFeedsCacheClient(d.Invoker, in.ContractID) + return void{}, c.AddFeedAdmin(b.GetContext(), in.Admin) + }, +) + +var removeFeedAdminOp = operations.NewOperation( + "df-cache:remove-feed-admin", opVersion, + "Revokes feed-admin rights on the cache", + func(b operations.Bundle, d StellarDeps, in feedAdminInput) (void, error) { + c := cache.NewDataFeedsCacheClient(d.Invoker, in.ContractID) + return void{}, c.RemoveFeedAdmin(b.GetContext(), in.Admin) + }, +) diff --git a/deployment/data-feeds/changeset/stellar/operation/operation.go b/deployment/data-feeds/changeset/stellar/operation/operation.go deleted file mode 100644 index 9fdcf350d0e..00000000000 --- a/deployment/data-feeds/changeset/stellar/operation/operation.go +++ /dev/null @@ -1,273 +0,0 @@ -// Package operation contains the CLDF operations for the Stellar Data Feeds -// cache and proxy contracts. Each operation is one on-chain interaction; the -// changesets in the parent package compose and execute them. -package operation - -import ( - "context" - - "github.com/Masterminds/semver/v3" - - cldfops "github.com/smartcontractkit/chainlink-deployments-framework/operations" - "github.com/smartcontractkit/chainlink-stellar/bindings" - cache "github.com/smartcontractkit/chainlink-stellar/bindings/contracts/data_feeds_cache" - proxy "github.com/smartcontractkit/chainlink-stellar/bindings/contracts/data_feeds_proxy" - "github.com/smartcontractkit/chainlink-stellar/bindings/scval" - "github.com/stellar/go-stellar-sdk/xdr" -) - -var Version1_0_0 = semver.MustParse("1.0.0") - -// SorobanContractDeployer is the deploy-time surface these operations need -// from chainlink-stellar's concrete deployment.Deployer, defined locally so -// the upstream package does not have to export a widened interface. -type SorobanContractDeployer interface { - DeployContractWithArgs(ctx context.Context, wasmPath string, salt [32]byte, ctorArgs []xdr.ScVal) (string, error) - UploadContractWASM(ctx context.Context, wasmPath string) (xdr.Hash, error) -} - -// StellarDeps bundles deploy-time and runtime chain I/O for these operations. -// The same *deployment.Deployer satisfies both fields. -type StellarDeps struct { - Deploy SorobanContractDeployer - Invoker bindings.Invoker -} - -// contractAdmin is the ownership/upgrade/recovery surface shared by the -// generated cache and proxy clients. -type contractAdmin interface { - TransferOwnership(ctx context.Context, newOwner string, liveUntilLedger uint32) error - AcceptOwnership(ctx context.Context) error - RenounceOwnership(ctx context.Context) error - Upgrade(ctx context.Context, newWasmHash [32]byte) error - RecoverTokens(ctx context.Context, token, to string, amount int64) error -} - -// adminClient returns the generated client selected by isProxy. -func adminClient(d StellarDeps, contractID string, isProxy bool) contractAdmin { - if isProxy { - return proxy.NewDataFeedsProxyClient(d.Invoker, contractID) - } - return cache.NewDataFeedsCacheClient(d.Invoker, contractID) -} - -// Void is the output for operations that return no payload. -type Void struct{} - -// DeployOutput carries a deployed contract address (C... strkey). -type DeployOutput struct { - ContractID string `json:"contract_id"` -} - -type DeployCacheInput struct { - WasmPath string `json:"wasm_path"` - Salt [32]byte `json:"salt"` - Owner string `json:"owner"` -} - -// DeployCache uploads the cache WASM and instantiates it via CreateContractV2 -// with __constructor(owner); the data-retention TTL is an on-chain constant, -// not a constructor input. -var DeployCache = cldfops.NewOperation( - "df-cache:deploy", Version1_0_0, - "Deploys the DataFeedsCache Soroban contract", - func(b cldfops.Bundle, d StellarDeps, in DeployCacheInput) (DeployOutput, error) { - args := []xdr.ScVal{ - scval.AddressToScVal(in.Owner), - } - cid, err := d.Deploy.DeployContractWithArgs(b.GetContext(), in.WasmPath, in.Salt, args) - if err != nil { - return DeployOutput{}, err - } - return DeployOutput{ContractID: cid}, nil - }, -) - -type DeployProxyInput struct { - WasmPath string `json:"wasm_path"` - Salt [32]byte `json:"salt"` - Owner string `json:"owner"` - Cache string `json:"cache"` -} - -// DeployProxy instantiates the proxy via __constructor(owner, cache). -var DeployProxy = cldfops.NewOperation( - "df-proxy:deploy", Version1_0_0, - "Deploys the DataFeedsProxy Soroban contract", - func(b cldfops.Bundle, d StellarDeps, in DeployProxyInput) (DeployOutput, error) { - args := []xdr.ScVal{ - scval.AddressToScVal(in.Owner), - scval.AddressToScVal(in.Cache), - } - cid, err := d.Deploy.DeployContractWithArgs(b.GetContext(), in.WasmPath, in.Salt, args) - if err != nil { - return DeployOutput{}, err - } - return DeployOutput{ContractID: cid}, nil - }, -) - -type SetFeedConfigsInput struct { - ContractID string `json:"contract_id"` - Admin string `json:"admin"` - Entries []cache.FeedConfigEntry `json:"entries"` -} - -var SetFeedConfigs = cldfops.NewOperation( - "df-cache:set-feed-configs", Version1_0_0, - "Sets per-feed descriptions and workflow write-permissions on the cache", - func(b cldfops.Bundle, d StellarDeps, in SetFeedConfigsInput) (Void, error) { - c := cache.NewDataFeedsCacheClient(d.Invoker, in.ContractID) - return Void{}, c.SetFeedConfigs(b.GetContext(), in.Admin, in.Entries) - }, -) - -type RemoveFeedConfigsInput struct { - ContractID string `json:"contract_id"` - Admin string `json:"admin"` - DataIDs [][16]byte `json:"data_ids"` -} - -var RemoveFeedConfigs = cldfops.NewOperation( - "df-cache:remove-feed-configs", Version1_0_0, - "Removes feed configs from the cache", - func(b cldfops.Bundle, d StellarDeps, in RemoveFeedConfigsInput) (Void, error) { - c := cache.NewDataFeedsCacheClient(d.Invoker, in.ContractID) - return Void{}, c.RemoveFeedConfigs(b.GetContext(), in.Admin, in.DataIDs) - }, -) - -type SetFeedFrozenInput struct { - ContractID string `json:"contract_id"` - Admin string `json:"admin"` - DataIDs [][16]byte `json:"data_ids"` - Frozen bool `json:"frozen"` -} - -var SetFeedFrozen = cldfops.NewOperation( - "df-cache:set-feed-frozen", Version1_0_0, - "Freezes or unfreezes feeds on the cache", - func(b cldfops.Bundle, d StellarDeps, in SetFeedFrozenInput) (Void, error) { - c := cache.NewDataFeedsCacheClient(d.Invoker, in.ContractID) - return Void{}, c.SetFeedFrozen(b.GetContext(), in.Admin, in.DataIDs, in.Frozen) - }, -) - -type FeedAdminInput struct { - ContractID string `json:"contract_id"` - Admin string `json:"admin"` -} - -var AddFeedAdmin = cldfops.NewOperation( - "df-cache:add-feed-admin", Version1_0_0, - "Grants feed-admin rights on the cache", - func(b cldfops.Bundle, d StellarDeps, in FeedAdminInput) (Void, error) { - c := cache.NewDataFeedsCacheClient(d.Invoker, in.ContractID) - return Void{}, c.AddFeedAdmin(b.GetContext(), in.Admin) - }, -) - -var RemoveFeedAdmin = cldfops.NewOperation( - "df-cache:remove-feed-admin", Version1_0_0, - "Revokes feed-admin rights on the cache", - func(b cldfops.Bundle, d StellarDeps, in FeedAdminInput) (Void, error) { - c := cache.NewDataFeedsCacheClient(d.Invoker, in.ContractID) - return Void{}, c.RemoveFeedAdmin(b.GetContext(), in.Admin) - }, -) - -// Ownership ops work for both contracts: the generated cache and proxy clients -// expose identical two-step-ownership methods, selected by ContractID + IsProxy. -type OwnershipInput struct { - ContractID string `json:"contract_id"` - IsProxy bool `json:"is_proxy"` - NewOwner string `json:"new_owner"` - LiveUntilLedger uint32 `json:"live_until_ledger"` -} - -var TransferOwnership = cldfops.NewOperation( - "df:transfer-ownership", Version1_0_0, - "Begins two-step ownership transfer", - func(b cldfops.Bundle, d StellarDeps, in OwnershipInput) (Void, error) { - return Void{}, adminClient(d, in.ContractID, in.IsProxy).TransferOwnership(b.GetContext(), in.NewOwner, in.LiveUntilLedger) - }, -) - -var AcceptOwnership = cldfops.NewOperation( - "df:accept-ownership", Version1_0_0, - "Accepts a pending ownership transfer (caller must be the pending owner)", - func(b cldfops.Bundle, d StellarDeps, in OwnershipInput) (Void, error) { - return Void{}, adminClient(d, in.ContractID, in.IsProxy).AcceptOwnership(b.GetContext()) - }, -) - -var RenounceOwnership = cldfops.NewOperation( - "df:renounce-ownership", Version1_0_0, - "Renounces ownership permanently", - func(b cldfops.Bundle, d StellarDeps, in OwnershipInput) (Void, error) { - return Void{}, adminClient(d, in.ContractID, in.IsProxy).RenounceOwnership(b.GetContext()) - }, -) - -type UploadWASMInput struct { - WasmPath string `json:"wasm_path"` -} -type UploadWASMOutput struct { - WasmHash [32]byte `json:"wasm_hash"` -} - -var UploadWASM = cldfops.NewOperation( - "df:upload-wasm", Version1_0_0, - "Uploads a WASM blob and returns its code hash (for upgrades)", - func(b cldfops.Bundle, d StellarDeps, in UploadWASMInput) (UploadWASMOutput, error) { - h, err := d.Deploy.UploadContractWASM(b.GetContext(), in.WasmPath) - if err != nil { - return UploadWASMOutput{}, err - } - return UploadWASMOutput{WasmHash: [32]byte(h)}, nil - }, -) - -type UpgradeContractInput struct { - ContractID string `json:"contract_id"` - IsProxy bool `json:"is_proxy"` - NewWasmHash [32]byte `json:"new_wasm_hash"` -} - -var UpgradeContract = cldfops.NewOperation( - "df:upgrade", Version1_0_0, - "Points the contract at a new WASM implementation", - func(b cldfops.Bundle, d StellarDeps, in UpgradeContractInput) (Void, error) { - return Void{}, adminClient(d, in.ContractID, in.IsProxy).Upgrade(b.GetContext(), in.NewWasmHash) - }, -) - -type RecoverTokensInput struct { - ContractID string `json:"contract_id"` - IsProxy bool `json:"is_proxy"` - Token string `json:"token"` - To string `json:"to"` - Amount int64 `json:"amount"` -} - -var RecoverTokens = cldfops.NewOperation( - "df:recover-tokens", Version1_0_0, - "Recovers tokens accidentally sent to the contract", - func(b cldfops.Bundle, d StellarDeps, in RecoverTokensInput) (Void, error) { - return Void{}, adminClient(d, in.ContractID, in.IsProxy).RecoverTokens(b.GetContext(), in.Token, in.To, in.Amount) - }, -) - -type SetProxyCacheInput struct { - ContractID string `json:"contract_id"` - Cache string `json:"cache"` -} - -var SetProxyCache = cldfops.NewOperation( - "df-proxy:set-cache", Version1_0_0, - "Points the proxy at a cache contract", - func(b cldfops.Bundle, d StellarDeps, in SetProxyCacheInput) (Void, error) { - c := proxy.NewDataFeedsProxyClient(d.Invoker, in.ContractID) - return Void{}, c.SetCache(b.GetContext(), in.Cache) - }, -) diff --git a/deployment/data-feeds/changeset/stellar/ownership.go b/deployment/data-feeds/changeset/stellar/ownership.go index c724300ee6e..4c6afe2be67 100644 --- a/deployment/data-feeds/changeset/stellar/ownership.go +++ b/deployment/data-feeds/changeset/stellar/ownership.go @@ -7,8 +7,6 @@ import ( "github.com/smartcontractkit/chainlink-deployments-framework/datastore" cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/smartcontractkit/chainlink-deployments-framework/operations" - - "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/stellar/operation" ) // OwnershipRequest drives the two-step ownership changesets for either DF @@ -64,7 +62,7 @@ func (TransferOwnership) Apply(env cldf.Environment, req *OwnershipRequest) (cld if err != nil { return out, err } - _, err = operations.ExecuteOperation(env.OperationsBundle, operation.TransferOwnership, d.deps, operation.OwnershipInput{ + _, err = operations.ExecuteOperation(env.OperationsBundle, transferOwnershipOp, d.deps, ownershipInput{ ContractID: d.contractID, IsProxy: req.Contract == ProxyContract, NewOwner: req.NewOwner, @@ -87,7 +85,7 @@ func (AcceptOwnership) Apply(env cldf.Environment, req *OwnershipRequest) (cldf. if err != nil { return out, err } - _, err = operations.ExecuteOperation(env.OperationsBundle, operation.AcceptOwnership, d.deps, operation.OwnershipInput{ + _, err = operations.ExecuteOperation(env.OperationsBundle, acceptOwnershipOp, d.deps, ownershipInput{ ContractID: d.contractID, IsProxy: req.Contract == ProxyContract, }) @@ -107,9 +105,41 @@ func (RenounceOwnership) Apply(env cldf.Environment, req *OwnershipRequest) (cld if err != nil { return out, err } - _, err = operations.ExecuteOperation(env.OperationsBundle, operation.RenounceOwnership, d.deps, operation.OwnershipInput{ + _, err = operations.ExecuteOperation(env.OperationsBundle, renounceOwnershipOp, d.deps, ownershipInput{ ContractID: d.contractID, IsProxy: req.Contract == ProxyContract, }) return out, err } + +// Ownership ops work for both contracts, selected by ContractID + IsProxy. +type ownershipInput struct { + ContractID string `json:"contract_id"` + IsProxy bool `json:"is_proxy"` + NewOwner string `json:"new_owner"` + LiveUntilLedger uint32 `json:"live_until_ledger"` +} + +var transferOwnershipOp = operations.NewOperation( + "df:transfer-ownership", opVersion, + "Begins two-step ownership transfer", + func(b operations.Bundle, d StellarDeps, in ownershipInput) (void, error) { + return void{}, adminClient(d, in.ContractID, in.IsProxy).TransferOwnership(b.GetContext(), in.NewOwner, in.LiveUntilLedger) + }, +) + +var acceptOwnershipOp = operations.NewOperation( + "df:accept-ownership", opVersion, + "Accepts a pending ownership transfer (caller must be the pending owner)", + func(b operations.Bundle, d StellarDeps, in ownershipInput) (void, error) { + return void{}, adminClient(d, in.ContractID, in.IsProxy).AcceptOwnership(b.GetContext()) + }, +) + +var renounceOwnershipOp = operations.NewOperation( + "df:renounce-ownership", opVersion, + "Renounces ownership permanently", + func(b operations.Bundle, d StellarDeps, in ownershipInput) (void, error) { + return void{}, adminClient(d, in.ContractID, in.IsProxy).RenounceOwnership(b.GetContext()) + }, +) diff --git a/deployment/data-feeds/changeset/stellar/recover_tokens.go b/deployment/data-feeds/changeset/stellar/recover_tokens.go index 4c451458301..4c3f31365f8 100644 --- a/deployment/data-feeds/changeset/stellar/recover_tokens.go +++ b/deployment/data-feeds/changeset/stellar/recover_tokens.go @@ -7,8 +7,6 @@ import ( "github.com/smartcontractkit/chainlink-deployments-framework/datastore" cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/smartcontractkit/chainlink-deployments-framework/operations" - - "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/stellar/operation" ) // RecoverTokensRequest recovers tokens accidentally sent to an already-deployed @@ -53,7 +51,7 @@ func (RecoverTokens) Apply(env cldf.Environment, req *RecoverTokensRequest) (cld if err != nil { return out, err } - _, err = operations.ExecuteOperation(env.OperationsBundle, operation.RecoverTokens, d.deps, operation.RecoverTokensInput{ + _, err = operations.ExecuteOperation(env.OperationsBundle, recoverTokensOp, d.deps, recoverTokensInput{ ContractID: d.contractID, IsProxy: req.Contract == ProxyContract, Token: req.Token, @@ -62,3 +60,19 @@ func (RecoverTokens) Apply(env cldf.Environment, req *RecoverTokensRequest) (cld }) return out, err } + +type recoverTokensInput struct { + ContractID string `json:"contract_id"` + IsProxy bool `json:"is_proxy"` + Token string `json:"token"` + To string `json:"to"` + Amount int64 `json:"amount"` +} + +var recoverTokensOp = operations.NewOperation( + "df:recover-tokens", opVersion, + "Recovers tokens accidentally sent to the contract", + func(b operations.Bundle, d StellarDeps, in recoverTokensInput) (void, error) { + return void{}, adminClient(d, in.ContractID, in.IsProxy).RecoverTokens(b.GetContext(), in.Token, in.To, in.Amount) + }, +) diff --git a/deployment/data-feeds/changeset/stellar/remove_feed_configs.go b/deployment/data-feeds/changeset/stellar/remove_feed_configs.go index 3a99fc05382..93553d90fa7 100644 --- a/deployment/data-feeds/changeset/stellar/remove_feed_configs.go +++ b/deployment/data-feeds/changeset/stellar/remove_feed_configs.go @@ -5,8 +5,7 @@ import ( cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/smartcontractkit/chainlink-deployments-framework/operations" - - "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/stellar/operation" + cache "github.com/smartcontractkit/chainlink-stellar/bindings/contracts/data_feeds_cache" ) // RemoveFeedConfigsRequest removes a batch of feed configs from an @@ -51,10 +50,25 @@ func (RemoveFeedConfigs) Apply(env cldf.Environment, req *RemoveFeedConfigsReque if err != nil { return out, err } - _, err = operations.ExecuteOperation(env.OperationsBundle, operation.RemoveFeedConfigs, d.deps, operation.RemoveFeedConfigsInput{ + _, err = operations.ExecuteOperation(env.OperationsBundle, removeFeedConfigsOp, d.deps, removeFeedConfigsInput{ ContractID: d.contractID, Admin: req.Admin, DataIDs: ids, }) return out, err } + +type removeFeedConfigsInput struct { + ContractID string `json:"contract_id"` + Admin string `json:"admin"` + DataIDs [][16]byte `json:"data_ids"` +} + +var removeFeedConfigsOp = operations.NewOperation( + "df-cache:remove-feed-configs", opVersion, + "Removes feed configs from the cache", + func(b operations.Bundle, d StellarDeps, in removeFeedConfigsInput) (void, error) { + c := cache.NewDataFeedsCacheClient(d.Invoker, in.ContractID) + return void{}, c.RemoveFeedConfigs(b.GetContext(), in.Admin, in.DataIDs) + }, +) diff --git a/deployment/data-feeds/changeset/stellar/set_feed_configs.go b/deployment/data-feeds/changeset/stellar/set_feed_configs.go index d4cd5aefc6c..2a05bdae296 100644 --- a/deployment/data-feeds/changeset/stellar/set_feed_configs.go +++ b/deployment/data-feeds/changeset/stellar/set_feed_configs.go @@ -7,8 +7,6 @@ import ( cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/smartcontractkit/chainlink-deployments-framework/operations" cache "github.com/smartcontractkit/chainlink-stellar/bindings/contracts/data_feeds_cache" - - "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/stellar/operation" ) // FeedPermission is the operator-facing shape of a workflow write-permission. @@ -114,10 +112,25 @@ func (SetFeedConfigs) Apply(env cldf.Environment, req *SetFeedConfigsRequest) (c } } - _, err = operations.ExecuteOperation(env.OperationsBundle, operation.SetFeedConfigs, d.deps, operation.SetFeedConfigsInput{ + _, err = operations.ExecuteOperation(env.OperationsBundle, setFeedConfigsOp, d.deps, setFeedConfigsInput{ ContractID: d.contractID, Admin: req.Admin, Entries: entries, }) return out, err } + +type setFeedConfigsInput struct { + ContractID string `json:"contract_id"` + Admin string `json:"admin"` + Entries []cache.FeedConfigEntry `json:"entries"` +} + +var setFeedConfigsOp = operations.NewOperation( + "df-cache:set-feed-configs", opVersion, + "Sets per-feed descriptions and workflow write-permissions on the cache", + func(b operations.Bundle, d StellarDeps, in setFeedConfigsInput) (void, error) { + c := cache.NewDataFeedsCacheClient(d.Invoker, in.ContractID) + return void{}, c.SetFeedConfigs(b.GetContext(), in.Admin, in.Entries) + }, +) diff --git a/deployment/data-feeds/changeset/stellar/set_feed_frozen.go b/deployment/data-feeds/changeset/stellar/set_feed_frozen.go index 6778c381946..343898e6360 100644 --- a/deployment/data-feeds/changeset/stellar/set_feed_frozen.go +++ b/deployment/data-feeds/changeset/stellar/set_feed_frozen.go @@ -5,8 +5,7 @@ import ( cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/smartcontractkit/chainlink-deployments-framework/operations" - - "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/stellar/operation" + cache "github.com/smartcontractkit/chainlink-stellar/bindings/contracts/data_feeds_cache" ) // SetFeedFrozenRequest freezes or unfreezes a batch of feeds on an @@ -54,7 +53,7 @@ func (SetFeedFrozen) Apply(env cldf.Environment, req *SetFeedFrozenRequest) (cld if err != nil { return out, err } - _, err = operations.ExecuteOperation(env.OperationsBundle, operation.SetFeedFrozen, d.deps, operation.SetFeedFrozenInput{ + _, err = operations.ExecuteOperation(env.OperationsBundle, setFeedFrozenOp, d.deps, setFeedFrozenInput{ ContractID: d.contractID, Admin: req.Admin, DataIDs: ids, @@ -62,3 +61,19 @@ func (SetFeedFrozen) Apply(env cldf.Environment, req *SetFeedFrozenRequest) (cld }) return out, err } + +type setFeedFrozenInput struct { + ContractID string `json:"contract_id"` + Admin string `json:"admin"` + DataIDs [][16]byte `json:"data_ids"` + Frozen bool `json:"frozen"` +} + +var setFeedFrozenOp = operations.NewOperation( + "df-cache:set-feed-frozen", opVersion, + "Freezes or unfreezes feeds on the cache", + func(b operations.Bundle, d StellarDeps, in setFeedFrozenInput) (void, error) { + c := cache.NewDataFeedsCacheClient(d.Invoker, in.ContractID) + return void{}, c.SetFeedFrozen(b.GetContext(), in.Admin, in.DataIDs, in.Frozen) + }, +) diff --git a/deployment/data-feeds/changeset/stellar/set_proxy_cache.go b/deployment/data-feeds/changeset/stellar/set_proxy_cache.go index 81339e2a14b..6afedccd57e 100644 --- a/deployment/data-feeds/changeset/stellar/set_proxy_cache.go +++ b/deployment/data-feeds/changeset/stellar/set_proxy_cache.go @@ -8,8 +8,7 @@ import ( "github.com/smartcontractkit/chainlink-deployments-framework/datastore" cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/smartcontractkit/chainlink-deployments-framework/operations" - - "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/stellar/operation" + proxy "github.com/smartcontractkit/chainlink-stellar/bindings/contracts/data_feeds_proxy" ) // SetProxyCacheRequest points an already-deployed DataFeedsProxy contract at a @@ -51,9 +50,23 @@ func (SetProxyCache) Apply(env cldf.Environment, req *SetProxyCacheRequest) (cld return out, fmt.Errorf("cache address ref not found for qualifier %q: %w", req.CacheQualifier, err) } - _, err = operations.ExecuteOperation(env.OperationsBundle, operation.SetProxyCache, proxyDeps.deps, operation.SetProxyCacheInput{ + _, err = operations.ExecuteOperation(env.OperationsBundle, setProxyCacheOp, proxyDeps.deps, setProxyCacheInput{ ContractID: proxyDeps.contractID, Cache: cacheRef.Address, }) return out, err } + +type setProxyCacheInput struct { + ContractID string `json:"contract_id"` + Cache string `json:"cache"` +} + +var setProxyCacheOp = operations.NewOperation( + "df-proxy:set-cache", opVersion, + "Points the proxy at a cache contract", + func(b operations.Bundle, d StellarDeps, in setProxyCacheInput) (void, error) { + c := proxy.NewDataFeedsProxyClient(d.Invoker, in.ContractID) + return void{}, c.SetCache(b.GetContext(), in.Cache) + }, +) diff --git a/deployment/data-feeds/changeset/stellar/testutil_test.go b/deployment/data-feeds/changeset/stellar/testutil_test.go index 4e603f3cadd..467ae93fcd7 100644 --- a/deployment/data-feeds/changeset/stellar/testutil_test.go +++ b/deployment/data-feeds/changeset/stellar/testutil_test.go @@ -19,8 +19,6 @@ import ( cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/smartcontractkit/chainlink-deployments-framework/offchain/ocr" cldflogger "github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger" - - "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/stellar/operation" ) const ( @@ -55,8 +53,8 @@ func newTestEnv(t *testing.T) (cldf.Environment, *fakeInvoker, *fakeDeployer) { origDeps := newStellarDeps t.Cleanup(func() { newStellarDeps = origDeps }) - newStellarDeps = func(_ cldfstellar.Chain) (operation.StellarDeps, error) { - return operation.StellarDeps{Deploy: deployer, Invoker: invoker}, nil + newStellarDeps = func(_ cldfstellar.Chain) (StellarDeps, error) { + return StellarDeps{Deploy: deployer, Invoker: invoker}, nil } blockChains := cldfchain.NewBlockChains(map[uint64]cldfchain.BlockChain{ diff --git a/deployment/data-feeds/changeset/stellar/upgrade.go b/deployment/data-feeds/changeset/stellar/upgrade.go index 1765a693b81..87a5b08ca80 100644 --- a/deployment/data-feeds/changeset/stellar/upgrade.go +++ b/deployment/data-feeds/changeset/stellar/upgrade.go @@ -7,8 +7,6 @@ import ( "github.com/smartcontractkit/chainlink-deployments-framework/datastore" cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/smartcontractkit/chainlink-deployments-framework/operations" - - "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/stellar/operation" ) // UpgradeRequest points an already-deployed cache or proxy contract at a new @@ -48,17 +46,50 @@ func (Upgrade) Apply(env cldf.Environment, req *UpgradeRequest) (cldf.ChangesetO return out, err } - uploadReport, err := operations.ExecuteOperation(env.OperationsBundle, operation.UploadWASM, d.deps, operation.UploadWASMInput{ + uploadReport, err := operations.ExecuteOperation(env.OperationsBundle, uploadWASMOp, d.deps, uploadWASMInput{ WasmPath: req.WasmPath, }) if err != nil { return out, err } - _, err = operations.ExecuteOperation(env.OperationsBundle, operation.UpgradeContract, d.deps, operation.UpgradeContractInput{ + _, err = operations.ExecuteOperation(env.OperationsBundle, upgradeContractOp, d.deps, upgradeContractInput{ ContractID: d.contractID, IsProxy: req.Contract == ProxyContract, NewWasmHash: uploadReport.Output.WasmHash, }) return out, err } + +type uploadWASMInput struct { + WasmPath string `json:"wasm_path"` +} +type uploadWASMOutput struct { + WasmHash [32]byte `json:"wasm_hash"` +} + +var uploadWASMOp = operations.NewOperation( + "df:upload-wasm", opVersion, + "Uploads a WASM blob and returns its code hash (for upgrades)", + func(b operations.Bundle, d StellarDeps, in uploadWASMInput) (uploadWASMOutput, error) { + h, err := d.Deploy.UploadContractWASM(b.GetContext(), in.WasmPath) + if err != nil { + return uploadWASMOutput{}, err + } + return uploadWASMOutput{WasmHash: [32]byte(h)}, nil + }, +) + +type upgradeContractInput struct { + ContractID string `json:"contract_id"` + IsProxy bool `json:"is_proxy"` + NewWasmHash [32]byte `json:"new_wasm_hash"` +} + +var upgradeContractOp = operations.NewOperation( + "df:upgrade", opVersion, + "Points the contract at a new WASM implementation", + func(b operations.Bundle, d StellarDeps, in upgradeContractInput) (void, error) { + return void{}, adminClient(d, in.ContractID, in.IsProxy).Upgrade(b.GetContext(), in.NewWasmHash) + }, +) From 18318e4d6ea60f21d1688ce8e33cb49e06fd8a0b Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Wed, 5 Aug 2026 13:11:52 +0100 Subject: [PATCH 12/19] Order every action file by one canonical skeleton MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Request, changeset type, operation input, VerifyPreconditions, Apply, operation definition — pure moves, no behavior change. Multi-changeset files group per changeset with its operation. --- .../data-feeds/changeset/stellar/deploy.go | 72 +++++++++---------- .../changeset/stellar/feed_admin.go | 28 ++++---- .../data-feeds/changeset/stellar/ownership.go | 48 ++++++------- .../changeset/stellar/recover_tokens.go | 16 ++--- .../changeset/stellar/remove_feed_configs.go | 12 ++-- .../changeset/stellar/set_feed_configs.go | 60 ++++++++-------- .../changeset/stellar/set_feed_frozen.go | 14 ++-- .../changeset/stellar/set_proxy_cache.go | 10 +-- .../data-feeds/changeset/stellar/upgrade.go | 31 ++++---- 9 files changed, 146 insertions(+), 145 deletions(-) diff --git a/deployment/data-feeds/changeset/stellar/deploy.go b/deployment/data-feeds/changeset/stellar/deploy.go index 7c56e979f05..0eb14b60d90 100644 --- a/deployment/data-feeds/changeset/stellar/deploy.go +++ b/deployment/data-feeds/changeset/stellar/deploy.go @@ -31,6 +31,16 @@ var _ cldf.ChangeSetV2[*DeployCacheRequest] = DeployCache{} // its address in the datastore under CacheContract. type DeployCache struct{} +type deployOutput struct { + ContractID string `json:"contract_id"` +} + +type deployCacheInput struct { + WasmPath string `json:"wasm_path"` + Salt [32]byte `json:"salt"` + Owner string `json:"owner"` +} + func (DeployCache) VerifyPreconditions(env cldf.Environment, req *DeployCacheRequest) error { if _, ok := env.BlockChains.StellarChains()[req.ChainSel]; !ok { return fmt.Errorf("stellar chain not found for chain selector %d", req.ChainSel) @@ -72,10 +82,28 @@ func (DeployCache) Apply(env cldf.Environment, req *DeployCacheRequest) (cldf.Ch return recordAddress(report.Output.ContractID, req.ChainSel, CacheContract, req.Qualifier, req.Version, req.LabelSet) } +// deployCacheOp uploads the cache WASM and instantiates it via CreateContractV2 +// with __constructor(owner); the data-retention TTL is an on-chain constant, +// not a constructor input. +var deployCacheOp = operations.NewOperation( + "df-cache:deploy", opVersion, + "Deploys the DataFeedsCache Soroban contract", + func(b operations.Bundle, d StellarDeps, in deployCacheInput) (deployOutput, error) { + args := []xdr.ScVal{ + scval.AddressToScVal(in.Owner), + } + cid, err := d.Deploy.DeployContractWithArgs(b.GetContext(), in.WasmPath, in.Salt, args) + if err != nil { + return deployOutput{}, err + } + return deployOutput{ContractID: cid}, nil + }, +) + // DeployProxyRequest configures a DataFeedsProxy deployment. The cache passed // to __constructor(owner, cache) is resolved from the datastore by (ChainSel, // CacheContract, Version, CacheQualifier) — it must already be recorded. -// Cross-contract lookups share the request's Version (see README). +// Cross-contract lookups share the request's Version. type DeployProxyRequest struct { ChainSel uint64 WasmPath string @@ -92,6 +120,13 @@ var _ cldf.ChangeSetV2[*DeployProxyRequest] = DeployProxy{} // its address in the datastore under ProxyContract. type DeployProxy struct{} +type deployProxyInput struct { + WasmPath string `json:"wasm_path"` + Salt [32]byte `json:"salt"` + Owner string `json:"owner"` + Cache string `json:"cache"` +} + func (DeployProxy) VerifyPreconditions(env cldf.Environment, req *DeployProxyRequest) error { if err := verifyContractRef(env, req.ChainSel, CacheContract, req.CacheQualifier, req.Version); err != nil { return err @@ -140,41 +175,6 @@ func (DeployProxy) Apply(env cldf.Environment, req *DeployProxyRequest) (cldf.Ch return recordAddress(report.Output.ContractID, req.ChainSel, ProxyContract, req.Qualifier, req.Version, req.LabelSet) } -type deployOutput struct { - ContractID string `json:"contract_id"` -} - -type deployCacheInput struct { - WasmPath string `json:"wasm_path"` - Salt [32]byte `json:"salt"` - Owner string `json:"owner"` -} - -// deployCacheOp uploads the cache WASM and instantiates it via CreateContractV2 -// with __constructor(owner); the data-retention TTL is an on-chain constant, -// not a constructor input. -var deployCacheOp = operations.NewOperation( - "df-cache:deploy", opVersion, - "Deploys the DataFeedsCache Soroban contract", - func(b operations.Bundle, d StellarDeps, in deployCacheInput) (deployOutput, error) { - args := []xdr.ScVal{ - scval.AddressToScVal(in.Owner), - } - cid, err := d.Deploy.DeployContractWithArgs(b.GetContext(), in.WasmPath, in.Salt, args) - if err != nil { - return deployOutput{}, err - } - return deployOutput{ContractID: cid}, nil - }, -) - -type deployProxyInput struct { - WasmPath string `json:"wasm_path"` - Salt [32]byte `json:"salt"` - Owner string `json:"owner"` - Cache string `json:"cache"` -} - // deployProxyOp instantiates the proxy via __constructor(owner, cache). var deployProxyOp = operations.NewOperation( "df-proxy:deploy", opVersion, diff --git a/deployment/data-feeds/changeset/stellar/feed_admin.go b/deployment/data-feeds/changeset/stellar/feed_admin.go index a17cc95cd7f..95d7a571dfb 100644 --- a/deployment/data-feeds/changeset/stellar/feed_admin.go +++ b/deployment/data-feeds/changeset/stellar/feed_admin.go @@ -30,6 +30,11 @@ var ( _ cldf.ChangeSetV2[*FeedAdminRequest] = RemoveFeedAdmin{} ) +type feedAdminInput struct { + ContractID string `json:"contract_id"` + Admin string `json:"admin"` +} + // AddFeedAdmin grants feed-admin rights on the cache. type AddFeedAdmin struct{} @@ -50,6 +55,15 @@ func (AddFeedAdmin) Apply(env cldf.Environment, req *FeedAdminRequest) (cldf.Cha return out, err } +var addFeedAdminOp = operations.NewOperation( + "df-cache:add-feed-admin", opVersion, + "Grants feed-admin rights on the cache", + func(b operations.Bundle, d StellarDeps, in feedAdminInput) (void, error) { + c := cache.NewDataFeedsCacheClient(d.Invoker, in.ContractID) + return void{}, c.AddFeedAdmin(b.GetContext(), in.Admin) + }, +) + // RemoveFeedAdmin revokes feed-admin rights on the cache. type RemoveFeedAdmin struct{} @@ -70,20 +84,6 @@ func (RemoveFeedAdmin) Apply(env cldf.Environment, req *FeedAdminRequest) (cldf. return out, err } -type feedAdminInput struct { - ContractID string `json:"contract_id"` - Admin string `json:"admin"` -} - -var addFeedAdminOp = operations.NewOperation( - "df-cache:add-feed-admin", opVersion, - "Grants feed-admin rights on the cache", - func(b operations.Bundle, d StellarDeps, in feedAdminInput) (void, error) { - c := cache.NewDataFeedsCacheClient(d.Invoker, in.ContractID) - return void{}, c.AddFeedAdmin(b.GetContext(), in.Admin) - }, -) - var removeFeedAdminOp = operations.NewOperation( "df-cache:remove-feed-admin", opVersion, "Revokes feed-admin rights on the cache", diff --git a/deployment/data-feeds/changeset/stellar/ownership.go b/deployment/data-feeds/changeset/stellar/ownership.go index 4c6afe2be67..5f688de688c 100644 --- a/deployment/data-feeds/changeset/stellar/ownership.go +++ b/deployment/data-feeds/changeset/stellar/ownership.go @@ -40,6 +40,14 @@ var ( _ cldf.ChangeSetV2[*OwnershipRequest] = RenounceOwnership{} ) +// Ownership ops work for both contracts, selected by ContractID + IsProxy. +type ownershipInput struct { + ContractID string `json:"contract_id"` + IsProxy bool `json:"is_proxy"` + NewOwner string `json:"new_owner"` + LiveUntilLedger uint32 `json:"live_until_ledger"` +} + // TransferOwnership begins a two-step ownership transfer on the cache or proxy. type TransferOwnership struct{} @@ -71,6 +79,14 @@ func (TransferOwnership) Apply(env cldf.Environment, req *OwnershipRequest) (cld return out, err } +var transferOwnershipOp = operations.NewOperation( + "df:transfer-ownership", opVersion, + "Begins two-step ownership transfer", + func(b operations.Bundle, d StellarDeps, in ownershipInput) (void, error) { + return void{}, adminClient(d, in.ContractID, in.IsProxy).TransferOwnership(b.GetContext(), in.NewOwner, in.LiveUntilLedger) + }, +) + // AcceptOwnership accepts a pending ownership transfer on the cache or proxy // (the caller signing the underlying transaction must be the pending owner). type AcceptOwnership struct{} @@ -92,6 +108,14 @@ func (AcceptOwnership) Apply(env cldf.Environment, req *OwnershipRequest) (cldf. return out, err } +var acceptOwnershipOp = operations.NewOperation( + "df:accept-ownership", opVersion, + "Accepts a pending ownership transfer (caller must be the pending owner)", + func(b operations.Bundle, d StellarDeps, in ownershipInput) (void, error) { + return void{}, adminClient(d, in.ContractID, in.IsProxy).AcceptOwnership(b.GetContext()) + }, +) + // RenounceOwnership permanently renounces ownership of the cache or proxy. type RenounceOwnership struct{} @@ -112,30 +136,6 @@ func (RenounceOwnership) Apply(env cldf.Environment, req *OwnershipRequest) (cld return out, err } -// Ownership ops work for both contracts, selected by ContractID + IsProxy. -type ownershipInput struct { - ContractID string `json:"contract_id"` - IsProxy bool `json:"is_proxy"` - NewOwner string `json:"new_owner"` - LiveUntilLedger uint32 `json:"live_until_ledger"` -} - -var transferOwnershipOp = operations.NewOperation( - "df:transfer-ownership", opVersion, - "Begins two-step ownership transfer", - func(b operations.Bundle, d StellarDeps, in ownershipInput) (void, error) { - return void{}, adminClient(d, in.ContractID, in.IsProxy).TransferOwnership(b.GetContext(), in.NewOwner, in.LiveUntilLedger) - }, -) - -var acceptOwnershipOp = operations.NewOperation( - "df:accept-ownership", opVersion, - "Accepts a pending ownership transfer (caller must be the pending owner)", - func(b operations.Bundle, d StellarDeps, in ownershipInput) (void, error) { - return void{}, adminClient(d, in.ContractID, in.IsProxy).AcceptOwnership(b.GetContext()) - }, -) - var renounceOwnershipOp = operations.NewOperation( "df:renounce-ownership", opVersion, "Renounces ownership permanently", diff --git a/deployment/data-feeds/changeset/stellar/recover_tokens.go b/deployment/data-feeds/changeset/stellar/recover_tokens.go index 4c3f31365f8..637d1c2806d 100644 --- a/deployment/data-feeds/changeset/stellar/recover_tokens.go +++ b/deployment/data-feeds/changeset/stellar/recover_tokens.go @@ -26,6 +26,14 @@ var _ cldf.ChangeSetV2[*RecoverTokensRequest] = RecoverTokens{} // RecoverTokens recovers tokens accidentally sent to the cache or proxy. type RecoverTokens struct{} +type recoverTokensInput struct { + ContractID string `json:"contract_id"` + IsProxy bool `json:"is_proxy"` + Token string `json:"token"` + To string `json:"to"` + Amount int64 `json:"amount"` +} + func (RecoverTokens) VerifyPreconditions(env cldf.Environment, req *RecoverTokensRequest) error { if err := validateContract(req.Contract); err != nil { return err @@ -61,14 +69,6 @@ func (RecoverTokens) Apply(env cldf.Environment, req *RecoverTokensRequest) (cld return out, err } -type recoverTokensInput struct { - ContractID string `json:"contract_id"` - IsProxy bool `json:"is_proxy"` - Token string `json:"token"` - To string `json:"to"` - Amount int64 `json:"amount"` -} - var recoverTokensOp = operations.NewOperation( "df:recover-tokens", opVersion, "Recovers tokens accidentally sent to the contract", diff --git a/deployment/data-feeds/changeset/stellar/remove_feed_configs.go b/deployment/data-feeds/changeset/stellar/remove_feed_configs.go index 93553d90fa7..a725ebe32f0 100644 --- a/deployment/data-feeds/changeset/stellar/remove_feed_configs.go +++ b/deployment/data-feeds/changeset/stellar/remove_feed_configs.go @@ -23,6 +23,12 @@ var _ cldf.ChangeSetV2[*RemoveFeedConfigsRequest] = RemoveFeedConfigs{} // RemoveFeedConfigs removes feed configs from the cache. type RemoveFeedConfigs struct{} +type removeFeedConfigsInput struct { + ContractID string `json:"contract_id"` + Admin string `json:"admin"` + DataIDs [][16]byte `json:"data_ids"` +} + func (RemoveFeedConfigs) VerifyPreconditions(env cldf.Environment, req *RemoveFeedConfigsRequest) error { if err := verifyContractRef(env, req.ChainSel, CacheContract, req.Qualifier, req.Version); err != nil { return err @@ -58,12 +64,6 @@ func (RemoveFeedConfigs) Apply(env cldf.Environment, req *RemoveFeedConfigsReque return out, err } -type removeFeedConfigsInput struct { - ContractID string `json:"contract_id"` - Admin string `json:"admin"` - DataIDs [][16]byte `json:"data_ids"` -} - var removeFeedConfigsOp = operations.NewOperation( "df-cache:remove-feed-configs", opVersion, "Removes feed configs from the cache", diff --git a/deployment/data-feeds/changeset/stellar/set_feed_configs.go b/deployment/data-feeds/changeset/stellar/set_feed_configs.go index 2a05bdae296..dd95dd9902c 100644 --- a/deployment/data-feeds/changeset/stellar/set_feed_configs.go +++ b/deployment/data-feeds/changeset/stellar/set_feed_configs.go @@ -30,12 +30,42 @@ type SetFeedConfigsRequest struct { Permissions []FeedPermission } +// permissions converts the operator shape to the generated binding shape. +func (req *SetFeedConfigsRequest) permissions() ([]cache.WorkflowPermission, error) { + out := make([]cache.WorkflowPermission, 0, len(req.Permissions)) + for _, p := range req.Permissions { + if err := validateAddress(p.AllowedSender); err != nil { + return nil, fmt.Errorf("allowed sender: %w", err) + } + owner, err := workflowOwnerToBytes(p.AllowedWorkflowOwner) + if err != nil { + return nil, err + } + name, err := workflowNameToBytes(p.AllowedWorkflowName) + if err != nil { + return nil, err + } + out = append(out, cache.WorkflowPermission{ + AllowedSender: p.AllowedSender, + AllowedWorkflowOwner: owner, + AllowedWorkflowName: name, + }) + } + return out, nil +} + var _ cldf.ChangeSetV2[*SetFeedConfigsRequest] = SetFeedConfigs{} // SetFeedConfigs sets per-feed descriptions and workflow write-permissions on // the cache. type SetFeedConfigs struct{} +type setFeedConfigsInput struct { + ContractID string `json:"contract_id"` + Admin string `json:"admin"` + Entries []cache.FeedConfigEntry `json:"entries"` +} + func (SetFeedConfigs) VerifyPreconditions(env cldf.Environment, req *SetFeedConfigsRequest) error { if err := verifyContractRef(env, req.ChainSel, CacheContract, req.Qualifier, req.Version); err != nil { return err @@ -61,30 +91,6 @@ func (SetFeedConfigs) VerifyPreconditions(env cldf.Environment, req *SetFeedConf return nil } -// permissions converts the operator shape to the generated binding shape. -func (req *SetFeedConfigsRequest) permissions() ([]cache.WorkflowPermission, error) { - out := make([]cache.WorkflowPermission, 0, len(req.Permissions)) - for _, p := range req.Permissions { - if err := validateAddress(p.AllowedSender); err != nil { - return nil, fmt.Errorf("allowed sender: %w", err) - } - owner, err := workflowOwnerToBytes(p.AllowedWorkflowOwner) - if err != nil { - return nil, err - } - name, err := workflowNameToBytes(p.AllowedWorkflowName) - if err != nil { - return nil, err - } - out = append(out, cache.WorkflowPermission{ - AllowedSender: p.AllowedSender, - AllowedWorkflowOwner: owner, - AllowedWorkflowName: name, - }) - } - return out, nil -} - func (SetFeedConfigs) Apply(env cldf.Environment, req *SetFeedConfigsRequest) (cldf.ChangesetOutput, error) { var out cldf.ChangesetOutput d, err := resolveDeps(env, req.ChainSel, CacheContract, req.Qualifier, req.Version) @@ -120,12 +126,6 @@ func (SetFeedConfigs) Apply(env cldf.Environment, req *SetFeedConfigsRequest) (c return out, err } -type setFeedConfigsInput struct { - ContractID string `json:"contract_id"` - Admin string `json:"admin"` - Entries []cache.FeedConfigEntry `json:"entries"` -} - var setFeedConfigsOp = operations.NewOperation( "df-cache:set-feed-configs", opVersion, "Sets per-feed descriptions and workflow write-permissions on the cache", diff --git a/deployment/data-feeds/changeset/stellar/set_feed_frozen.go b/deployment/data-feeds/changeset/stellar/set_feed_frozen.go index 343898e6360..869a666aa00 100644 --- a/deployment/data-feeds/changeset/stellar/set_feed_frozen.go +++ b/deployment/data-feeds/changeset/stellar/set_feed_frozen.go @@ -26,6 +26,13 @@ var _ cldf.ChangeSetV2[*SetFeedFrozenRequest] = SetFeedFrozen{} // frozen (NoFeedState). type SetFeedFrozen struct{} +type setFeedFrozenInput struct { + ContractID string `json:"contract_id"` + Admin string `json:"admin"` + DataIDs [][16]byte `json:"data_ids"` + Frozen bool `json:"frozen"` +} + func (SetFeedFrozen) VerifyPreconditions(env cldf.Environment, req *SetFeedFrozenRequest) error { if err := verifyContractRef(env, req.ChainSel, CacheContract, req.Qualifier, req.Version); err != nil { return err @@ -62,13 +69,6 @@ func (SetFeedFrozen) Apply(env cldf.Environment, req *SetFeedFrozenRequest) (cld return out, err } -type setFeedFrozenInput struct { - ContractID string `json:"contract_id"` - Admin string `json:"admin"` - DataIDs [][16]byte `json:"data_ids"` - Frozen bool `json:"frozen"` -} - var setFeedFrozenOp = operations.NewOperation( "df-cache:set-feed-frozen", opVersion, "Freezes or unfreezes feeds on the cache", diff --git a/deployment/data-feeds/changeset/stellar/set_proxy_cache.go b/deployment/data-feeds/changeset/stellar/set_proxy_cache.go index 6afedccd57e..34b0395f182 100644 --- a/deployment/data-feeds/changeset/stellar/set_proxy_cache.go +++ b/deployment/data-feeds/changeset/stellar/set_proxy_cache.go @@ -27,6 +27,11 @@ var _ cldf.ChangeSetV2[*SetProxyCacheRequest] = SetProxyCache{} // SetProxyCache points the proxy at a cache contract. type SetProxyCache struct{} +type setProxyCacheInput struct { + ContractID string `json:"contract_id"` + Cache string `json:"cache"` +} + func (SetProxyCache) VerifyPreconditions(env cldf.Environment, req *SetProxyCacheRequest) error { if err := verifyContractRef(env, req.ChainSel, ProxyContract, req.Qualifier, req.Version); err != nil { return err @@ -57,11 +62,6 @@ func (SetProxyCache) Apply(env cldf.Environment, req *SetProxyCacheRequest) (cld return out, err } -type setProxyCacheInput struct { - ContractID string `json:"contract_id"` - Cache string `json:"cache"` -} - var setProxyCacheOp = operations.NewOperation( "df-proxy:set-cache", opVersion, "Points the proxy at a cache contract", diff --git a/deployment/data-feeds/changeset/stellar/upgrade.go b/deployment/data-feeds/changeset/stellar/upgrade.go index 87a5b08ca80..c3e6f565a4f 100644 --- a/deployment/data-feeds/changeset/stellar/upgrade.go +++ b/deployment/data-feeds/changeset/stellar/upgrade.go @@ -22,10 +22,24 @@ type UpgradeRequest struct { var _ cldf.ChangeSetV2[*UpgradeRequest] = Upgrade{} // Upgrade uploads a new WASM blob and points the contract at it. Apply chains -// two operations: UploadWASM produces the code hash that UpgradeContract then -// applies on-chain. +// two operations: uploadWASMOp produces the code hash that upgradeContractOp +// then applies on-chain. type Upgrade struct{} +type uploadWASMInput struct { + WasmPath string `json:"wasm_path"` +} + +type uploadWASMOutput struct { + WasmHash [32]byte `json:"wasm_hash"` +} + +type upgradeContractInput struct { + ContractID string `json:"contract_id"` + IsProxy bool `json:"is_proxy"` + NewWasmHash [32]byte `json:"new_wasm_hash"` +} + func (Upgrade) VerifyPreconditions(env cldf.Environment, req *UpgradeRequest) error { if err := validateContract(req.Contract); err != nil { return err @@ -61,13 +75,6 @@ func (Upgrade) Apply(env cldf.Environment, req *UpgradeRequest) (cldf.ChangesetO return out, err } -type uploadWASMInput struct { - WasmPath string `json:"wasm_path"` -} -type uploadWASMOutput struct { - WasmHash [32]byte `json:"wasm_hash"` -} - var uploadWASMOp = operations.NewOperation( "df:upload-wasm", opVersion, "Uploads a WASM blob and returns its code hash (for upgrades)", @@ -80,12 +87,6 @@ var uploadWASMOp = operations.NewOperation( }, ) -type upgradeContractInput struct { - ContractID string `json:"contract_id"` - IsProxy bool `json:"is_proxy"` - NewWasmHash [32]byte `json:"new_wasm_hash"` -} - var upgradeContractOp = operations.NewOperation( "df:upgrade", opVersion, "Points the contract at a new WASM implementation", From 6cecb2a2dc7e027586aa9de5cf54c658f6625799 Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Wed, 5 Aug 2026 13:20:16 +0100 Subject: [PATCH 13/19] Trim comments to the essentials Drop restated signatures, parenthetical asides, and narration of visible code; keep constraint comments like frozen-read errors, data-id alignment, and the test seam. --- .../data-feeds/changeset/stellar/config.go | 3 +- .../data-feeds/changeset/stellar/deploy.go | 18 ++++------ .../changeset/stellar/deploy_test.go | 6 ++-- .../data-feeds/changeset/stellar/deps.go | 29 ++++++---------- .../data-feeds/changeset/stellar/doc.go | 5 ++- .../data-feeds/changeset/stellar/e2e_test.go | 27 +++++---------- .../changeset/stellar/feed_admin.go | 3 +- .../data-feeds/changeset/stellar/ownership.go | 13 +++----- .../changeset/stellar/recover_tokens.go | 3 +- .../changeset/stellar/remove_feed_configs.go | 3 +- .../changeset/stellar/set_feed_configs.go | 14 ++++---- .../changeset/stellar/set_feed_frozen.go | 8 ++--- .../changeset/stellar/set_proxy_cache.go | 6 ++-- .../data-feeds/changeset/stellar/state.go | 3 +- .../changeset/stellar/testutil_test.go | 33 +++++-------------- .../data-feeds/changeset/stellar/upgrade.go | 7 ++-- .../changeset/stellar/validation.go | 8 ++--- 17 files changed, 65 insertions(+), 124 deletions(-) diff --git a/deployment/data-feeds/changeset/stellar/config.go b/deployment/data-feeds/changeset/stellar/config.go index a0d85714c7c..b2e391ce515 100644 --- a/deployment/data-feeds/changeset/stellar/config.go +++ b/deployment/data-feeds/changeset/stellar/config.go @@ -2,9 +2,8 @@ package stellar import "github.com/smartcontractkit/chainlink-deployments-framework/datastore" +// Datastore contract types for the two DF contracts. const ( - // CacheContract is the datastore contract type for the DataFeedsCache Soroban contract. CacheContract datastore.ContractType = "DataFeedsCache" - // ProxyContract is the datastore contract type for the DataFeedsProxy Soroban contract. ProxyContract datastore.ContractType = "DataFeedsProxy" ) diff --git a/deployment/data-feeds/changeset/stellar/deploy.go b/deployment/data-feeds/changeset/stellar/deploy.go index 0eb14b60d90..4290a969dde 100644 --- a/deployment/data-feeds/changeset/stellar/deploy.go +++ b/deployment/data-feeds/changeset/stellar/deploy.go @@ -27,8 +27,7 @@ type DeployCacheRequest struct { var _ cldf.ChangeSetV2[*DeployCacheRequest] = DeployCache{} -// DeployCache uploads and instantiates the DataFeedsCache contract and records -// its address in the datastore under CacheContract. +// DeployCache deploys the cache contract and records its address. type DeployCache struct{} type deployOutput struct { @@ -82,9 +81,8 @@ func (DeployCache) Apply(env cldf.Environment, req *DeployCacheRequest) (cldf.Ch return recordAddress(report.Output.ContractID, req.ChainSel, CacheContract, req.Qualifier, req.Version, req.LabelSet) } -// deployCacheOp uploads the cache WASM and instantiates it via CreateContractV2 -// with __constructor(owner); the data-retention TTL is an on-chain constant, -// not a constructor input. +// The cache constructor takes only the owner; the data-retention TTL is an +// on-chain constant. var deployCacheOp = operations.NewOperation( "df-cache:deploy", opVersion, "Deploys the DataFeedsCache Soroban contract", @@ -100,10 +98,8 @@ var deployCacheOp = operations.NewOperation( }, ) -// DeployProxyRequest configures a DataFeedsProxy deployment. The cache passed -// to __constructor(owner, cache) is resolved from the datastore by (ChainSel, -// CacheContract, Version, CacheQualifier) — it must already be recorded. -// Cross-contract lookups share the request's Version. +// DeployProxyRequest configures a DataFeedsProxy deployment. The cache is +// resolved from the datastore by CacheQualifier and must already be recorded. type DeployProxyRequest struct { ChainSel uint64 WasmPath string @@ -116,8 +112,7 @@ type DeployProxyRequest struct { var _ cldf.ChangeSetV2[*DeployProxyRequest] = DeployProxy{} -// DeployProxy uploads and instantiates the DataFeedsProxy contract and records -// its address in the datastore under ProxyContract. +// DeployProxy deploys the proxy contract and records its address. type DeployProxy struct{} type deployProxyInput struct { @@ -175,7 +170,6 @@ func (DeployProxy) Apply(env cldf.Environment, req *DeployProxyRequest) (cldf.Ch return recordAddress(report.Output.ContractID, req.ChainSel, ProxyContract, req.Qualifier, req.Version, req.LabelSet) } -// deployProxyOp instantiates the proxy via __constructor(owner, cache). var deployProxyOp = operations.NewOperation( "df-proxy:deploy", opVersion, "Deploys the DataFeedsProxy Soroban contract", diff --git a/deployment/data-feeds/changeset/stellar/deploy_test.go b/deployment/data-feeds/changeset/stellar/deploy_test.go index fe9d3bcdab3..637a523a386 100644 --- a/deployment/data-feeds/changeset/stellar/deploy_test.go +++ b/deployment/data-feeds/changeset/stellar/deploy_test.go @@ -57,10 +57,8 @@ func TestDeployCacheChangeset_MissingChain(t *testing.T) { require.Error(t, DeployCache{}.VerifyPreconditions(env, req)) } -// TestDeployCacheChangeset_ExplicitOwner pins that an explicit req.Owner -// seeds the deploy salt directly. deploy.go's DeployCache.Apply only falls back to -// ch.Signer.Address() when req.Owner is empty; when it's set, the salt is -// GenerateDeterministicSalt(req.Owner, ...) -- not the chain signer's address. +// An explicit Owner seeds the deploy salt; the chain signer is only the +// fallback when Owner is empty. func TestDeployCacheChangeset_ExplicitOwner(t *testing.T) { env, _, dep := newTestEnv(t) diff --git a/deployment/data-feeds/changeset/stellar/deps.go b/deployment/data-feeds/changeset/stellar/deps.go index 7719cade875..578ce0c11d0 100644 --- a/deployment/data-feeds/changeset/stellar/deps.go +++ b/deployment/data-feeds/changeset/stellar/deps.go @@ -17,16 +17,15 @@ import ( stellardeploy "github.com/smartcontractkit/chainlink-stellar/deployment" ) -// StellarDeps bundles deploy-time and runtime chain I/O for the operations. -// The same *deployment.Deployer satisfies both fields. +// StellarDeps bundles deploy and invoke chain I/O for the operations. The +// same chainlink-stellar Deployer satisfies both fields. type StellarDeps struct { Deploy SorobanContractDeployer Invoker bindings.Invoker } -// SorobanContractDeployer is the deploy-time surface the operations need from -// chainlink-stellar's concrete deployment.Deployer, defined locally so the -// upstream package does not have to export a widened interface. +// SorobanContractDeployer is the deploy surface needed from chainlink-stellar's +// Deployer, defined locally so upstream need not export it. type SorobanContractDeployer interface { DeployContractWithArgs(ctx context.Context, wasmPath string, salt [32]byte, ctorArgs []xdr.ScVal) (string, error) UploadContractWASM(ctx context.Context, wasmPath string) (xdr.Hash, error) @@ -47,7 +46,6 @@ type contractAdmin interface { RecoverTokens(ctx context.Context, token, to string, amount int64) error } -// adminClient returns the generated client selected by isProxy. func adminClient(d StellarDeps, contractID string, isProxy bool) contractAdmin { if isProxy { return proxy.NewDataFeedsProxyClient(d.Invoker, contractID) @@ -55,8 +53,7 @@ func adminClient(d StellarDeps, contractID string, isProxy bool) contractAdmin { return cache.NewDataFeedsCacheClient(d.Invoker, contractID) } -// newStellarDeps builds the deploy/invoke dependencies for a CLDF Stellar -// chain. Package-level var so unit tests can substitute fakes. +// newStellarDeps is a package-level var so unit tests can substitute fakes. var newStellarDeps = func(ch cldfstellar.Chain) (StellarDeps, error) { d, err := stellardeploy.NewDeployerFromChain(ch) if err != nil { @@ -65,8 +62,6 @@ var newStellarDeps = func(ch cldfstellar.Chain) (StellarDeps, error) { return StellarDeps{Deploy: d, Invoker: d}, nil } -// chainDeps resolves the CLDF Stellar chain for chainSel and builds its -// deploy/invoke deps. func chainDeps(env cldf.Environment, chainSel uint64) (cldfstellar.Chain, StellarDeps, error) { ch, ok := env.BlockChains.StellarChains()[chainSel] if !ok { @@ -87,8 +82,6 @@ func ownerOrSigner(ch cldfstellar.Chain, owner string) (string, error) { return ch.Signer.Address(), nil } -// recordAddress returns a ChangesetOutput whose datastore holds the deployed -// contract's AddressRef. func recordAddress(address string, chainSel uint64, contractType datastore.ContractType, qualifier, version string, labels datastore.LabelSet) (cldf.ChangesetOutput, error) { var out cldf.ChangesetOutput out.DataStore = datastore.NewMemoryDataStore() @@ -102,16 +95,14 @@ func recordAddress(address string, chainSel uint64, contractType datastore.Contr }) } -// stellarApplyDeps bundles a resolved contract's address with the chain deps -// needed to invoke operations against it. +// stellarApplyDeps pairs a resolved contract address with its chain deps. type stellarApplyDeps struct { deps StellarDeps contractID string } -// verifyContractRef checks that chainSel names a known Stellar chain, -// versionStr parses, and an AddressRef exists for (chainSel, contractType, -// version, qualifier). +// verifyContractRef checks the chain exists, the version parses, and an +// AddressRef exists for the contract. func verifyContractRef(env cldf.Environment, chainSel uint64, contractType datastore.ContractType, qualifier, versionStr string) error { if _, ok := env.BlockChains.StellarChains()[chainSel]; !ok { return fmt.Errorf("stellar chain not found for chain selector %d", chainSel) @@ -134,8 +125,8 @@ func resolveDeps(env cldf.Environment, chainSel uint64, contractType datastore.C return d, err } -// resolveContractDeps looks up the AddressRef for (chainSel, contractType, -// version, qualifier) and bundles it with the chain's deploy/invoke deps. +// resolveContractDeps resolves the contract's AddressRef and bundles it with +// the chain deps. func resolveContractDeps(env cldf.Environment, chainSel uint64, contractType datastore.ContractType, qualifier, version string) (stellarApplyDeps, datastore.AddressRef, error) { v, err := semver.NewVersion(version) if err != nil { diff --git a/deployment/data-feeds/changeset/stellar/doc.go b/deployment/data-feeds/changeset/stellar/doc.go index 88509c8608e..5367aaa09eb 100644 --- a/deployment/data-feeds/changeset/stellar/doc.go +++ b/deployment/data-feeds/changeset/stellar/doc.go @@ -1,4 +1,3 @@ -// Package stellar contains CLD changesets for the Stellar (Soroban) Data Feeds -// cache and proxy contracts. Style follows the Solana suite (ChangeSetV2 + -// operations); functional coverage follows the EVM suite. +// Package stellar contains CLD changesets for the Stellar Data Feeds cache +// and proxy contracts. package stellar diff --git a/deployment/data-feeds/changeset/stellar/e2e_test.go b/deployment/data-feeds/changeset/stellar/e2e_test.go index fa578ddf1df..672dd370c54 100644 --- a/deployment/data-feeds/changeset/stellar/e2e_test.go +++ b/deployment/data-feeds/changeset/stellar/e2e_test.go @@ -89,8 +89,7 @@ import ( // re-enters the contract => host trap Error(Context, // InvalidAction), asserted -// e2eOnce guards the CTF container lifecycle for this suite (the provider -// requires a *sync.Once; one test => one Once). +// e2eOnce guards the CTF container lifecycle; the provider requires a sync.Once. var e2eOnce sync.Once const ( @@ -163,8 +162,7 @@ func TestStellarDataFeedsE2E(t *testing.T) { ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, CacheQualifier: e2eQualifier, }) - // Precompute the binding-typed feed id / permission tuple used by the - // direct calls below (reuse the package's own hex->bytes converters). + // Binding-typed feed id and permission tuple for the direct calls below. ids, err := dataIDsToBytes([]string{e2eDataID}) require.NoError(t, err) did := [16]byte(ids[0]) @@ -184,8 +182,7 @@ func TestStellarDataFeedsE2E(t *testing.T) { require.NoError(t, cacheClient.OnReport(ctx, deployer, metadata, report), "on_report with a valid encoding from the configured AllowedSender must succeed") - // Prove the report was actually stored (this is what makes the proxy - // latest_round/get_round reads return data, i.e. outcome (a)). + // Prove the report was stored; the proxy read-through depends on it. stored, err := cacheClient.LatestRound(ctx, did) require.NoError(t, err) require.NotNil(t, stored, "on_report should have stored a round") @@ -296,14 +293,10 @@ func TestStellarDataFeedsE2E(t *testing.T) { Contract: ProxyContract, WasmPath: proxyWasm, }) - // recover_tokens invokes the SAC transfer(from=self, to, amount). We pass the - // contract's own address as the token, so the transfer sub-call re-enters the - // same contract; Soroban forbids contract re-entry and the host traps with - // Error(Context, InvalidAction) ("Contract re-entry is not allowed"). This is - // the documented (b) outcome: recover_tokens is really invoked and the trap - // is a contract/host-domain error, not a transport/XDR/auth-plumbing failure. - // Standing up a funded native-XLM SAC balance for a success path is - // disproportionate for this gate. + // recover_tokens is exercised with the contract's own address as the token: + // the SAC transfer sub-call re-enters the contract and Soroban traps with + // Error(Context, InvalidAction). The call is really invoked on-chain; a + // funded token balance for a success path is disproportionate here. proxyRecoverErr := applyErr(t, env, RecoverTokens{}, &RecoverTokensRequest{ ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, Contract: ProxyContract, Token: proxyClient.ContractID(), To: deployer, Amount: 1, @@ -331,10 +324,8 @@ func TestStellarDataFeedsE2E(t *testing.T) { Contract: CacheContract, WasmPath: cacheWasm, }) - // recover_tokens: same real-transfer semantics as the proxy. Token is the - // cache's own address, so the SAC transfer(from=self) re-enters the contract - // and the host traps with Error(Context, InvalidAction) — documented (b). Run - // while the deployer is still owner (ownership renounce happens last). + // recover_tokens: same re-entry trap as the proxy. Runs while the deployer + // is still owner; renounce happens last. cacheRecoverErr := applyErr(t, env, RecoverTokens{}, &RecoverTokensRequest{ ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, Contract: CacheContract, Token: cacheAddr, To: deployer, Amount: 1, diff --git a/deployment/data-feeds/changeset/stellar/feed_admin.go b/deployment/data-feeds/changeset/stellar/feed_admin.go index 95d7a571dfb..e14fef6dc98 100644 --- a/deployment/data-feeds/changeset/stellar/feed_admin.go +++ b/deployment/data-feeds/changeset/stellar/feed_admin.go @@ -6,8 +6,7 @@ import ( cache "github.com/smartcontractkit/chainlink-stellar/bindings/contracts/data_feeds_cache" ) -// FeedAdminRequest grants or revokes feed-admin rights on an already-deployed -// DataFeedsCache contract. +// FeedAdminRequest grants or revokes feed-admin rights on the cache. type FeedAdminRequest struct { ChainSel uint64 Qualifier string diff --git a/deployment/data-feeds/changeset/stellar/ownership.go b/deployment/data-feeds/changeset/stellar/ownership.go index 5f688de688c..adaa0e2a8ae 100644 --- a/deployment/data-feeds/changeset/stellar/ownership.go +++ b/deployment/data-feeds/changeset/stellar/ownership.go @@ -9,9 +9,7 @@ import ( "github.com/smartcontractkit/chainlink-deployments-framework/operations" ) -// OwnershipRequest drives the two-step ownership changesets for either DF -// contract (CacheContract or ProxyContract) — the generated cache and proxy -// clients expose identical ownership methods. +// OwnershipRequest drives the ownership changesets for the cache or proxy. type OwnershipRequest struct { ChainSel uint64 Qualifier string @@ -40,7 +38,6 @@ var ( _ cldf.ChangeSetV2[*OwnershipRequest] = RenounceOwnership{} ) -// Ownership ops work for both contracts, selected by ContractID + IsProxy. type ownershipInput struct { ContractID string `json:"contract_id"` IsProxy bool `json:"is_proxy"` @@ -48,7 +45,7 @@ type ownershipInput struct { LiveUntilLedger uint32 `json:"live_until_ledger"` } -// TransferOwnership begins a two-step ownership transfer on the cache or proxy. +// TransferOwnership begins a two-step ownership transfer. type TransferOwnership struct{} func (TransferOwnership) VerifyPreconditions(env cldf.Environment, req *OwnershipRequest) error { @@ -87,8 +84,8 @@ var transferOwnershipOp = operations.NewOperation( }, ) -// AcceptOwnership accepts a pending ownership transfer on the cache or proxy -// (the caller signing the underlying transaction must be the pending owner). +// AcceptOwnership accepts a pending ownership transfer. The transaction +// signer must be the pending owner. type AcceptOwnership struct{} func (AcceptOwnership) VerifyPreconditions(env cldf.Environment, req *OwnershipRequest) error { @@ -116,7 +113,7 @@ var acceptOwnershipOp = operations.NewOperation( }, ) -// RenounceOwnership permanently renounces ownership of the cache or proxy. +// RenounceOwnership permanently renounces ownership. type RenounceOwnership struct{} func (RenounceOwnership) VerifyPreconditions(env cldf.Environment, req *OwnershipRequest) error { diff --git a/deployment/data-feeds/changeset/stellar/recover_tokens.go b/deployment/data-feeds/changeset/stellar/recover_tokens.go index 637d1c2806d..b841c6a36c3 100644 --- a/deployment/data-feeds/changeset/stellar/recover_tokens.go +++ b/deployment/data-feeds/changeset/stellar/recover_tokens.go @@ -9,8 +9,7 @@ import ( "github.com/smartcontractkit/chainlink-deployments-framework/operations" ) -// RecoverTokensRequest recovers tokens accidentally sent to an already-deployed -// cache or proxy contract. +// RecoverTokensRequest recovers tokens accidentally sent to the cache or proxy. type RecoverTokensRequest struct { ChainSel uint64 Qualifier string diff --git a/deployment/data-feeds/changeset/stellar/remove_feed_configs.go b/deployment/data-feeds/changeset/stellar/remove_feed_configs.go index a725ebe32f0..146b434e16f 100644 --- a/deployment/data-feeds/changeset/stellar/remove_feed_configs.go +++ b/deployment/data-feeds/changeset/stellar/remove_feed_configs.go @@ -8,8 +8,7 @@ import ( cache "github.com/smartcontractkit/chainlink-stellar/bindings/contracts/data_feeds_cache" ) -// RemoveFeedConfigsRequest removes a batch of feed configs from an -// already-deployed DataFeedsCache contract. +// RemoveFeedConfigsRequest removes a batch of feed configs from the cache. type RemoveFeedConfigsRequest struct { ChainSel uint64 Qualifier string diff --git a/deployment/data-feeds/changeset/stellar/set_feed_configs.go b/deployment/data-feeds/changeset/stellar/set_feed_configs.go index dd95dd9902c..369217f95f8 100644 --- a/deployment/data-feeds/changeset/stellar/set_feed_configs.go +++ b/deployment/data-feeds/changeset/stellar/set_feed_configs.go @@ -9,17 +9,16 @@ import ( cache "github.com/smartcontractkit/chainlink-stellar/bindings/contracts/data_feeds_cache" ) -// FeedPermission is the operator-facing shape of a workflow write-permission. -// AllowedSender is the on-chain caller permitted to invoke on_report — the CRE -// forwarder contract. +// FeedPermission is a workflow write-permission. AllowedSender is the caller +// allowed to invoke on_report, i.e. the CRE forwarder contract. type FeedPermission struct { - AllowedSender string // Stellar address (C... or G...) + AllowedSender string // C... or G... address AllowedWorkflowOwner string // 20-byte hex - AllowedWorkflowName string // <= 10 ASCII chars + AllowedWorkflowName string // up to 10 ASCII chars } -// SetFeedConfigsRequest sets descriptions + workflow permissions for a batch of -// feeds. Permissions apply to every feed in the batch. +// SetFeedConfigsRequest configures a batch of feeds. Permissions apply to +// every feed in the batch. type SetFeedConfigsRequest struct { ChainSel uint64 Qualifier string @@ -30,7 +29,6 @@ type SetFeedConfigsRequest struct { Permissions []FeedPermission } -// permissions converts the operator shape to the generated binding shape. func (req *SetFeedConfigsRequest) permissions() ([]cache.WorkflowPermission, error) { out := make([]cache.WorkflowPermission, 0, len(req.Permissions)) for _, p := range req.Permissions { diff --git a/deployment/data-feeds/changeset/stellar/set_feed_frozen.go b/deployment/data-feeds/changeset/stellar/set_feed_frozen.go index 869a666aa00..a0ddcbe36df 100644 --- a/deployment/data-feeds/changeset/stellar/set_feed_frozen.go +++ b/deployment/data-feeds/changeset/stellar/set_feed_frozen.go @@ -8,8 +8,7 @@ import ( cache "github.com/smartcontractkit/chainlink-stellar/bindings/contracts/data_feeds_cache" ) -// SetFeedFrozenRequest freezes or unfreezes a batch of feeds on an -// already-deployed DataFeedsCache contract. +// SetFeedFrozenRequest freezes or unfreezes a batch of feeds. type SetFeedFrozenRequest struct { ChainSel uint64 Qualifier string @@ -21,9 +20,8 @@ type SetFeedFrozenRequest struct { var _ cldf.ChangeSetV2[*SetFeedFrozenRequest] = SetFeedFrozen{} -// SetFeedFrozen freezes or unfreezes feeds on the cache. Reads on a frozen -// feed are rejected with FeedFrozen; a feed with no recorded round cannot be -// frozen (NoFeedState). +// SetFeedFrozen freezes or unfreezes feeds. Reads on a frozen feed fail with +// FeedFrozen; freezing a feed with no recorded round fails with NoFeedState. type SetFeedFrozen struct{} type setFeedFrozenInput struct { diff --git a/deployment/data-feeds/changeset/stellar/set_proxy_cache.go b/deployment/data-feeds/changeset/stellar/set_proxy_cache.go index 34b0395f182..b7b8b123b6e 100644 --- a/deployment/data-feeds/changeset/stellar/set_proxy_cache.go +++ b/deployment/data-feeds/changeset/stellar/set_proxy_cache.go @@ -11,10 +11,8 @@ import ( proxy "github.com/smartcontractkit/chainlink-stellar/bindings/contracts/data_feeds_proxy" ) -// SetProxyCacheRequest points an already-deployed DataFeedsProxy contract at a -// (possibly different) cache contract. Qualifier resolves the proxy being -// acted on; CacheQualifier resolves the new cache target. Cross-contract -// lookups share the request's Version (see README). +// SetProxyCacheRequest points the proxy at a new cache. Qualifier resolves +// the proxy; CacheQualifier resolves the cache. type SetProxyCacheRequest struct { ChainSel uint64 Qualifier string diff --git a/deployment/data-feeds/changeset/stellar/state.go b/deployment/data-feeds/changeset/stellar/state.go index d9c5bc20036..5dcd53b0570 100644 --- a/deployment/data-feeds/changeset/stellar/state.go +++ b/deployment/data-feeds/changeset/stellar/state.go @@ -7,8 +7,7 @@ import ( proxy "github.com/smartcontractkit/chainlink-stellar/bindings/contracts/data_feeds_proxy" ) -// LoadCacheClient returns a generated cache client bound to the resolved -// CacheContract AddressRef for (chainSel, qualifier, version). +// LoadCacheClient returns a cache client bound to the resolved AddressRef. func LoadCacheClient(env cldf.Environment, chainSel uint64, qualifier, version string) (*cache.DataFeedsCacheClient, datastore.AddressRef, error) { d, ref, err := resolveContractDeps(env, chainSel, CacheContract, qualifier, version) if err != nil { diff --git a/deployment/data-feeds/changeset/stellar/testutil_test.go b/deployment/data-feeds/changeset/stellar/testutil_test.go index 467ae93fcd7..d264d5b1e5f 100644 --- a/deployment/data-feeds/changeset/stellar/testutil_test.go +++ b/deployment/data-feeds/changeset/stellar/testutil_test.go @@ -24,19 +24,15 @@ import ( const ( testContractID = "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA" testAdmin = "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN7" - // testProxyAddress is a second, distinct contract address used wherever a - // test needs to tell the proxy and cache refs apart (e.g. asserting Apply - // invoked the proxy, not the cache). + // testProxyAddress is distinct so tests can tell proxy and cache refs apart. testProxyAddress = "CBPROXY00000000000000000000000000000000000000000000000AA" ) -// testChainSel is a real Stellar chain selector (from chain-selectors) so -// that ChainMetadata methods (Family, NetworkType, ...) resolve correctly. +// testChainSel is a real chain selector so ChainMetadata methods resolve. var testChainSel = chainsel.STELLAR_TESTNET.Selector -// newTestEnv returns an Environment with one fake Stellar chain and swapped-in fake deps. -// It swaps the package-level newStellarDeps seam to hand back fakeInvoker/fakeDeployer -// instead of talking to a real Soroban RPC endpoint, and restores the seam on cleanup. +// newTestEnv returns an Environment with one fake Stellar chain. It swaps the +// newStellarDeps seam to fakes and restores it on cleanup. func newTestEnv(t *testing.T) (cldf.Environment, *fakeInvoker, *fakeDeployer) { t.Helper() @@ -76,7 +72,6 @@ func newTestEnv(t *testing.T) (cldf.Environment, *fakeInvoker, *fakeDeployer) { return *env, invoker, deployer } -// contractRefSpec describes one AddressRef to seed via seedContractRefs. type contractRefSpec struct { contractType datastore.ContractType address string @@ -84,11 +79,8 @@ type contractRefSpec struct { version string } -// seedContractRefs pre-seeds env.DataStore with the given AddressRefs (all in -// one MemoryDataStore) so changesets can resolve contracts by -// (ChainSel, Type, Version, Qualifier). Replaces any refs seeded by a prior -// call on the same env — call once per test with every ref the test needs -// (e.g. both a proxy and a cache ref for SetProxyCache). +// seedContractRefs replaces env.DataStore with one holding the given refs. +// Call once per test with every ref the test needs. func seedContractRefs(t *testing.T, env *cldf.Environment, refs ...contractRefSpec) { t.Helper() ds := datastore.NewMemoryDataStore() @@ -104,25 +96,18 @@ func seedContractRefs(t *testing.T, env *cldf.Environment, refs ...contractRefSp env.DataStore = ds.Seal() } -// seedCacheRef pre-seeds env.DataStore with a single cache AddressRef so -// config changesets (SetFeedConfigs, RemoveFeedConfigs, Add/RemoveFeedAdmin) -// can resolve the cache contract by (ChainSel, CacheContract, Version, Qualifier). func seedCacheRef(t *testing.T, env *cldf.Environment, address, qualifier, version string) { t.Helper() seedContractRefs(t, env, contractRefSpec{CacheContract, address, qualifier, version}) } -// seedProxyRef pre-seeds env.DataStore with a single proxy AddressRef so -// changesets can resolve the proxy contract by -// (ChainSel, ProxyContract, Version, Qualifier). func seedProxyRef(t *testing.T, env *cldf.Environment, address, qualifier, version string) { t.Helper() seedContractRefs(t, env, contractRefSpec{ProxyContract, address, qualifier, version}) } -// writeDummyWasm writes a placeholder wasm file under t.TempDir() so -// VerifyPreconditions' filesystem check (os.Stat) has a real path to find. -// The contents are never read — deployment is faked via fakeDeployer. +// writeDummyWasm writes a placeholder wasm file for VerifyPreconditions' path +// check; the contents are never read. func writeDummyWasm(t *testing.T, name string) string { t.Helper() path := filepath.Join(t.TempDir(), name) @@ -166,7 +151,7 @@ type fakeDeployer struct { deploys []deployCall contractID string wasmHash xdr.Hash - uploads []string // wasm paths passed to UploadContractWASM, in call order + uploads []string // wasm paths, in call order } func (f *fakeDeployer) DeployContractWithArgs(_ context.Context, wasmPath string, salt [32]byte, args []xdr.ScVal) (string, error) { diff --git a/deployment/data-feeds/changeset/stellar/upgrade.go b/deployment/data-feeds/changeset/stellar/upgrade.go index c3e6f565a4f..fb26a07f488 100644 --- a/deployment/data-feeds/changeset/stellar/upgrade.go +++ b/deployment/data-feeds/changeset/stellar/upgrade.go @@ -9,8 +9,7 @@ import ( "github.com/smartcontractkit/chainlink-deployments-framework/operations" ) -// UpgradeRequest points an already-deployed cache or proxy contract at a new -// WASM implementation. +// UpgradeRequest points the cache or proxy at a new WASM implementation. type UpgradeRequest struct { ChainSel uint64 Qualifier string @@ -21,9 +20,7 @@ type UpgradeRequest struct { var _ cldf.ChangeSetV2[*UpgradeRequest] = Upgrade{} -// Upgrade uploads a new WASM blob and points the contract at it. Apply chains -// two operations: uploadWASMOp produces the code hash that upgradeContractOp -// then applies on-chain. +// Upgrade uploads a new WASM blob and points the contract at it. type Upgrade struct{} type uploadWASMInput struct { diff --git a/deployment/data-feeds/changeset/stellar/validation.go b/deployment/data-feeds/changeset/stellar/validation.go index aa155a78721..e64036e29fb 100644 --- a/deployment/data-feeds/changeset/stellar/validation.go +++ b/deployment/data-feeds/changeset/stellar/validation.go @@ -18,7 +18,7 @@ func validateContract(t datastore.ContractType) error { return nil } -// validateAddress accepts a Stellar account (G...) or contract (C...) strkey. +// validateAddress accepts a G... account or C... contract strkey. func validateAddress(s string) error { if strkey.IsValidEd25519PublicKey(s) { return nil @@ -29,8 +29,8 @@ func validateAddress(s string) error { return fmt.Errorf("%q is not a valid Stellar account or contract address", s) } -// dataIDsToBytes converts 0x-prefixed hex feed IDs to [16]byte, left-justified -// with trailing zero padding (data IDs are canonically left-aligned). +// dataIDsToBytes converts hex feed IDs to [16]byte. Data IDs are canonically +// left-aligned, so short values are left-justified with trailing zero padding. func dataIDsToBytes(ids []string) ([][16]byte, error) { out := make([][16]byte, 0, len(ids)) for _, id := range ids { @@ -58,7 +58,7 @@ func workflowNameToBytes(s string) ([10]byte, error) { return out, nil } -// workflowOwnerToBytes decodes a 20-byte 0x-prefixed hex workflow owner. +// workflowOwnerToBytes decodes a 20-byte hex workflow owner. func workflowOwnerToBytes(hexStr string) ([20]byte, error) { var out [20]byte b, err := hex.DecodeString(strings.TrimPrefix(hexStr, "0x")) From f4a25dcef763149d892c3963c4c7a66b93759262 Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Wed, 5 Aug 2026 13:38:04 +0100 Subject: [PATCH 14/19] Add registry smoke test for all thirteen changesets Registers every changeset into a CLD ChangesetsRegistry and lists the keys, the same path cld durable-pipeline list uses. Doubles as the registration template for the chainlink-deployments wiring. --- .../changeset/stellar/registry_test.go | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 deployment/data-feeds/changeset/stellar/registry_test.go diff --git a/deployment/data-feeds/changeset/stellar/registry_test.go b/deployment/data-feeds/changeset/stellar/registry_test.go new file mode 100644 index 00000000000..fe849ba64c5 --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/registry_test.go @@ -0,0 +1,35 @@ +package stellar + +import ( + "testing" + + "github.com/stretchr/testify/require" + + cldregistry "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/changeset" +) + +// Registers every changeset the way a chainlink-deployments +// domain will and lists the keys, mirroring cld durable-pipeline list. +func TestChangesetRegistryKeys(t *testing.T) { + r := cldregistry.NewChangesetsRegistry() + + r.Add("0001_deploy_cache", cldregistry.Configure(DeployCache{}).WithEnvInput()) + r.Add("0002_deploy_proxy", cldregistry.Configure(DeployProxy{}).WithEnvInput()) + r.Add("0003_set_feed_configs", cldregistry.Configure(SetFeedConfigs{}).WithEnvInput()) + r.Add("0004_remove_feed_configs", cldregistry.Configure(RemoveFeedConfigs{}).WithEnvInput()) + r.Add("0005_set_feed_frozen", cldregistry.Configure(SetFeedFrozen{}).WithEnvInput()) + r.Add("0006_add_feed_admin", cldregistry.Configure(AddFeedAdmin{}).WithEnvInput()) + r.Add("0007_remove_feed_admin", cldregistry.Configure(RemoveFeedAdmin{}).WithEnvInput()) + r.Add("0008_transfer_ownership", cldregistry.Configure(TransferOwnership{}).WithEnvInput()) + r.Add("0009_accept_ownership", cldregistry.Configure(AcceptOwnership{}).WithEnvInput()) + r.Add("0010_renounce_ownership", cldregistry.Configure(RenounceOwnership{}).WithEnvInput()) + r.Add("0011_upgrade", cldregistry.Configure(Upgrade{}).WithEnvInput()) + r.Add("0012_recover_tokens", cldregistry.Configure(RecoverTokens{}).WithEnvInput()) + r.Add("0013_set_proxy_cache", cldregistry.Configure(SetProxyCache{}).WithEnvInput()) + + keys := r.ListKeys() + require.Len(t, keys, 13) + for _, k := range keys { + t.Logf("registered: %s", k) + } +} From 78f2fe002251ae52690286bb049df436b76a14bd Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Wed, 5 Aug 2026 14:15:34 +0100 Subject: [PATCH 15/19] Mirror feed configs into datastore contract metadata - SetFeedConfigs and RemoveFeedConfigs maintain a per-cache Feeds map of description, decimals, and workflow permissions, keyed by data id - DeployProxy and SetProxyCache record the proxy's current cache target - On-chain state stays authoritative; the mirror makes configuration queryable off-chain --- .../data-feeds/changeset/stellar/deploy.go | 10 ++- .../data-feeds/changeset/stellar/metadata.go | 63 +++++++++++++++++++ .../changeset/stellar/remove_feed_configs.go | 9 ++- .../changeset/stellar/set_feed_configs.go | 17 ++++- .../changeset/stellar/set_proxy_cache.go | 7 ++- 5 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 deployment/data-feeds/changeset/stellar/metadata.go diff --git a/deployment/data-feeds/changeset/stellar/deploy.go b/deployment/data-feeds/changeset/stellar/deploy.go index 4290a969dde..9bf3d74a69e 100644 --- a/deployment/data-feeds/changeset/stellar/deploy.go +++ b/deployment/data-feeds/changeset/stellar/deploy.go @@ -167,7 +167,15 @@ func (DeployProxy) Apply(env cldf.Environment, req *DeployProxyRequest) (cldf.Ch if err != nil { return out, err } - return recordAddress(report.Output.ContractID, req.ChainSel, ProxyContract, req.Qualifier, req.Version, req.LabelSet) + out, err = recordAddress(report.Output.ContractID, req.ChainSel, ProxyContract, req.Qualifier, req.Version, req.LabelSet) + if err != nil { + return out, err + } + return out, out.DataStore.ContractMetadata().Upsert(datastore.ContractMetadata{ + ChainSelector: req.ChainSel, + Address: report.Output.ContractID, + Metadata: ContractMetadata{Cache: cacheRef.Address}, + }) } var deployProxyOp = operations.NewOperation( diff --git a/deployment/data-feeds/changeset/stellar/metadata.go b/deployment/data-feeds/changeset/stellar/metadata.go new file mode 100644 index 00000000000..a62e6a4111d --- /dev/null +++ b/deployment/data-feeds/changeset/stellar/metadata.go @@ -0,0 +1,63 @@ +package stellar + +import ( + "encoding/hex" + + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" +) + +// ContractMetadata mirrors a cache contract's feed configuration so it is +// queryable off-chain. On-chain state stays authoritative; only the feed +// config changesets mutate this. +type ContractMetadata struct { + Cache string `json:"cache,omitempty"` // proxy only: current cache target + Feeds map[string]FeedMetadata `json:"feeds,omitempty"` // cache only, keyed by 0x data id +} + +// FeedMetadata mirrors one configured feed. +type FeedMetadata struct { + Description string `json:"description"` + Decimals uint32 `json:"decimals"` + Permissions []FeedPermission `json:"permissions"` +} + +// dataIDHex canonicalizes a feed id to full-width 0x hex. +func dataIDHex(id [16]byte) string { + return "0x" + hex.EncodeToString(id[:]) +} + +// decimalsFromID mirrors the contract: byte 7 in [0x20,0x60] encodes decimals. +func decimalsFromID(id [16]byte) uint32 { + if b := id[7]; b >= 0x20 && b <= 0x60 { + return uint32(b - 0x20) + } + return 0 +} + +// metadataOutput applies mutate to the contract's latest stored metadata and +// returns a ChangesetOutput carrying the updated record. +func metadataOutput(env cldf.Environment, chainSel uint64, address string, mutate func(*ContractMetadata)) (cldf.ChangesetOutput, error) { + meta := loadMetadata(env, chainSel, address) + mutate(&meta) + var out cldf.ChangesetOutput + out.DataStore = datastore.NewMemoryDataStore() + return out, out.DataStore.ContractMetadata().Upsert(datastore.ContractMetadata{ + ChainSelector: chainSel, + Address: address, + Metadata: meta, + }) +} + +// loadMetadata returns the contract's stored metadata, or a zero value. +func loadMetadata(env cldf.Environment, chainSel uint64, address string) ContractMetadata { + rec, err := env.DataStore.ContractMetadata().Get(datastore.NewContractMetadataKey(chainSel, address)) + if err != nil { + return ContractMetadata{} + } + meta, err := datastore.As[ContractMetadata](rec.Metadata) + if err != nil { + return ContractMetadata{} + } + return meta +} diff --git a/deployment/data-feeds/changeset/stellar/remove_feed_configs.go b/deployment/data-feeds/changeset/stellar/remove_feed_configs.go index 146b434e16f..da8aede2539 100644 --- a/deployment/data-feeds/changeset/stellar/remove_feed_configs.go +++ b/deployment/data-feeds/changeset/stellar/remove_feed_configs.go @@ -60,7 +60,14 @@ func (RemoveFeedConfigs) Apply(env cldf.Environment, req *RemoveFeedConfigsReque Admin: req.Admin, DataIDs: ids, }) - return out, err + if err != nil { + return out, err + } + return metadataOutput(env, req.ChainSel, d.contractID, func(m *ContractMetadata) { + for _, id := range ids { + delete(m.Feeds, dataIDHex(id)) + } + }) } var removeFeedConfigsOp = operations.NewOperation( diff --git a/deployment/data-feeds/changeset/stellar/set_feed_configs.go b/deployment/data-feeds/changeset/stellar/set_feed_configs.go index 369217f95f8..827cd124eb8 100644 --- a/deployment/data-feeds/changeset/stellar/set_feed_configs.go +++ b/deployment/data-feeds/changeset/stellar/set_feed_configs.go @@ -121,7 +121,22 @@ func (SetFeedConfigs) Apply(env cldf.Environment, req *SetFeedConfigsRequest) (c Admin: req.Admin, Entries: entries, }) - return out, err + if err != nil { + return out, err + } + return metadataOutput(env, req.ChainSel, d.contractID, func(m *ContractMetadata) { + if m.Feeds == nil { + m.Feeds = map[string]FeedMetadata{} + } + for i, id := range ids { + key := dataIDHex(id) + m.Feeds[key] = FeedMetadata{ + Description: req.Descriptions[i], + Decimals: decimalsFromID(id), + Permissions: req.Permissions, + } + } + }) } var setFeedConfigsOp = operations.NewOperation( diff --git a/deployment/data-feeds/changeset/stellar/set_proxy_cache.go b/deployment/data-feeds/changeset/stellar/set_proxy_cache.go index b7b8b123b6e..8c1dd7cc794 100644 --- a/deployment/data-feeds/changeset/stellar/set_proxy_cache.go +++ b/deployment/data-feeds/changeset/stellar/set_proxy_cache.go @@ -57,7 +57,12 @@ func (SetProxyCache) Apply(env cldf.Environment, req *SetProxyCacheRequest) (cld ContractID: proxyDeps.contractID, Cache: cacheRef.Address, }) - return out, err + if err != nil { + return out, err + } + return metadataOutput(env, req.ChainSel, proxyDeps.contractID, func(m *ContractMetadata) { + m.Cache = cacheRef.Address + }) } var setProxyCacheOp = operations.NewOperation( From 019bd7b9ca5e37d7f2c403a4e6be28935b4b925f Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Wed, 5 Aug 2026 15:55:09 +0100 Subject: [PATCH 16/19] Strengthen changeset tests and fold duplicates - Assert the ContractMetadata mirror in the set/remove feed config, set proxy cache, and deploy proxy tests, including the read-modify- write merge with pre-existing metadata and id-derived decimals - Run a two-feed batch through SetFeedConfigs Apply - Add bad-version and missing-wasm precondition rejections to deploys - Inline standalone rejection tests, reuse seedCacheRef, and fold the per-error state_test funcs into the happy-path tests --- .../changeset/stellar/deploy_test.go | 53 ++++++++----------- .../stellar/remove_feed_configs_test.go | 15 +++++- .../stellar/set_feed_configs_test.go | 29 ++++++---- .../changeset/stellar/set_proxy_cache_test.go | 6 ++- .../changeset/stellar/state_test.go | 32 +++-------- .../changeset/stellar/testutil_test.go | 23 ++++++++ 6 files changed, 90 insertions(+), 68 deletions(-) diff --git a/deployment/data-feeds/changeset/stellar/deploy_test.go b/deployment/data-feeds/changeset/stellar/deploy_test.go index 637a523a386..d4b13355a75 100644 --- a/deployment/data-feeds/changeset/stellar/deploy_test.go +++ b/deployment/data-feeds/changeset/stellar/deploy_test.go @@ -42,19 +42,21 @@ func TestDeployCacheChangeset(t *testing.T) { badOwner := *req badOwner.Owner = "not-a-key" require.Error(t, DeployCache{}.VerifyPreconditions(env, &badOwner)) -} -func TestDeployCacheChangeset_MissingChain(t *testing.T) { - env, _, _ := newTestEnv(t) + // unknown chain must fail preconditions + badChain := *req + badChain.ChainSel = 999999 + require.Error(t, DeployCache{}.VerifyPreconditions(env, &badChain)) - req := &DeployCacheRequest{ - ChainSel: 999999, - WasmPath: writeDummyWasm(t, "data_feeds_cache.wasm"), - Qualifier: "test-cache", - Version: "1.0.0", - } + // invalid version must fail preconditions + badVersion := *req + badVersion.Version = "not-semver" + require.Error(t, DeployCache{}.VerifyPreconditions(env, &badVersion)) - require.Error(t, DeployCache{}.VerifyPreconditions(env, req)) + // missing wasm file must fail preconditions + badWasm := *req + badWasm.WasmPath = "/does/not/exist.wasm" + require.Error(t, DeployCache{}.VerifyPreconditions(env, &badWasm)) } // An explicit Owner seeds the deploy salt; the chain signer is only the @@ -86,16 +88,8 @@ func TestDeployCacheChangeset_ExplicitOwner(t *testing.T) { func TestDeployProxyChangeset(t *testing.T) { env, _, dep := newTestEnv(t) - // Seed the datastore with a cache AddressRef so DeployProxy can resolve it. - seedDS := datastore.NewMemoryDataStore() - require.NoError(t, seedDS.Addresses().Add(datastore.AddressRef{ - Address: "CCACHEFAKE0000000000000000000000000000000000000000000000", - ChainSelector: testChainSel, - Type: CacheContract, - Version: semver.MustParse("1.0.0"), - Qualifier: "test-cache", - })) - env.DataStore = seedDS.Seal() + cacheAddr := "CCACHEFAKE0000000000000000000000000000000000000000000000" + seedCacheRef(t, &env, cacheAddr, "test-cache", "1.0.0") req := &DeployProxyRequest{ ChainSel: testChainSel, @@ -122,18 +116,13 @@ func TestDeployProxyChangeset(t *testing.T) { ref, err := out.DataStore.Addresses().Get(key) require.NoError(t, err) require.Equal(t, testContractID, ref.Address) -} -func TestDeployProxyChangeset_MissingCache(t *testing.T) { - env, _, _ := newTestEnv(t) - - req := &DeployProxyRequest{ - ChainSel: testChainSel, - WasmPath: writeDummyWasm(t, "data_feeds_proxy.wasm"), - CacheQualifier: "does-not-exist", - Qualifier: "test-proxy", - Version: "1.0.0", - } + // the metadata mirror records which cache the proxy points at + meta := outputMetadata(t, out, testContractID) + require.Equal(t, cacheAddr, meta.Cache) - require.Error(t, DeployProxy{}.VerifyPreconditions(env, req)) + // missing cache ref must fail preconditions + missing := *req + missing.CacheQualifier = "does-not-exist" + require.Error(t, DeployProxy{}.VerifyPreconditions(env, &missing)) } diff --git a/deployment/data-feeds/changeset/stellar/remove_feed_configs_test.go b/deployment/data-feeds/changeset/stellar/remove_feed_configs_test.go index 245e2deefe6..5ddadd02388 100644 --- a/deployment/data-feeds/changeset/stellar/remove_feed_configs_test.go +++ b/deployment/data-feeds/changeset/stellar/remove_feed_configs_test.go @@ -9,22 +9,33 @@ import ( func TestRemoveFeedConfigsChangeset(t *testing.T) { env, inv, _ := newTestEnv(t) seedCacheRef(t, &env, testContractID, "test", "1.0.0") + seedContractMetadata(t, &env, testContractID, ContractMetadata{ + Feeds: map[string]FeedMetadata{ + "0x018e16c39e0003320000000000000000": {Description: "BTC/USD"}, + "0x01c50f0e2106d5fd0000000000000000": {Description: "SOL/USD"}, + }, + }) req := &RemoveFeedConfigsRequest{ ChainSel: testChainSel, Qualifier: "test", Version: "1.0.0", Admin: testAdmin, - DataIDs: []string{"0x018e16c39e00032000000"}, + DataIDs: []string{"0x018e16c39e0003320000000000000000"}, } require.NoError(t, RemoveFeedConfigs{}.VerifyPreconditions(env, req)) - _, err := RemoveFeedConfigs{}.Apply(env, req) + out, err := RemoveFeedConfigs{}.Apply(env, req) require.NoError(t, err) require.Len(t, inv.calls, 1) require.Equal(t, "remove_feed_configs", inv.calls[0].Function) require.Equal(t, testContractID, inv.calls[0].ContractID) + // only the removed feed leaves the metadata mirror + meta := outputMetadata(t, out, testContractID) + require.NotContains(t, meta.Feeds, "0x018e16c39e0003320000000000000000") + require.Equal(t, "SOL/USD", meta.Feeds["0x01c50f0e2106d5fd0000000000000000"].Description) + // empty DataIDs must fail preconditions empty := *req empty.DataIDs = nil diff --git a/deployment/data-feeds/changeset/stellar/set_feed_configs_test.go b/deployment/data-feeds/changeset/stellar/set_feed_configs_test.go index fa06b3e74d1..6b8ed63623c 100644 --- a/deployment/data-feeds/changeset/stellar/set_feed_configs_test.go +++ b/deployment/data-feeds/changeset/stellar/set_feed_configs_test.go @@ -9,14 +9,19 @@ import ( func TestSetFeedConfigsChangeset(t *testing.T) { env, inv, _ := newTestEnv(t) seedCacheRef(t, &env, testContractID, "test", "1.0.0") + seedContractMetadata(t, &env, testContractID, ContractMetadata{ + Feeds: map[string]FeedMetadata{ + "0x01c50f0e2106d5fd0000000000000000": {Description: "SOL/USD"}, + }, + }) req := &SetFeedConfigsRequest{ ChainSel: testChainSel, Qualifier: "test", Version: "1.0.0", Admin: testAdmin, - DataIDs: []string{"0x018e16c39e00032000000"}, - Descriptions: []string{"BTC/USD"}, + DataIDs: []string{"0x018e16c39e0003320000000000000000", "0x018e16c39e0003990000000000000000"}, + Descriptions: []string{"BTC/USD", "ETH/USD"}, Permissions: []FeedPermission{{ AllowedSender: testContractID, // the forwarder AllowedWorkflowOwner: "0x0102030405060708090a0b0c0d0e0f1011121314", @@ -25,18 +30,24 @@ func TestSetFeedConfigsChangeset(t *testing.T) { } require.NoError(t, SetFeedConfigs{}.VerifyPreconditions(env, req)) - _, err := SetFeedConfigs{}.Apply(env, req) + out, err := SetFeedConfigs{}.Apply(env, req) require.NoError(t, err) require.Len(t, inv.calls, 1) require.Equal(t, "set_feed_configs", inv.calls[0].Function) require.Equal(t, testContractID, inv.calls[0].ContractID) - // Permissions apply to every feed in the batch: a single permission set - // with two DataIDs must still pass length validation. - multi := *req - multi.DataIDs = []string{"0x01", "0x02"} - multi.Descriptions = []string{"BTC/USD", "ETH/USD"} - require.NoError(t, SetFeedConfigs{}.VerifyPreconditions(env, &multi)) + // the metadata mirror keeps the pre-existing feed and adds both new ones, + // each with its own description and id-derived decimals + meta := outputMetadata(t, out, testContractID) + require.Len(t, meta.Feeds, 3) + require.Equal(t, "SOL/USD", meta.Feeds["0x01c50f0e2106d5fd0000000000000000"].Description) + btc := meta.Feeds["0x018e16c39e0003320000000000000000"] + require.Equal(t, "BTC/USD", btc.Description) + require.Equal(t, uint32(18), btc.Decimals) + require.Equal(t, req.Permissions, btc.Permissions) + eth := meta.Feeds["0x018e16c39e0003990000000000000000"] + require.Equal(t, "ETH/USD", eth.Description) + require.Equal(t, uint32(0), eth.Decimals) // byte 7 outside the decimals range // mismatched lengths must fail preconditions bad := *req diff --git a/deployment/data-feeds/changeset/stellar/set_proxy_cache_test.go b/deployment/data-feeds/changeset/stellar/set_proxy_cache_test.go index 2ff1e334a87..608bc596555 100644 --- a/deployment/data-feeds/changeset/stellar/set_proxy_cache_test.go +++ b/deployment/data-feeds/changeset/stellar/set_proxy_cache_test.go @@ -21,13 +21,17 @@ func TestSetProxyCacheChangeset(t *testing.T) { } require.NoError(t, SetProxyCache{}.VerifyPreconditions(env, req)) - _, err := SetProxyCache{}.Apply(env, req) + out, err := SetProxyCache{}.Apply(env, req) require.NoError(t, err) require.Len(t, inv.calls, 1) require.Equal(t, "set_cache", inv.calls[0].Function) require.Equal(t, testProxyAddress, inv.calls[0].ContractID) require.Len(t, inv.calls[0].Args, 1) + // the metadata mirror records the proxy's new cache target + meta := outputMetadata(t, out, testProxyAddress) + require.Equal(t, testContractID, meta.Cache) + // missing cache ref must fail preconditions (proxy ref alone isn't enough) proxyOnly, _, _ := newTestEnv(t) seedProxyRef(t, &proxyOnly, testProxyAddress, "test-proxy", "1.0.0") diff --git a/deployment/data-feeds/changeset/stellar/state_test.go b/deployment/data-feeds/changeset/stellar/state_test.go index 219480f4602..b5a704eb93d 100644 --- a/deployment/data-feeds/changeset/stellar/state_test.go +++ b/deployment/data-feeds/changeset/stellar/state_test.go @@ -15,21 +15,13 @@ func TestLoadCacheClient(t *testing.T) { require.NotNil(t, client) require.Equal(t, testContractID, ref.Address) require.Equal(t, testContractID, client.ContractID()) -} - -func TestLoadCacheClient_MissingRef(t *testing.T) { - env, _, _ := newTestEnv(t) - seedCacheRef(t, &env, testContractID, "test-cache", "1.0.0") - _, _, err := LoadCacheClient(env, testChainSel, "does-not-exist", "1.0.0") + // missing ref must fail + _, _, err = LoadCacheClient(env, testChainSel, "does-not-exist", "1.0.0") require.Error(t, err) -} - -func TestLoadCacheClient_UnknownChain(t *testing.T) { - env, _, _ := newTestEnv(t) - seedCacheRef(t, &env, testContractID, "test-cache", "1.0.0") - _, _, err := LoadCacheClient(env, 999999, "test-cache", "1.0.0") + // unknown chain must fail + _, _, err = LoadCacheClient(env, 999999, "test-cache", "1.0.0") require.Error(t, err) } @@ -42,20 +34,12 @@ func TestLoadProxyClient(t *testing.T) { require.NotNil(t, client) require.Equal(t, testProxyAddress, ref.Address) require.Equal(t, testProxyAddress, client.ContractID()) -} - -func TestLoadProxyClient_MissingRef(t *testing.T) { - env, _, _ := newTestEnv(t) - seedProxyRef(t, &env, testProxyAddress, "test-proxy", "1.0.0") - _, _, err := LoadProxyClient(env, testChainSel, "does-not-exist", "1.0.0") + // missing ref must fail + _, _, err = LoadProxyClient(env, testChainSel, "does-not-exist", "1.0.0") require.Error(t, err) -} - -func TestLoadProxyClient_UnknownChain(t *testing.T) { - env, _, _ := newTestEnv(t) - seedProxyRef(t, &env, testProxyAddress, "test-proxy", "1.0.0") - _, _, err := LoadProxyClient(env, 999999, "test-proxy", "1.0.0") + // unknown chain must fail + _, _, err = LoadProxyClient(env, 999999, "test-proxy", "1.0.0") require.Error(t, err) } diff --git a/deployment/data-feeds/changeset/stellar/testutil_test.go b/deployment/data-feeds/changeset/stellar/testutil_test.go index d264d5b1e5f..23ef25a75de 100644 --- a/deployment/data-feeds/changeset/stellar/testutil_test.go +++ b/deployment/data-feeds/changeset/stellar/testutil_test.go @@ -106,6 +106,29 @@ func seedProxyRef(t *testing.T, env *cldf.Environment, address, qualifier, versi seedContractRefs(t, env, contractRefSpec{ProxyContract, address, qualifier, version}) } +// seedContractMetadata merges a contract metadata record into env.DataStore. +func seedContractMetadata(t *testing.T, env *cldf.Environment, address string, meta ContractMetadata) { + t.Helper() + ds := datastore.NewMemoryDataStore() + require.NoError(t, ds.Merge(env.DataStore)) + require.NoError(t, ds.ContractMetadata().Upsert(datastore.ContractMetadata{ + ChainSelector: testChainSel, + Address: address, + Metadata: meta, + })) + env.DataStore = ds.Seal() +} + +// outputMetadata returns the ContractMetadata a changeset recorded for address. +func outputMetadata(t *testing.T, out cldf.ChangesetOutput, address string) ContractMetadata { + t.Helper() + rec, err := out.DataStore.Seal().ContractMetadata().Get(datastore.NewContractMetadataKey(testChainSel, address)) + require.NoError(t, err) + meta, err := datastore.As[ContractMetadata](rec.Metadata) + require.NoError(t, err) + return meta +} + // writeDummyWasm writes a placeholder wasm file for VerifyPreconditions' path // check; the contents are never read. func writeDummyWasm(t *testing.T, name string) string { From e84d03e489cf21404982ea1e6166cd0200895e17 Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Wed, 5 Aug 2026 16:07:44 +0100 Subject: [PATCH 17/19] Remove the RenounceOwnership changeset Renouncing is irreversible and permanently bricks every owner-gated function, so it should not sit one changeset away. The e2e still exercises renounce_ownership on both contracts via direct client calls, keeping the every-exported-function guarantee. --- .../data-feeds/changeset/stellar/deps.go | 1 - .../data-feeds/changeset/stellar/e2e_test.go | 16 +++++----- .../data-feeds/changeset/stellar/ownership.go | 29 ------------------ .../changeset/stellar/ownership_test.go | 30 +++++-------------- .../changeset/stellar/registry_test.go | 9 +++--- 5 files changed, 20 insertions(+), 65 deletions(-) diff --git a/deployment/data-feeds/changeset/stellar/deps.go b/deployment/data-feeds/changeset/stellar/deps.go index 578ce0c11d0..9023b834493 100644 --- a/deployment/data-feeds/changeset/stellar/deps.go +++ b/deployment/data-feeds/changeset/stellar/deps.go @@ -41,7 +41,6 @@ var opVersion = semver.MustParse("1.0.0") type contractAdmin interface { TransferOwnership(ctx context.Context, newOwner string, liveUntilLedger uint32) error AcceptOwnership(ctx context.Context) error - RenounceOwnership(ctx context.Context) error Upgrade(ctx context.Context, newWasmHash [32]byte) error RecoverTokens(ctx context.Context, token, to string, amount int64) error } diff --git a/deployment/data-feeds/changeset/stellar/e2e_test.go b/deployment/data-feeds/changeset/stellar/e2e_test.go index 672dd370c54..77af570fa3f 100644 --- a/deployment/data-feeds/changeset/stellar/e2e_test.go +++ b/deployment/data-feeds/changeset/stellar/e2e_test.go @@ -48,7 +48,8 @@ import ( // remove_feed_configs (a) RemoveFeedConfigs changeset // transfer_ownership (a) TransferOwnership changeset (self-transfer) // accept_ownership (a) AcceptOwnership changeset -// renounce_ownership (a) RenounceOwnership changeset (LAST — irreversible) +// renounce_ownership (a) LoadCacheClient direct (LAST — irreversible; +// deliberately no changeset) // version (a) LoadCacheClient direct // type_and_version (a) LoadCacheClient direct // get_owner (a) LoadCacheClient direct @@ -76,7 +77,8 @@ import ( // set_cache (a) SetProxyCache changeset // transfer_ownership (a) TransferOwnership changeset (self-transfer) // accept_ownership (a) AcceptOwnership changeset -// renounce_ownership (a) RenounceOwnership changeset (LAST — irreversible) +// renounce_ownership (a) LoadProxyClient direct (LAST — irreversible; +// deliberately no changeset) // version (a) LoadProxyClient direct // type_and_version (a) LoadProxyClient direct // get_owner (a) LoadProxyClient direct @@ -314,9 +316,8 @@ func TestStellarDataFeedsE2E(t *testing.T) { env = apply(t, env, AcceptOwnership{}, &OwnershipRequest{ ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, Contract: ProxyContract, }) - env = apply(t, env, RenounceOwnership{}, &OwnershipRequest{ - ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, Contract: ProxyContract, - }) + // renounce deliberately has no changeset; call the client directly + require.NoError(t, proxyClient.RenounceOwnership(ctx)) // --- cache upgrade + recover + config/admin removal -------------------- env = apply(t, env, Upgrade{}, &UpgradeRequest{ @@ -363,9 +364,8 @@ func TestStellarDataFeedsE2E(t *testing.T) { env = apply(t, env, AcceptOwnership{}, &OwnershipRequest{ ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, Contract: CacheContract, }) - env = apply(t, env, RenounceOwnership{}, &OwnershipRequest{ - ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, Contract: CacheContract, - }) + // renounce deliberately has no changeset; call the client directly + require.NoError(t, cacheClient.RenounceOwnership(ctx)) } // skipInCI mirrors the Solana suite convention: container-backed e2e tests do diff --git a/deployment/data-feeds/changeset/stellar/ownership.go b/deployment/data-feeds/changeset/stellar/ownership.go index adaa0e2a8ae..19ec7c430e5 100644 --- a/deployment/data-feeds/changeset/stellar/ownership.go +++ b/deployment/data-feeds/changeset/stellar/ownership.go @@ -35,7 +35,6 @@ func (req *OwnershipRequest) verifyPreconditions(env cldf.Environment) error { var ( _ cldf.ChangeSetV2[*OwnershipRequest] = TransferOwnership{} _ cldf.ChangeSetV2[*OwnershipRequest] = AcceptOwnership{} - _ cldf.ChangeSetV2[*OwnershipRequest] = RenounceOwnership{} ) type ownershipInput struct { @@ -112,31 +111,3 @@ var acceptOwnershipOp = operations.NewOperation( return void{}, adminClient(d, in.ContractID, in.IsProxy).AcceptOwnership(b.GetContext()) }, ) - -// RenounceOwnership permanently renounces ownership. -type RenounceOwnership struct{} - -func (RenounceOwnership) VerifyPreconditions(env cldf.Environment, req *OwnershipRequest) error { - return req.verifyPreconditions(env) -} - -func (RenounceOwnership) Apply(env cldf.Environment, req *OwnershipRequest) (cldf.ChangesetOutput, error) { - var out cldf.ChangesetOutput - d, err := resolveDeps(env, req.ChainSel, req.Contract, req.Qualifier, req.Version) - if err != nil { - return out, err - } - _, err = operations.ExecuteOperation(env.OperationsBundle, renounceOwnershipOp, d.deps, ownershipInput{ - ContractID: d.contractID, - IsProxy: req.Contract == ProxyContract, - }) - return out, err -} - -var renounceOwnershipOp = operations.NewOperation( - "df:renounce-ownership", opVersion, - "Renounces ownership permanently", - func(b operations.Bundle, d StellarDeps, in ownershipInput) (void, error) { - return void{}, adminClient(d, in.ContractID, in.IsProxy).RenounceOwnership(b.GetContext()) - }, -) diff --git a/deployment/data-feeds/changeset/stellar/ownership_test.go b/deployment/data-feeds/changeset/stellar/ownership_test.go index 146f8a1421b..4bc10bdf162 100644 --- a/deployment/data-feeds/changeset/stellar/ownership_test.go +++ b/deployment/data-feeds/changeset/stellar/ownership_test.go @@ -40,15 +40,8 @@ func TestOwnershipChangesets(t *testing.T) { require.Equal(t, testContractID, inv.calls[1].ContractID) require.Empty(t, inv.calls[1].Args) - require.NoError(t, RenounceOwnership{}.VerifyPreconditions(env, cacheReq)) - _, err = RenounceOwnership{}.Apply(env, cacheReq) - require.NoError(t, err) - require.Len(t, inv.calls, 3) - require.Equal(t, "renounce_ownership", inv.calls[2].Function) - require.Equal(t, testContractID, inv.calls[2].ContractID) - - // same three ops against the proxy: IsProxy selects the proxy client and - // the resolved contractID is the proxy's address, not the cache's. + // same ops against the proxy: IsProxy selects the proxy client and the + // resolved contractID is the proxy's address, not the cache's. proxyReq := *cacheReq proxyReq.Qualifier = "test-proxy" proxyReq.Contract = ProxyContract @@ -56,30 +49,23 @@ func TestOwnershipChangesets(t *testing.T) { require.NoError(t, TransferOwnership{}.VerifyPreconditions(env, &proxyReq)) _, err = TransferOwnership{}.Apply(env, &proxyReq) require.NoError(t, err) - require.Equal(t, "transfer_ownership", inv.calls[3].Function) - require.Equal(t, testProxyAddress, inv.calls[3].ContractID) + require.Equal(t, "transfer_ownership", inv.calls[2].Function) + require.Equal(t, testProxyAddress, inv.calls[2].ContractID) require.NoError(t, AcceptOwnership{}.VerifyPreconditions(env, &proxyReq)) _, err = AcceptOwnership{}.Apply(env, &proxyReq) require.NoError(t, err) - require.Equal(t, "accept_ownership", inv.calls[4].Function) - require.Equal(t, testProxyAddress, inv.calls[4].ContractID) - - require.NoError(t, RenounceOwnership{}.VerifyPreconditions(env, &proxyReq)) - _, err = RenounceOwnership{}.Apply(env, &proxyReq) - require.NoError(t, err) - require.Equal(t, "renounce_ownership", inv.calls[5].Function) - require.Equal(t, testProxyAddress, inv.calls[5].ContractID) + require.Equal(t, "accept_ownership", inv.calls[3].Function) + require.Equal(t, testProxyAddress, inv.calls[3].ContractID) - // unknown Contract type must fail preconditions for all three changesets + // unknown Contract type must fail preconditions for both changesets badContract := *cacheReq badContract.Contract = datastore.ContractType("SomethingElse") require.Error(t, TransferOwnership{}.VerifyPreconditions(env, &badContract)) require.Error(t, AcceptOwnership{}.VerifyPreconditions(env, &badContract)) - require.Error(t, RenounceOwnership{}.VerifyPreconditions(env, &badContract)) // TransferOwnership requires a valid NewOwner and nonzero LiveUntilLedger; - // Accept/Renounce don't touch those fields so the same request stays valid. + // Accept doesn't touch those fields so the same request stays valid. missingOwner := *cacheReq missingOwner.NewOwner = "" require.Error(t, TransferOwnership{}.VerifyPreconditions(env, &missingOwner)) diff --git a/deployment/data-feeds/changeset/stellar/registry_test.go b/deployment/data-feeds/changeset/stellar/registry_test.go index fe849ba64c5..59ba40b77fc 100644 --- a/deployment/data-feeds/changeset/stellar/registry_test.go +++ b/deployment/data-feeds/changeset/stellar/registry_test.go @@ -22,13 +22,12 @@ func TestChangesetRegistryKeys(t *testing.T) { r.Add("0007_remove_feed_admin", cldregistry.Configure(RemoveFeedAdmin{}).WithEnvInput()) r.Add("0008_transfer_ownership", cldregistry.Configure(TransferOwnership{}).WithEnvInput()) r.Add("0009_accept_ownership", cldregistry.Configure(AcceptOwnership{}).WithEnvInput()) - r.Add("0010_renounce_ownership", cldregistry.Configure(RenounceOwnership{}).WithEnvInput()) - r.Add("0011_upgrade", cldregistry.Configure(Upgrade{}).WithEnvInput()) - r.Add("0012_recover_tokens", cldregistry.Configure(RecoverTokens{}).WithEnvInput()) - r.Add("0013_set_proxy_cache", cldregistry.Configure(SetProxyCache{}).WithEnvInput()) + r.Add("0010_upgrade", cldregistry.Configure(Upgrade{}).WithEnvInput()) + r.Add("0011_recover_tokens", cldregistry.Configure(RecoverTokens{}).WithEnvInput()) + r.Add("0012_set_proxy_cache", cldregistry.Configure(SetProxyCache{}).WithEnvInput()) keys := r.ListKeys() - require.Len(t, keys, 13) + require.Len(t, keys, 12) for _, k := range keys { t.Logf("registered: %s", k) } From b0934639097920e95eb41e41eaa6289607d82d14 Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Wed, 5 Aug 2026 23:08:39 +0100 Subject: [PATCH 18/19] Split stellar ownership request, dedupe helpers - Split TransferOwnershipRequest from OwnershipRequest so accept no longer carries transfer-only fields (NewOwner, LiveUntilLedger) - Share cache feed preconditions via verifyFeedPreconditions - Add CacheVersion so a proxy can reference a cache recorded under a different version - Drop the resolveDeps wrapper; resolve via resolveContractDeps and a shared getAddressRef - Return errors instead of panicking on bad versions in apply paths --- .../data-feeds/changeset/stellar/deploy.go | 21 +++++-- .../changeset/stellar/deploy_test.go | 29 +++++++++ .../data-feeds/changeset/stellar/deps.go | 45 +++++++------- .../data-feeds/changeset/stellar/e2e_test.go | 18 ++++-- .../changeset/stellar/feed_admin.go | 4 +- .../data-feeds/changeset/stellar/ownership.go | 40 ++++++------ .../changeset/stellar/ownership_test.go | 62 +++++++++++-------- .../changeset/stellar/recover_tokens.go | 2 +- .../changeset/stellar/remove_feed_configs.go | 18 +----- .../changeset/stellar/set_feed_configs.go | 19 ++---- .../changeset/stellar/set_feed_frozen.go | 18 +----- .../changeset/stellar/set_proxy_cache.go | 28 ++++----- .../changeset/stellar/set_proxy_cache_test.go | 31 ++++++++++ .../data-feeds/changeset/stellar/upgrade.go | 2 +- .../changeset/stellar/validation.go | 18 ++++++ 15 files changed, 210 insertions(+), 145 deletions(-) diff --git a/deployment/data-feeds/changeset/stellar/deploy.go b/deployment/data-feeds/changeset/stellar/deploy.go index 9bf3d74a69e..8f47f6d3499 100644 --- a/deployment/data-feeds/changeset/stellar/deploy.go +++ b/deployment/data-feeds/changeset/stellar/deploy.go @@ -105,11 +105,21 @@ type DeployProxyRequest struct { WasmPath string Owner string // defaults to the chain's deployer address when empty CacheQualifier string + CacheVersion string // cache's datastore version; defaults to Version Qualifier string Version string LabelSet datastore.LabelSet } +// cacheVersion defaults an empty CacheVersion to Version: cache and proxy +// usually share a release. +func (req *DeployProxyRequest) cacheVersion() string { + if req.CacheVersion != "" { + return req.CacheVersion + } + return req.Version +} + var _ cldf.ChangeSetV2[*DeployProxyRequest] = DeployProxy{} // DeployProxy deploys the proxy contract and records its address. @@ -123,7 +133,10 @@ type deployProxyInput struct { } func (DeployProxy) VerifyPreconditions(env cldf.Environment, req *DeployProxyRequest) error { - if err := verifyContractRef(env, req.ChainSel, CacheContract, req.CacheQualifier, req.Version); err != nil { + if _, err := semver.NewVersion(req.Version); err != nil { + return fmt.Errorf("invalid version %q: %w", req.Version, err) + } + if err := verifyContractRef(env, req.ChainSel, CacheContract, req.CacheQualifier, req.cacheVersion()); err != nil { return err } if _, err := os.Stat(req.WasmPath); err != nil { @@ -146,11 +159,9 @@ func (DeployProxy) Apply(env cldf.Environment, req *DeployProxyRequest) (cldf.Ch if err != nil { return out, err } - cacheRef, err := env.DataStore.Addresses().Get( - datastore.NewAddressRefKey(req.ChainSel, CacheContract, semver.MustParse(req.Version), req.CacheQualifier), - ) + cacheRef, err := getAddressRef(env, req.ChainSel, CacheContract, req.CacheQualifier, req.cacheVersion()) if err != nil { - return out, fmt.Errorf("cache address ref not found for qualifier %q: %w", req.CacheQualifier, err) + return out, err } owner, err := ownerOrSigner(ch, req.Owner) if err != nil { diff --git a/deployment/data-feeds/changeset/stellar/deploy_test.go b/deployment/data-feeds/changeset/stellar/deploy_test.go index d4b13355a75..e7d5e04bd4a 100644 --- a/deployment/data-feeds/changeset/stellar/deploy_test.go +++ b/deployment/data-feeds/changeset/stellar/deploy_test.go @@ -126,3 +126,32 @@ func TestDeployProxyChangeset(t *testing.T) { missing.CacheQualifier = "does-not-exist" require.Error(t, DeployProxy{}.VerifyPreconditions(env, &missing)) } + +// CacheVersion resolves a cache recorded under a different version than the +// proxy being deployed; empty CacheVersion falls back to Version (covered above). +func TestDeployProxyChangeset_CrossVersion(t *testing.T) { + env, _, _ := newTestEnv(t) + + cacheAddr := "CCACHEFAKE0000000000000000000000000000000000000000000000" + seedCacheRef(t, &env, cacheAddr, "test-cache", "1.0.0") + + req := &DeployProxyRequest{ + ChainSel: testChainSel, + WasmPath: writeDummyWasm(t, "data_feeds_proxy.wasm"), + CacheQualifier: "test-cache", + CacheVersion: "1.0.0", + Qualifier: "test-proxy", + Version: "1.1.0", + } + require.NoError(t, DeployProxy{}.VerifyPreconditions(env, req)) + + out, err := DeployProxy{}.Apply(env, req) + require.NoError(t, err) + meta := outputMetadata(t, out, testContractID) + require.Equal(t, cacheAddr, meta.Cache) + + // without CacheVersion the cache ref must not resolve at the proxy's version + noCacheVersion := *req + noCacheVersion.CacheVersion = "" + require.Error(t, DeployProxy{}.VerifyPreconditions(env, &noCacheVersion)) +} diff --git a/deployment/data-feeds/changeset/stellar/deps.go b/deployment/data-feeds/changeset/stellar/deps.go index 9023b834493..48da67083b5 100644 --- a/deployment/data-feeds/changeset/stellar/deps.go +++ b/deployment/data-feeds/changeset/stellar/deps.go @@ -83,12 +83,16 @@ func ownerOrSigner(ch cldfstellar.Chain, owner string) (string, error) { func recordAddress(address string, chainSel uint64, contractType datastore.ContractType, qualifier, version string, labels datastore.LabelSet) (cldf.ChangesetOutput, error) { var out cldf.ChangesetOutput + v, err := semver.NewVersion(version) + if err != nil { + return out, fmt.Errorf("invalid version %q: %w", version, err) + } out.DataStore = datastore.NewMemoryDataStore() return out, out.DataStore.Addresses().Add(datastore.AddressRef{ Address: address, ChainSelector: chainSel, Type: contractType, - Version: semver.MustParse(version), + Version: v, Qualifier: qualifier, Labels: labels, }) @@ -102,44 +106,39 @@ type stellarApplyDeps struct { // verifyContractRef checks the chain exists, the version parses, and an // AddressRef exists for the contract. -func verifyContractRef(env cldf.Environment, chainSel uint64, contractType datastore.ContractType, qualifier, versionStr string) error { +func verifyContractRef(env cldf.Environment, chainSel uint64, contractType datastore.ContractType, qualifier, version string) error { if _, ok := env.BlockChains.StellarChains()[chainSel]; !ok { return fmt.Errorf("stellar chain not found for chain selector %d", chainSel) } - version, err := semver.NewVersion(versionStr) + _, err := getAddressRef(env, chainSel, contractType, qualifier, version) + return err +} + +// getAddressRef parses version and fetches the contract's AddressRef. +func getAddressRef(env cldf.Environment, chainSel uint64, contractType datastore.ContractType, qualifier, version string) (datastore.AddressRef, error) { + v, err := semver.NewVersion(version) if err != nil { - return fmt.Errorf("invalid version %q: %w", versionStr, err) + return datastore.AddressRef{}, fmt.Errorf("invalid version %q: %w", version, err) } - if _, err := env.DataStore.Addresses().Get( - datastore.NewAddressRefKey(chainSel, contractType, version, qualifier), - ); err != nil { - return fmt.Errorf("%s address ref not found for qualifier %q: %w", contractType, qualifier, err) + ref, err := env.DataStore.Addresses().Get( + datastore.NewAddressRefKey(chainSel, contractType, v, qualifier), + ) + if err != nil { + return datastore.AddressRef{}, fmt.Errorf("%s address ref not found for qualifier %q: %w", contractType, qualifier, err) } - return nil -} - -// resolveDeps is resolveContractDeps for callers that don't need the AddressRef. -func resolveDeps(env cldf.Environment, chainSel uint64, contractType datastore.ContractType, qualifier, version string) (stellarApplyDeps, error) { - d, _, err := resolveContractDeps(env, chainSel, contractType, qualifier, version) - return d, err + return ref, nil } // resolveContractDeps resolves the contract's AddressRef and bundles it with // the chain deps. func resolveContractDeps(env cldf.Environment, chainSel uint64, contractType datastore.ContractType, qualifier, version string) (stellarApplyDeps, datastore.AddressRef, error) { - v, err := semver.NewVersion(version) - if err != nil { - return stellarApplyDeps{}, datastore.AddressRef{}, fmt.Errorf("invalid version %q: %w", version, err) - } _, deps, err := chainDeps(env, chainSel) if err != nil { return stellarApplyDeps{}, datastore.AddressRef{}, err } - ref, err := env.DataStore.Addresses().Get( - datastore.NewAddressRefKey(chainSel, contractType, v, qualifier), - ) + ref, err := getAddressRef(env, chainSel, contractType, qualifier, version) if err != nil { - return stellarApplyDeps{}, datastore.AddressRef{}, fmt.Errorf("%s address ref not found for qualifier %q: %w", contractType, qualifier, err) + return stellarApplyDeps{}, datastore.AddressRef{}, err } return stellarApplyDeps{deps: deps, contractID: ref.Address}, ref, nil } diff --git a/deployment/data-feeds/changeset/stellar/e2e_test.go b/deployment/data-feeds/changeset/stellar/e2e_test.go index 77af570fa3f..439585bd914 100644 --- a/deployment/data-feeds/changeset/stellar/e2e_test.go +++ b/deployment/data-feeds/changeset/stellar/e2e_test.go @@ -309,9 +309,12 @@ func TestStellarDataFeedsE2E(t *testing.T) { // --- proxy ownership: transfer -> accept -> renounce (renounce LAST) --- proxyLiveUntil := nextLiveUntil(t, ch) - env = apply(t, env, TransferOwnership{}, &OwnershipRequest{ - ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, - Contract: ProxyContract, NewOwner: deployer, LiveUntilLedger: proxyLiveUntil, + env = apply(t, env, TransferOwnership{}, &TransferOwnershipRequest{ + OwnershipRequest: OwnershipRequest{ + ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, + Contract: ProxyContract, + }, + NewOwner: deployer, LiveUntilLedger: proxyLiveUntil, }) env = apply(t, env, AcceptOwnership{}, &OwnershipRequest{ ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, Contract: ProxyContract, @@ -357,9 +360,12 @@ func TestStellarDataFeedsE2E(t *testing.T) { // --- cache ownership: transfer -> accept -> renounce (renounce LAST) --- cacheLiveUntil := nextLiveUntil(t, ch) - env = apply(t, env, TransferOwnership{}, &OwnershipRequest{ - ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, - Contract: CacheContract, NewOwner: deployer, LiveUntilLedger: cacheLiveUntil, + env = apply(t, env, TransferOwnership{}, &TransferOwnershipRequest{ + OwnershipRequest: OwnershipRequest{ + ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, + Contract: CacheContract, + }, + NewOwner: deployer, LiveUntilLedger: cacheLiveUntil, }) env = apply(t, env, AcceptOwnership{}, &OwnershipRequest{ ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, Contract: CacheContract, diff --git a/deployment/data-feeds/changeset/stellar/feed_admin.go b/deployment/data-feeds/changeset/stellar/feed_admin.go index e14fef6dc98..f6d8557feb5 100644 --- a/deployment/data-feeds/changeset/stellar/feed_admin.go +++ b/deployment/data-feeds/changeset/stellar/feed_admin.go @@ -43,7 +43,7 @@ func (AddFeedAdmin) VerifyPreconditions(env cldf.Environment, req *FeedAdminRequ func (AddFeedAdmin) Apply(env cldf.Environment, req *FeedAdminRequest) (cldf.ChangesetOutput, error) { var out cldf.ChangesetOutput - d, err := resolveDeps(env, req.ChainSel, CacheContract, req.Qualifier, req.Version) + d, _, err := resolveContractDeps(env, req.ChainSel, CacheContract, req.Qualifier, req.Version) if err != nil { return out, err } @@ -72,7 +72,7 @@ func (RemoveFeedAdmin) VerifyPreconditions(env cldf.Environment, req *FeedAdminR func (RemoveFeedAdmin) Apply(env cldf.Environment, req *FeedAdminRequest) (cldf.ChangesetOutput, error) { var out cldf.ChangesetOutput - d, err := resolveDeps(env, req.ChainSel, CacheContract, req.Qualifier, req.Version) + d, _, err := resolveContractDeps(env, req.ChainSel, CacheContract, req.Qualifier, req.Version) if err != nil { return out, err } diff --git a/deployment/data-feeds/changeset/stellar/ownership.go b/deployment/data-feeds/changeset/stellar/ownership.go index 19ec7c430e5..7e39f0b5ca8 100644 --- a/deployment/data-feeds/changeset/stellar/ownership.go +++ b/deployment/data-feeds/changeset/stellar/ownership.go @@ -9,32 +9,32 @@ import ( "github.com/smartcontractkit/chainlink-deployments-framework/operations" ) -// OwnershipRequest drives the ownership changesets for the cache or proxy. +// OwnershipRequest identifies the cache or proxy whose ownership changes. type OwnershipRequest struct { - ChainSel uint64 - Qualifier string - Version string - Contract datastore.ContractType // CacheContract or ProxyContract - NewOwner string // TransferOwnership only - LiveUntilLedger uint32 // TransferOwnership only; pending-transfer expiry ledger + ChainSel uint64 + Qualifier string + Version string + Contract datastore.ContractType // CacheContract or ProxyContract +} + +// TransferOwnershipRequest begins a two-step ownership transfer to NewOwner. +// The pending transfer expires at LiveUntilLedger unless accepted first. +type TransferOwnershipRequest struct { + OwnershipRequest + NewOwner string + LiveUntilLedger uint32 // pending-transfer expiry ledger } func (req *OwnershipRequest) verifyPreconditions(env cldf.Environment) error { - if _, ok := env.BlockChains.StellarChains()[req.ChainSel]; !ok { - return fmt.Errorf("stellar chain not found for chain selector %d", req.ChainSel) - } if err := validateContract(req.Contract); err != nil { return err } - if err := verifyContractRef(env, req.ChainSel, req.Contract, req.Qualifier, req.Version); err != nil { - return err - } - return nil + return verifyContractRef(env, req.ChainSel, req.Contract, req.Qualifier, req.Version) } var ( - _ cldf.ChangeSetV2[*OwnershipRequest] = TransferOwnership{} - _ cldf.ChangeSetV2[*OwnershipRequest] = AcceptOwnership{} + _ cldf.ChangeSetV2[*TransferOwnershipRequest] = TransferOwnership{} + _ cldf.ChangeSetV2[*OwnershipRequest] = AcceptOwnership{} ) type ownershipInput struct { @@ -47,7 +47,7 @@ type ownershipInput struct { // TransferOwnership begins a two-step ownership transfer. type TransferOwnership struct{} -func (TransferOwnership) VerifyPreconditions(env cldf.Environment, req *OwnershipRequest) error { +func (TransferOwnership) VerifyPreconditions(env cldf.Environment, req *TransferOwnershipRequest) error { if err := req.verifyPreconditions(env); err != nil { return err } @@ -60,9 +60,9 @@ func (TransferOwnership) VerifyPreconditions(env cldf.Environment, req *Ownershi return nil } -func (TransferOwnership) Apply(env cldf.Environment, req *OwnershipRequest) (cldf.ChangesetOutput, error) { +func (TransferOwnership) Apply(env cldf.Environment, req *TransferOwnershipRequest) (cldf.ChangesetOutput, error) { var out cldf.ChangesetOutput - d, err := resolveDeps(env, req.ChainSel, req.Contract, req.Qualifier, req.Version) + d, _, err := resolveContractDeps(env, req.ChainSel, req.Contract, req.Qualifier, req.Version) if err != nil { return out, err } @@ -93,7 +93,7 @@ func (AcceptOwnership) VerifyPreconditions(env cldf.Environment, req *OwnershipR func (AcceptOwnership) Apply(env cldf.Environment, req *OwnershipRequest) (cldf.ChangesetOutput, error) { var out cldf.ChangesetOutput - d, err := resolveDeps(env, req.ChainSel, req.Contract, req.Qualifier, req.Version) + d, _, err := resolveContractDeps(env, req.ChainSel, req.Contract, req.Qualifier, req.Version) if err != nil { return out, err } diff --git a/deployment/data-feeds/changeset/stellar/ownership_test.go b/deployment/data-feeds/changeset/stellar/ownership_test.go index 4bc10bdf162..279285166ad 100644 --- a/deployment/data-feeds/changeset/stellar/ownership_test.go +++ b/deployment/data-feeds/changeset/stellar/ownership_test.go @@ -15,25 +15,28 @@ func TestOwnershipChangesets(t *testing.T) { contractRefSpec{ProxyContract, testProxyAddress, "test-proxy", "1.0.0"}, ) - cacheReq := &OwnershipRequest{ - ChainSel: testChainSel, - Qualifier: "test-cache", - Version: "1.0.0", - Contract: CacheContract, - NewOwner: testAdmin, - LiveUntilLedger: 100, + base := OwnershipRequest{ + ChainSel: testChainSel, + Qualifier: "test-cache", + Version: "1.0.0", + Contract: CacheContract, + } + transferReq := &TransferOwnershipRequest{ + OwnershipRequest: base, + NewOwner: testAdmin, + LiveUntilLedger: 100, } - require.NoError(t, TransferOwnership{}.VerifyPreconditions(env, cacheReq)) - _, err := TransferOwnership{}.Apply(env, cacheReq) + require.NoError(t, TransferOwnership{}.VerifyPreconditions(env, transferReq)) + _, err := TransferOwnership{}.Apply(env, transferReq) require.NoError(t, err) require.Len(t, inv.calls, 1) require.Equal(t, "transfer_ownership", inv.calls[0].Function) require.Equal(t, testContractID, inv.calls[0].ContractID) require.Len(t, inv.calls[0].Args, 2) - require.NoError(t, AcceptOwnership{}.VerifyPreconditions(env, cacheReq)) - _, err = AcceptOwnership{}.Apply(env, cacheReq) + require.NoError(t, AcceptOwnership{}.VerifyPreconditions(env, &base)) + _, err = AcceptOwnership{}.Apply(env, &base) require.NoError(t, err) require.Len(t, inv.calls, 2) require.Equal(t, "accept_ownership", inv.calls[1].Function) @@ -42,45 +45,52 @@ func TestOwnershipChangesets(t *testing.T) { // same ops against the proxy: IsProxy selects the proxy client and the // resolved contractID is the proxy's address, not the cache's. - proxyReq := *cacheReq - proxyReq.Qualifier = "test-proxy" - proxyReq.Contract = ProxyContract + proxyBase := base + proxyBase.Qualifier = "test-proxy" + proxyBase.Contract = ProxyContract + proxyTransfer := &TransferOwnershipRequest{ + OwnershipRequest: proxyBase, + NewOwner: testAdmin, + LiveUntilLedger: 100, + } - require.NoError(t, TransferOwnership{}.VerifyPreconditions(env, &proxyReq)) - _, err = TransferOwnership{}.Apply(env, &proxyReq) + require.NoError(t, TransferOwnership{}.VerifyPreconditions(env, proxyTransfer)) + _, err = TransferOwnership{}.Apply(env, proxyTransfer) require.NoError(t, err) require.Equal(t, "transfer_ownership", inv.calls[2].Function) require.Equal(t, testProxyAddress, inv.calls[2].ContractID) - require.NoError(t, AcceptOwnership{}.VerifyPreconditions(env, &proxyReq)) - _, err = AcceptOwnership{}.Apply(env, &proxyReq) + require.NoError(t, AcceptOwnership{}.VerifyPreconditions(env, &proxyBase)) + _, err = AcceptOwnership{}.Apply(env, &proxyBase) require.NoError(t, err) require.Equal(t, "accept_ownership", inv.calls[3].Function) require.Equal(t, testProxyAddress, inv.calls[3].ContractID) // unknown Contract type must fail preconditions for both changesets - badContract := *cacheReq + badContract := base badContract.Contract = datastore.ContractType("SomethingElse") - require.Error(t, TransferOwnership{}.VerifyPreconditions(env, &badContract)) require.Error(t, AcceptOwnership{}.VerifyPreconditions(env, &badContract)) + badTransfer := *transferReq + badTransfer.Contract = badContract.Contract + require.Error(t, TransferOwnership{}.VerifyPreconditions(env, &badTransfer)) // TransferOwnership requires a valid NewOwner and nonzero LiveUntilLedger; - // Accept doesn't touch those fields so the same request stays valid. - missingOwner := *cacheReq + // AcceptOwnership's request doesn't carry those fields at all. + missingOwner := *transferReq missingOwner.NewOwner = "" require.Error(t, TransferOwnership{}.VerifyPreconditions(env, &missingOwner)) - require.NoError(t, AcceptOwnership{}.VerifyPreconditions(env, &missingOwner)) + require.NoError(t, AcceptOwnership{}.VerifyPreconditions(env, &missingOwner.OwnershipRequest)) - badOwner := *cacheReq + badOwner := *transferReq badOwner.NewOwner = "not-a-key" require.Error(t, TransferOwnership{}.VerifyPreconditions(env, &badOwner)) - zeroLedger := *cacheReq + zeroLedger := *transferReq zeroLedger.LiveUntilLedger = 0 require.Error(t, TransferOwnership{}.VerifyPreconditions(env, &zeroLedger)) // missing address ref must fail preconditions - badQualifier := *cacheReq + badQualifier := *transferReq badQualifier.Qualifier = "does-not-exist" require.Error(t, TransferOwnership{}.VerifyPreconditions(env, &badQualifier)) } diff --git a/deployment/data-feeds/changeset/stellar/recover_tokens.go b/deployment/data-feeds/changeset/stellar/recover_tokens.go index b841c6a36c3..b7ad825e252 100644 --- a/deployment/data-feeds/changeset/stellar/recover_tokens.go +++ b/deployment/data-feeds/changeset/stellar/recover_tokens.go @@ -54,7 +54,7 @@ func (RecoverTokens) VerifyPreconditions(env cldf.Environment, req *RecoverToken func (RecoverTokens) Apply(env cldf.Environment, req *RecoverTokensRequest) (cldf.ChangesetOutput, error) { var out cldf.ChangesetOutput - d, err := resolveDeps(env, req.ChainSel, req.Contract, req.Qualifier, req.Version) + d, _, err := resolveContractDeps(env, req.ChainSel, req.Contract, req.Qualifier, req.Version) if err != nil { return out, err } diff --git a/deployment/data-feeds/changeset/stellar/remove_feed_configs.go b/deployment/data-feeds/changeset/stellar/remove_feed_configs.go index da8aede2539..b2a8711ae86 100644 --- a/deployment/data-feeds/changeset/stellar/remove_feed_configs.go +++ b/deployment/data-feeds/changeset/stellar/remove_feed_configs.go @@ -1,8 +1,6 @@ package stellar import ( - "errors" - cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/smartcontractkit/chainlink-deployments-framework/operations" cache "github.com/smartcontractkit/chainlink-stellar/bindings/contracts/data_feeds_cache" @@ -29,24 +27,12 @@ type removeFeedConfigsInput struct { } func (RemoveFeedConfigs) VerifyPreconditions(env cldf.Environment, req *RemoveFeedConfigsRequest) error { - if err := verifyContractRef(env, req.ChainSel, CacheContract, req.Qualifier, req.Version); err != nil { - return err - } - if err := validateAddress(req.Admin); err != nil { - return err - } - if len(req.DataIDs) == 0 { - return errors.New("DataIDs cannot be empty") - } - if _, err := dataIDsToBytes(req.DataIDs); err != nil { - return err - } - return nil + return verifyFeedPreconditions(env, req.ChainSel, req.Qualifier, req.Version, req.Admin, req.DataIDs) } func (RemoveFeedConfigs) Apply(env cldf.Environment, req *RemoveFeedConfigsRequest) (cldf.ChangesetOutput, error) { var out cldf.ChangesetOutput - d, err := resolveDeps(env, req.ChainSel, CacheContract, req.Qualifier, req.Version) + d, _, err := resolveContractDeps(env, req.ChainSel, CacheContract, req.Qualifier, req.Version) if err != nil { return out, err } diff --git a/deployment/data-feeds/changeset/stellar/set_feed_configs.go b/deployment/data-feeds/changeset/stellar/set_feed_configs.go index 827cd124eb8..68e23265969 100644 --- a/deployment/data-feeds/changeset/stellar/set_feed_configs.go +++ b/deployment/data-feeds/changeset/stellar/set_feed_configs.go @@ -65,33 +65,22 @@ type setFeedConfigsInput struct { } func (SetFeedConfigs) VerifyPreconditions(env cldf.Environment, req *SetFeedConfigsRequest) error { - if err := verifyContractRef(env, req.ChainSel, CacheContract, req.Qualifier, req.Version); err != nil { + if err := verifyFeedPreconditions(env, req.ChainSel, req.Qualifier, req.Version, req.Admin, req.DataIDs); err != nil { return err } - if err := validateAddress(req.Admin); err != nil { - return err - } - if len(req.DataIDs) == 0 { - return errors.New("DataIDs cannot be empty") - } if len(req.DataIDs) != len(req.Descriptions) { return errors.New("DataIDs and Descriptions must have the same length") } if len(req.Permissions) == 0 { return errors.New("Permissions cannot be empty") } - if _, err := dataIDsToBytes(req.DataIDs); err != nil { - return err - } - if _, err := req.permissions(); err != nil { - return err - } - return nil + _, err := req.permissions() + return err } func (SetFeedConfigs) Apply(env cldf.Environment, req *SetFeedConfigsRequest) (cldf.ChangesetOutput, error) { var out cldf.ChangesetOutput - d, err := resolveDeps(env, req.ChainSel, CacheContract, req.Qualifier, req.Version) + d, _, err := resolveContractDeps(env, req.ChainSel, CacheContract, req.Qualifier, req.Version) if err != nil { return out, err } diff --git a/deployment/data-feeds/changeset/stellar/set_feed_frozen.go b/deployment/data-feeds/changeset/stellar/set_feed_frozen.go index a0ddcbe36df..496e0794155 100644 --- a/deployment/data-feeds/changeset/stellar/set_feed_frozen.go +++ b/deployment/data-feeds/changeset/stellar/set_feed_frozen.go @@ -1,8 +1,6 @@ package stellar import ( - "errors" - cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/smartcontractkit/chainlink-deployments-framework/operations" cache "github.com/smartcontractkit/chainlink-stellar/bindings/contracts/data_feeds_cache" @@ -32,24 +30,12 @@ type setFeedFrozenInput struct { } func (SetFeedFrozen) VerifyPreconditions(env cldf.Environment, req *SetFeedFrozenRequest) error { - if err := verifyContractRef(env, req.ChainSel, CacheContract, req.Qualifier, req.Version); err != nil { - return err - } - if err := validateAddress(req.Admin); err != nil { - return err - } - if len(req.DataIDs) == 0 { - return errors.New("DataIDs cannot be empty") - } - if _, err := dataIDsToBytes(req.DataIDs); err != nil { - return err - } - return nil + return verifyFeedPreconditions(env, req.ChainSel, req.Qualifier, req.Version, req.Admin, req.DataIDs) } func (SetFeedFrozen) Apply(env cldf.Environment, req *SetFeedFrozenRequest) (cldf.ChangesetOutput, error) { var out cldf.ChangesetOutput - d, err := resolveDeps(env, req.ChainSel, CacheContract, req.Qualifier, req.Version) + d, _, err := resolveContractDeps(env, req.ChainSel, CacheContract, req.Qualifier, req.Version) if err != nil { return out, err } diff --git a/deployment/data-feeds/changeset/stellar/set_proxy_cache.go b/deployment/data-feeds/changeset/stellar/set_proxy_cache.go index 8c1dd7cc794..eb784dc57f4 100644 --- a/deployment/data-feeds/changeset/stellar/set_proxy_cache.go +++ b/deployment/data-feeds/changeset/stellar/set_proxy_cache.go @@ -1,11 +1,6 @@ package stellar import ( - "fmt" - - "github.com/Masterminds/semver/v3" - - "github.com/smartcontractkit/chainlink-deployments-framework/datastore" cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/smartcontractkit/chainlink-deployments-framework/operations" proxy "github.com/smartcontractkit/chainlink-stellar/bindings/contracts/data_feeds_proxy" @@ -18,6 +13,16 @@ type SetProxyCacheRequest struct { Qualifier string Version string CacheQualifier string + CacheVersion string // cache's datastore version; defaults to Version +} + +// cacheVersion defaults an empty CacheVersion to Version: cache and proxy +// usually share a release. +func (req *SetProxyCacheRequest) cacheVersion() string { + if req.CacheVersion != "" { + return req.CacheVersion + } + return req.Version } var _ cldf.ChangeSetV2[*SetProxyCacheRequest] = SetProxyCache{} @@ -34,23 +39,18 @@ func (SetProxyCache) VerifyPreconditions(env cldf.Environment, req *SetProxyCach if err := verifyContractRef(env, req.ChainSel, ProxyContract, req.Qualifier, req.Version); err != nil { return err } - if err := verifyContractRef(env, req.ChainSel, CacheContract, req.CacheQualifier, req.Version); err != nil { - return err - } - return nil + return verifyContractRef(env, req.ChainSel, CacheContract, req.CacheQualifier, req.cacheVersion()) } func (SetProxyCache) Apply(env cldf.Environment, req *SetProxyCacheRequest) (cldf.ChangesetOutput, error) { var out cldf.ChangesetOutput - proxyDeps, err := resolveDeps(env, req.ChainSel, ProxyContract, req.Qualifier, req.Version) + proxyDeps, _, err := resolveContractDeps(env, req.ChainSel, ProxyContract, req.Qualifier, req.Version) if err != nil { return out, err } - cacheRef, err := env.DataStore.Addresses().Get( - datastore.NewAddressRefKey(req.ChainSel, CacheContract, semver.MustParse(req.Version), req.CacheQualifier), - ) + cacheRef, err := getAddressRef(env, req.ChainSel, CacheContract, req.CacheQualifier, req.cacheVersion()) if err != nil { - return out, fmt.Errorf("cache address ref not found for qualifier %q: %w", req.CacheQualifier, err) + return out, err } _, err = operations.ExecuteOperation(env.OperationsBundle, setProxyCacheOp, proxyDeps.deps, setProxyCacheInput{ diff --git a/deployment/data-feeds/changeset/stellar/set_proxy_cache_test.go b/deployment/data-feeds/changeset/stellar/set_proxy_cache_test.go index 608bc596555..7888bd5752e 100644 --- a/deployment/data-feeds/changeset/stellar/set_proxy_cache_test.go +++ b/deployment/data-feeds/changeset/stellar/set_proxy_cache_test.go @@ -42,3 +42,34 @@ func TestSetProxyCacheChangeset(t *testing.T) { seedCacheRef(t, &cacheOnly, testContractID, "test-cache", "1.0.0") require.Error(t, SetProxyCache{}.VerifyPreconditions(cacheOnly, req)) } + +// CacheVersion resolves a cache recorded under a different version than the +// proxy; empty CacheVersion falls back to Version (covered above). +func TestSetProxyCacheChangeset_CrossVersion(t *testing.T) { + env, inv, _ := newTestEnv(t) + seedContractRefs(t, &env, + contractRefSpec{ProxyContract, testProxyAddress, "test-proxy", "1.1.0"}, + contractRefSpec{CacheContract, testContractID, "test-cache", "1.0.0"}, + ) + + req := &SetProxyCacheRequest{ + ChainSel: testChainSel, + Qualifier: "test-proxy", + Version: "1.1.0", + CacheQualifier: "test-cache", + CacheVersion: "1.0.0", + } + require.NoError(t, SetProxyCache{}.VerifyPreconditions(env, req)) + + out, err := SetProxyCache{}.Apply(env, req) + require.NoError(t, err) + require.Equal(t, "set_cache", inv.calls[0].Function) + + meta := outputMetadata(t, out, testProxyAddress) + require.Equal(t, testContractID, meta.Cache) + + // without CacheVersion the cache ref must not resolve at the proxy's version + noCacheVersion := *req + noCacheVersion.CacheVersion = "" + require.Error(t, SetProxyCache{}.VerifyPreconditions(env, &noCacheVersion)) +} diff --git a/deployment/data-feeds/changeset/stellar/upgrade.go b/deployment/data-feeds/changeset/stellar/upgrade.go index fb26a07f488..44271e3e176 100644 --- a/deployment/data-feeds/changeset/stellar/upgrade.go +++ b/deployment/data-feeds/changeset/stellar/upgrade.go @@ -52,7 +52,7 @@ func (Upgrade) VerifyPreconditions(env cldf.Environment, req *UpgradeRequest) er func (Upgrade) Apply(env cldf.Environment, req *UpgradeRequest) (cldf.ChangesetOutput, error) { var out cldf.ChangesetOutput - d, err := resolveDeps(env, req.ChainSel, req.Contract, req.Qualifier, req.Version) + d, _, err := resolveContractDeps(env, req.ChainSel, req.Contract, req.Qualifier, req.Version) if err != nil { return out, err } diff --git a/deployment/data-feeds/changeset/stellar/validation.go b/deployment/data-feeds/changeset/stellar/validation.go index e64036e29fb..a17f9579117 100644 --- a/deployment/data-feeds/changeset/stellar/validation.go +++ b/deployment/data-feeds/changeset/stellar/validation.go @@ -2,11 +2,13 @@ package stellar import ( "encoding/hex" + "errors" "fmt" "math/big" "strings" "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/stellar/go-stellar-sdk/strkey" ) @@ -29,6 +31,22 @@ func validateAddress(s string) error { return fmt.Errorf("%q is not a valid Stellar account or contract address", s) } +// verifyFeedPreconditions checks the preconditions shared by the cache feed +// changesets: cache address ref, valid admin, non-empty parseable DataIDs. +func verifyFeedPreconditions(env cldf.Environment, chainSel uint64, qualifier, version, admin string, dataIDs []string) error { + if err := verifyContractRef(env, chainSel, CacheContract, qualifier, version); err != nil { + return err + } + if err := validateAddress(admin); err != nil { + return err + } + if len(dataIDs) == 0 { + return errors.New("DataIDs cannot be empty") + } + _, err := dataIDsToBytes(dataIDs) + return err +} + // dataIDsToBytes converts hex feed IDs to [16]byte. Data IDs are canonically // left-aligned, so short values are left-justified with trailing zero padding. func dataIDsToBytes(ids []string) ([][16]byte, error) { From e6e49a1338421de4aa8f2f46e4e829a32a75d6af Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Sat, 8 Aug 2026 10:37:45 +0100 Subject: [PATCH 19/19] Reorganize stellar changeset helpers - Rename deps.go to chain_util.go and fold the byte converters (dataIDsToBytes, workflowNameToBytes, workflowOwnerToBytes) into it - Move recordAddress next to its only callers in deploy.go - Rename testutil_test.go to test_util.go - Drop validation_test.go --- .../stellar/{deps.go => chain_util.go} | 77 ++- .../data-feeds/changeset/stellar/deploy.go | 17 + .../data-feeds/changeset/stellar/doc.go | 3 - .../data-feeds/changeset/stellar/e2e_test.go | 548 ------------------ .../changeset/stellar/report_encoding_test.go | 71 --- .../changeset/stellar/set_proxy_cache_test.go | 2 +- .../data-feeds/changeset/stellar/state.go | 26 - .../changeset/stellar/state_test.go | 45 -- .../{testutil_test.go => test_util.go} | 9 - .../changeset/stellar/testdata/.gitignore | 5 - .../changeset/stellar/testdata/README.md | 33 -- .../stellar/testdata/data_feeds_cache.wasm | Bin 23100 -> 0 bytes .../stellar/testdata/data_feeds_proxy.wasm | Bin 9503 -> 0 bytes .../changeset/stellar/validation.go | 46 -- .../changeset/stellar/validation_test.go | 75 --- 15 files changed, 64 insertions(+), 893 deletions(-) rename deployment/data-feeds/changeset/stellar/{deps.go => chain_util.go} (72%) delete mode 100644 deployment/data-feeds/changeset/stellar/doc.go delete mode 100644 deployment/data-feeds/changeset/stellar/e2e_test.go delete mode 100644 deployment/data-feeds/changeset/stellar/report_encoding_test.go delete mode 100644 deployment/data-feeds/changeset/stellar/state.go delete mode 100644 deployment/data-feeds/changeset/stellar/state_test.go rename deployment/data-feeds/changeset/stellar/{testutil_test.go => test_util.go} (89%) delete mode 100644 deployment/data-feeds/changeset/stellar/testdata/.gitignore delete mode 100644 deployment/data-feeds/changeset/stellar/testdata/README.md delete mode 100644 deployment/data-feeds/changeset/stellar/testdata/data_feeds_cache.wasm delete mode 100644 deployment/data-feeds/changeset/stellar/testdata/data_feeds_proxy.wasm delete mode 100644 deployment/data-feeds/changeset/stellar/validation_test.go diff --git a/deployment/data-feeds/changeset/stellar/deps.go b/deployment/data-feeds/changeset/stellar/chain_util.go similarity index 72% rename from deployment/data-feeds/changeset/stellar/deps.go rename to deployment/data-feeds/changeset/stellar/chain_util.go index 48da67083b5..59cc456347f 100644 --- a/deployment/data-feeds/changeset/stellar/deps.go +++ b/deployment/data-feeds/changeset/stellar/chain_util.go @@ -2,8 +2,11 @@ package stellar import ( "context" + "encoding/hex" "errors" "fmt" + "math/big" + "strings" "github.com/Masterminds/semver/v3" "github.com/stellar/go-stellar-sdk/xdr" @@ -17,26 +20,20 @@ import ( stellardeploy "github.com/smartcontractkit/chainlink-stellar/deployment" ) -// StellarDeps bundles deploy and invoke chain I/O for the operations. The -// same chainlink-stellar Deployer satisfies both fields. type StellarDeps struct { Deploy SorobanContractDeployer Invoker bindings.Invoker } -// SorobanContractDeployer is the deploy surface needed from chainlink-stellar's -// Deployer, defined locally so upstream need not export it. type SorobanContractDeployer interface { DeployContractWithArgs(ctx context.Context, wasmPath string, salt [32]byte, ctorArgs []xdr.ScVal) (string, error) UploadContractWASM(ctx context.Context, wasmPath string) (xdr.Hash, error) } -// void is the output for operations that return no payload. type void struct{} var opVersion = semver.MustParse("1.0.0") -// contractAdmin is the ownership/upgrade/recovery surface shared by the // generated cache and proxy clients. type contractAdmin interface { TransferOwnership(ctx context.Context, newOwner string, liveUntilLedger uint32) error @@ -52,7 +49,6 @@ func adminClient(d StellarDeps, contractID string, isProxy bool) contractAdmin { return cache.NewDataFeedsCacheClient(d.Invoker, contractID) } -// newStellarDeps is a package-level var so unit tests can substitute fakes. var newStellarDeps = func(ch cldfstellar.Chain) (StellarDeps, error) { d, err := stellardeploy.NewDeployerFromChain(ch) if err != nil { @@ -70,7 +66,6 @@ func chainDeps(env cldf.Environment, chainSel uint64) (cldfstellar.Chain, Stella return ch, deps, err } -// ownerOrSigner defaults an empty owner to the chain's deployer signer. func ownerOrSigner(ch cldfstellar.Chain, owner string) (string, error) { if owner != "" { return owner, nil @@ -81,31 +76,11 @@ func ownerOrSigner(ch cldfstellar.Chain, owner string) (string, error) { return ch.Signer.Address(), nil } -func recordAddress(address string, chainSel uint64, contractType datastore.ContractType, qualifier, version string, labels datastore.LabelSet) (cldf.ChangesetOutput, error) { - var out cldf.ChangesetOutput - v, err := semver.NewVersion(version) - if err != nil { - return out, fmt.Errorf("invalid version %q: %w", version, err) - } - out.DataStore = datastore.NewMemoryDataStore() - return out, out.DataStore.Addresses().Add(datastore.AddressRef{ - Address: address, - ChainSelector: chainSel, - Type: contractType, - Version: v, - Qualifier: qualifier, - Labels: labels, - }) -} - -// stellarApplyDeps pairs a resolved contract address with its chain deps. type stellarApplyDeps struct { deps StellarDeps contractID string } -// verifyContractRef checks the chain exists, the version parses, and an -// AddressRef exists for the contract. func verifyContractRef(env cldf.Environment, chainSel uint64, contractType datastore.ContractType, qualifier, version string) error { if _, ok := env.BlockChains.StellarChains()[chainSel]; !ok { return fmt.Errorf("stellar chain not found for chain selector %d", chainSel) @@ -114,7 +89,6 @@ func verifyContractRef(env cldf.Environment, chainSel uint64, contractType datas return err } -// getAddressRef parses version and fetches the contract's AddressRef. func getAddressRef(env cldf.Environment, chainSel uint64, contractType datastore.ContractType, qualifier, version string) (datastore.AddressRef, error) { v, err := semver.NewVersion(version) if err != nil { @@ -129,8 +103,6 @@ func getAddressRef(env cldf.Environment, chainSel uint64, contractType datastore return ref, nil } -// resolveContractDeps resolves the contract's AddressRef and bundles it with -// the chain deps. func resolveContractDeps(env cldf.Environment, chainSel uint64, contractType datastore.ContractType, qualifier, version string) (stellarApplyDeps, datastore.AddressRef, error) { _, deps, err := chainDeps(env, chainSel) if err != nil { @@ -142,3 +114,46 @@ func resolveContractDeps(env cldf.Environment, chainSel uint64, contractType dat } return stellarApplyDeps{deps: deps, contractID: ref.Address}, ref, nil } + +// dataIDsToBytes converts hex feed IDs to [16]byte. Data IDs are canonically +// left-aligned, so short values are left-justified with trailing zero padding. +func dataIDsToBytes(ids []string) ([][16]byte, error) { + out := make([][16]byte, 0, len(ids)) + for _, id := range ids { + v, ok := new(big.Int).SetString(id, 0) + if !ok { + return nil, fmt.Errorf("invalid data_id: %q", id) + } + if v.BitLen() > 128 { + return nil, fmt.Errorf("data_id too long: %q (%d bits)", id, v.BitLen()) + } + var b [16]byte + copy(b[:], v.Bytes()) + out = append(out, b) + } + return out, nil +} + +// workflowNameToBytes right-pads an ASCII workflow name into [10]byte. +func workflowNameToBytes(s string) ([10]byte, error) { + var out [10]byte + if len(s) > len(out) { + return out, fmt.Errorf("workflow name %q exceeds %d bytes", s, len(out)) + } + copy(out[:], s) + return out, nil +} + +// workflowOwnerToBytes decodes a 20-byte hex workflow owner. +func workflowOwnerToBytes(hexStr string) ([20]byte, error) { + var out [20]byte + b, err := hex.DecodeString(strings.TrimPrefix(hexStr, "0x")) + if err != nil { + return out, fmt.Errorf("invalid workflow owner %q: %w", hexStr, err) + } + if len(b) != len(out) { + return out, fmt.Errorf("workflow owner must be %d bytes, got %d", len(out), len(b)) + } + copy(out[:], b) + return out, nil +} diff --git a/deployment/data-feeds/changeset/stellar/deploy.go b/deployment/data-feeds/changeset/stellar/deploy.go index 8f47f6d3499..5624b8e4b30 100644 --- a/deployment/data-feeds/changeset/stellar/deploy.go +++ b/deployment/data-feeds/changeset/stellar/deploy.go @@ -83,6 +83,23 @@ func (DeployCache) Apply(env cldf.Environment, req *DeployCacheRequest) (cldf.Ch // The cache constructor takes only the owner; the data-retention TTL is an // on-chain constant. +func recordAddress(address string, chainSel uint64, contractType datastore.ContractType, qualifier, version string, labels datastore.LabelSet) (cldf.ChangesetOutput, error) { + var out cldf.ChangesetOutput + v, err := semver.NewVersion(version) + if err != nil { + return out, fmt.Errorf("invalid version %q: %w", version, err) + } + out.DataStore = datastore.NewMemoryDataStore() + return out, out.DataStore.Addresses().Add(datastore.AddressRef{ + Address: address, + ChainSelector: chainSel, + Type: contractType, + Version: v, + Qualifier: qualifier, + Labels: labels, + }) +} + var deployCacheOp = operations.NewOperation( "df-cache:deploy", opVersion, "Deploys the DataFeedsCache Soroban contract", diff --git a/deployment/data-feeds/changeset/stellar/doc.go b/deployment/data-feeds/changeset/stellar/doc.go deleted file mode 100644 index 5367aaa09eb..00000000000 --- a/deployment/data-feeds/changeset/stellar/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package stellar contains CLD changesets for the Stellar Data Feeds cache -// and proxy contracts. -package stellar diff --git a/deployment/data-feeds/changeset/stellar/e2e_test.go b/deployment/data-feeds/changeset/stellar/e2e_test.go deleted file mode 100644 index 439585bd914..00000000000 --- a/deployment/data-feeds/changeset/stellar/e2e_test.go +++ /dev/null @@ -1,548 +0,0 @@ -package stellar - -import ( - "bytes" - "fmt" - "io" - "math/big" - "net/http" - "net/url" - "os" - "sync" - "testing" - "time" - - chainsel "github.com/smartcontractkit/chain-selectors" - "github.com/stellar/go-stellar-sdk/xdr" - "github.com/stretchr/testify/require" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - cldfchain "github.com/smartcontractkit/chainlink-deployments-framework/chain" - cldfstellar "github.com/smartcontractkit/chainlink-deployments-framework/chain/stellar" - stellarprovider "github.com/smartcontractkit/chainlink-deployments-framework/chain/stellar/provider" - cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" - "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/environment" - cache "github.com/smartcontractkit/chainlink-stellar/bindings/contracts/data_feeds_cache" - "github.com/smartcontractkit/chainlink-stellar/bindings/scval" - - commonchangeset "github.com/smartcontractkit/chainlink/deployment/common/changeset" -) - -// TestStellarDataFeedsE2E is the Definition-of-Done gate: it stands up a local -// CTF Soroban devnet, deploys both Data Feeds contracts, and invokes EVERY -// exported function on-chain — the 26 cache fns and the 14 proxy fns — through -// the real changesets/operations/bindings (no fakes). -// -// Function-coverage checklist. Outcome (a) = on-chain success; outcome (b) = a -// documented, asserted, expected contract-domain/host error (never ignored). -// -// DataFeedsCache (26): -// __constructor (a) DeployCache changeset -// upgrade (a) Upgrade changeset -// recover_tokens (b) RecoverTokens changeset — SAC transfer(from=self) -// re-enters the contract => host trap Error(Context, -// InvalidAction), asserted -// add_feed_admin (a) AddFeedAdmin changeset -// remove_feed_admin (a) RemoveFeedAdmin changeset -// set_feed_configs (a) SetFeedConfigs changeset -// remove_feed_configs (a) RemoveFeedConfigs changeset -// transfer_ownership (a) TransferOwnership changeset (self-transfer) -// accept_ownership (a) AcceptOwnership changeset -// renounce_ownership (a) LoadCacheClient direct (LAST — irreversible; -// deliberately no changeset) -// version (a) LoadCacheClient direct -// type_and_version (a) LoadCacheClient direct -// get_owner (a) LoadCacheClient direct -// decimals (a) LoadCacheClient direct — Some(0) while configured, -// None after remove_feed_configs -// description (a) LoadCacheClient direct — Some while configured, -// None after remove_feed_configs -// is_configured (a) LoadCacheClient direct — true, false after removal -// is_frozen (a) LoadCacheClient direct — false / true while frozen -// set_feed_frozen (a) SetFeedFrozen changeset — freeze/unfreeze -// round-trip; frozen decimals asserts the -// FeedFrozen error (b) -// is_feed_admin (a) LoadCacheClient direct -// has_permission (a) LoadCacheClient direct -// get_feed_permissions (a) LoadCacheClient direct -// on_report (a) LoadCacheClient direct — valid metadata+XDR report from the -// configured AllowedSender; stores a round -// latest_round (a) LoadCacheClient direct — returns the stored round -// get_round (a) LoadCacheClient direct — returns the stored round -// round_range (a) LoadCacheClient direct -// find_round (a) LoadCacheClient direct -// -// DataFeedsProxy (14): -// __constructor (a) DeployProxy changeset -// set_cache (a) SetProxyCache changeset -// transfer_ownership (a) TransferOwnership changeset (self-transfer) -// accept_ownership (a) AcceptOwnership changeset -// renounce_ownership (a) LoadProxyClient direct (LAST — irreversible; -// deliberately no changeset) -// version (a) LoadProxyClient direct -// type_and_version (a) LoadProxyClient direct -// get_owner (a) LoadProxyClient direct -// decimals (a) LoadProxyClient direct -// description (a) LoadProxyClient direct -// latest_round (a) LoadProxyClient direct — reads the round stored via on_report -// get_round (a) LoadProxyClient direct — reads the round stored via on_report -// upgrade (a) Upgrade changeset -// recover_tokens (b) RecoverTokens changeset — SAC transfer(from=self) -// re-enters the contract => host trap Error(Context, -// InvalidAction), asserted - -// e2eOnce guards the CTF container lifecycle; the provider requires a sync.Once. -var e2eOnce sync.Once - -const ( - e2eQualifier = "e2e" - e2eVersion = "1.0.0" - // e2eDataID is a canonical left-aligned feed id (BTC/USD-shaped). After - // dataIDsToBytes left-justifies it into [16]byte, byte[7] is 0x00, which is - // outside the cache's decimals window [0x20,0x60], so cache.decimals returns - // Some(0) while configured. Non-zero id => a valid feed id (is_valid_id passes). - e2eDataID = "0x018e16c39e00032000000" - e2eDescription = "BTC/USD" - e2eWorkflowName = "e2e" - // e2eWorkflowOwner is the 20-byte workflow owner shared by the feed config - // permission and the on_report metadata (they must match for the report to - // be accepted and stored). - e2eWorkflowOwner = "0x0102030405060708090a0b0c0d0e0f1011121314" - // e2eReportTS must be > 0 (the initial stored timestamp) so the report is - // recorded as a new round rather than rejected as stale. - e2eReportTS = uint64(1_700_000_000) -) - -func TestStellarDataFeedsE2E(t *testing.T) { - skipInCI(t) - - sel := chainsel.STELLAR_LOCALNET.Selector - ctx := t.Context() - - // --- stand up / attach to the local Soroban devnet --------------------- - bc := newE2EChain(t, sel) - - e, err := environment.New(ctx, - environment.WithChains(bc), - environment.WithLogger(logger.Test(t)), - ) - require.NoError(t, err) - env := *e - - ch, ok := env.BlockChains.StellarChains()[sel] - require.True(t, ok, "stellar chain %d must be present in the environment", sel) - deployer := ch.Signer.Address() - - // The random deployer key has no balance; friendbot funds it before any fee- - // bearing operation. Tolerates "already funded". - fundViaFriendbot(t, ch.FriendbotURL, deployer) - - const cacheWasm = "testdata/data_feeds_cache.wasm" - const proxyWasm = "testdata/data_feeds_proxy.wasm" - - // --- deploy + configure (changeset pipeline) --------------------------- - env = apply(t, env, DeployCache{}, &DeployCacheRequest{ - ChainSel: sel, WasmPath: cacheWasm, Qualifier: e2eQualifier, Version: e2eVersion, - }) - env = apply(t, env, DeployProxy{}, &DeployProxyRequest{ - ChainSel: sel, WasmPath: proxyWasm, CacheQualifier: e2eQualifier, - Qualifier: e2eQualifier, Version: e2eVersion, - }) - env = apply(t, env, AddFeedAdmin{}, &FeedAdminRequest{ - ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, Admin: deployer, - }) - env = apply(t, env, SetFeedConfigs{}, &SetFeedConfigsRequest{ - ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, Admin: deployer, - DataIDs: []string{e2eDataID}, Descriptions: []string{e2eDescription}, - Permissions: []FeedPermission{{ - AllowedSender: deployer, - AllowedWorkflowOwner: e2eWorkflowOwner, - AllowedWorkflowName: e2eWorkflowName, - }}, - }) - env = apply(t, env, SetProxyCache{}, &SetProxyCacheRequest{ - ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, CacheQualifier: e2eQualifier, - }) - - // Binding-typed feed id and permission tuple for the direct calls below. - ids, err := dataIDsToBytes([]string{e2eDataID}) - require.NoError(t, err) - did := [16]byte(ids[0]) - - ownerBytes, err := workflowOwnerToBytes(e2eWorkflowOwner) - require.NoError(t, err) - nameBytes, err := workflowNameToBytes(e2eWorkflowName) - require.NoError(t, err) - - // --- cache: on_report (stores a round) + every read ------------------- - cacheClient, cacheRef, err := LoadCacheClient(env, sel, e2eQualifier, e2eVersion) - require.NoError(t, err) - cacheAddr := cacheRef.Address - - metadata := buildReportMetadata([10]byte(nameBytes), [20]byte(ownerBytes)) - report := buildReport(t, ids[0], big.NewInt(123456), e2eReportTS) - require.NoError(t, cacheClient.OnReport(ctx, deployer, metadata, report), - "on_report with a valid encoding from the configured AllowedSender must succeed") - - // Prove the report was stored; the proxy read-through depends on it. - stored, err := cacheClient.LatestRound(ctx, did) - require.NoError(t, err) - require.NotNil(t, stored, "on_report should have stored a round") - require.Equalf(t, 0, stored.Answer.Cmp(big.NewInt(123456)), - "stored answer mismatch: got %v", stored.Answer) - require.Equal(t, uint64(1), stored.RoundId) - - mustCall(t, "cache.version", func() error { _, e := cacheClient.Version(ctx); return e }) - mustCall(t, "cache.type_and_version", func() error { _, e := cacheClient.TypeAndVersion(ctx); return e }) - mustCall(t, "cache.get_owner", func() error { _, e := cacheClient.GetOwner(ctx); return e }) - mustCall(t, "cache.decimals", func() error { - dec, e := cacheClient.Decimals(ctx, did) - if e != nil { - return e - } - require.NotNil(t, dec, "decimals must be present for a configured feed") - require.Equal(t, uint32(0), *dec) - return nil - }) - mustCall(t, "cache.description", func() error { - desc, e := cacheClient.Description(ctx, did) - if e != nil { - return e - } - require.NotNil(t, desc, "description must be present for a configured feed") - require.Equal(t, e2eDescription, *desc) - return nil - }) - mustCall(t, "cache.is_configured", func() error { - configured, e := cacheClient.IsConfigured(ctx, did) - if e != nil { - return e - } - require.True(t, configured, "feed was configured via SetFeedConfigs") - return nil - }) - mustCall(t, "cache.is_frozen", func() error { - frozen, e := cacheClient.IsFrozen(ctx, did) - if e != nil { - return e - } - require.False(t, frozen, "feed starts unfrozen") - return nil - }) - mustCall(t, "cache.is_feed_admin", func() error { _, e := cacheClient.IsFeedAdmin(ctx, deployer); return e }) - mustCall(t, "cache.has_permission", func() error { - _, e := cacheClient.HasPermission(ctx, did, deployer, [20]byte(ownerBytes), [10]byte(nameBytes)) - return e - }) - mustCall(t, "cache.get_feed_permissions", func() error { _, e := cacheClient.GetFeedPermissions(ctx, did); return e }) - mustCall(t, "cache.get_round", func() error { - r, e := cacheClient.GetRound(ctx, did, 1) - if e != nil { - return e - } - require.NotNil(t, r, "get_round(did,1) must return the stored round, not void") - return nil - }) - mustCall(t, "cache.round_range", func() error { - rounds, e := cacheClient.RoundRange(ctx, did, 1, 1) - if e != nil { - return e - } - require.NotEmpty(t, rounds, "round_range should return the stored round") - return nil - }) - mustCall(t, "cache.find_round", func() error { - r, e := cacheClient.FindRound(ctx, did, e2eReportTS, cache.BoundAtOrBefore) - if e != nil { - return e - } - require.NotNil(t, r, "find_round must return the stored round, not void") - return nil - }) - - // --- set_feed_frozen: freeze -> reads rejected -> unfreeze ------------- - env = apply(t, env, SetFeedFrozen{}, &SetFeedFrozenRequest{ - ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, Admin: deployer, - DataIDs: []string{e2eDataID}, Frozen: true, - }) - frozen, err := cacheClient.IsFrozen(ctx, did) - require.NoError(t, err) - require.True(t, frozen, "is_frozen must report the freeze") - _, frozenErr := cacheClient.Decimals(ctx, did) - require.Error(t, frozenErr, "decimals on a frozen feed must be rejected") - require.ErrorContains(t, frozenErr, "#109", "expected CacheError::FeedFrozen") - t.Logf("(b) cache.decimals while frozen error: %v", frozenErr) - env = apply(t, env, SetFeedFrozen{}, &SetFeedFrozenRequest{ - ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, Admin: deployer, - DataIDs: []string{e2eDataID}, Frozen: false, - }) - - // --- proxy reads (read through to the cache's stored round) ----------- - proxyClient, _, err := LoadProxyClient(env, sel, e2eQualifier, e2eVersion) - require.NoError(t, err) - - mustCall(t, "proxy.version", func() error { _, e := proxyClient.Version(ctx); return e }) - mustCall(t, "proxy.type_and_version", func() error { _, e := proxyClient.TypeAndVersion(ctx); return e }) - mustCall(t, "proxy.get_owner", func() error { _, e := proxyClient.GetOwner(ctx); return e }) - mustCall(t, "proxy.decimals", func() error { _, e := proxyClient.Decimals(ctx, did); return e }) - mustCall(t, "proxy.description", func() error { _, e := proxyClient.Description(ctx, did); return e }) - mustCall(t, "proxy.latest_round", func() error { _, e := proxyClient.LatestRound(ctx, did); return e }) - mustCall(t, "proxy.get_round", func() error { _, e := proxyClient.GetRound(ctx, did, 1); return e }) - - // --- proxy upgrade + recover_tokens ------------------------------------ - env = apply(t, env, Upgrade{}, &UpgradeRequest{ - ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, - Contract: ProxyContract, WasmPath: proxyWasm, - }) - - // recover_tokens is exercised with the contract's own address as the token: - // the SAC transfer sub-call re-enters the contract and Soroban traps with - // Error(Context, InvalidAction). The call is really invoked on-chain; a - // funded token balance for a success path is disproportionate here. - proxyRecoverErr := applyErr(t, env, RecoverTokens{}, &RecoverTokensRequest{ - ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, - Contract: ProxyContract, Token: proxyClient.ContractID(), To: deployer, Amount: 1, - }) - require.Error(t, proxyRecoverErr, "proxy recover_tokens must fail: self-transfer re-enters the contract") - require.ErrorContains(t, proxyRecoverErr, "InvalidAction", "expected a contract re-entry host trap") - t.Logf("(b) proxy.recover_tokens error: %v", proxyRecoverErr) - - // --- proxy ownership: transfer -> accept -> renounce (renounce LAST) --- - proxyLiveUntil := nextLiveUntil(t, ch) - env = apply(t, env, TransferOwnership{}, &TransferOwnershipRequest{ - OwnershipRequest: OwnershipRequest{ - ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, - Contract: ProxyContract, - }, - NewOwner: deployer, LiveUntilLedger: proxyLiveUntil, - }) - env = apply(t, env, AcceptOwnership{}, &OwnershipRequest{ - ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, Contract: ProxyContract, - }) - // renounce deliberately has no changeset; call the client directly - require.NoError(t, proxyClient.RenounceOwnership(ctx)) - - // --- cache upgrade + recover + config/admin removal -------------------- - env = apply(t, env, Upgrade{}, &UpgradeRequest{ - ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, - Contract: CacheContract, WasmPath: cacheWasm, - }) - - // recover_tokens: same re-entry trap as the proxy. Runs while the deployer - // is still owner; renounce happens last. - cacheRecoverErr := applyErr(t, env, RecoverTokens{}, &RecoverTokensRequest{ - ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, - Contract: CacheContract, Token: cacheAddr, To: deployer, Amount: 1, - }) - require.Error(t, cacheRecoverErr, "cache recover_tokens must fail: self-transfer re-enters the contract") - require.ErrorContains(t, cacheRecoverErr, "InvalidAction", "expected a contract re-entry host trap") - t.Logf("(b) cache.recover_tokens error: %v", cacheRecoverErr) - - env = apply(t, env, RemoveFeedConfigs{}, &RemoveFeedConfigsRequest{ - ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, Admin: deployer, - DataIDs: []string{e2eDataID}, - }) - - // The optional reads must decode None once the feed config is gone. - dec, err := cacheClient.Decimals(ctx, did) - require.NoError(t, err) - require.Nil(t, dec, "decimals must be absent after remove_feed_configs") - desc, err := cacheClient.Description(ctx, did) - require.NoError(t, err) - require.Nil(t, desc, "description must be absent after remove_feed_configs") - configured, err := cacheClient.IsConfigured(ctx, did) - require.NoError(t, err) - require.False(t, configured, "is_configured must be false after remove_feed_configs") - - env = apply(t, env, RemoveFeedAdmin{}, &FeedAdminRequest{ - ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, Admin: deployer, - }) - - // --- cache ownership: transfer -> accept -> renounce (renounce LAST) --- - cacheLiveUntil := nextLiveUntil(t, ch) - env = apply(t, env, TransferOwnership{}, &TransferOwnershipRequest{ - OwnershipRequest: OwnershipRequest{ - ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, - Contract: CacheContract, - }, - NewOwner: deployer, LiveUntilLedger: cacheLiveUntil, - }) - env = apply(t, env, AcceptOwnership{}, &OwnershipRequest{ - ChainSel: sel, Qualifier: e2eQualifier, Version: e2eVersion, Contract: CacheContract, - }) - // renounce deliberately has no changeset; call the client directly - require.NoError(t, cacheClient.RenounceOwnership(ctx)) -} - -// skipInCI mirrors the Solana suite convention: container-backed e2e tests do -// not run in CI (they need a local Docker daemon). -func skipInCI(t *testing.T) { - t.Helper() - if os.Getenv("CI") == "true" { - t.Skip("Skipping container-backed e2e test in CI") - } -} - -// newE2EChain provisions the Stellar devnet chain. -// -// The contracts under test are built with soroban-sdk 26.1.0, i.e. their wasm -// env-meta requires ledger protocol 26. quickstart:latest arms its local -// network at protocol 25 (uploads fail with `HostError: Error(Context, -// InternalError)`), so the CTF path pins stellar/quickstart:future, whose -// local network defaults to protocol 26. -// -// Default path: boot a CTF quickstart container via NewCTFChainProvider. -// -// Override path (STELLAR_E2E_RPC_URL set): attach to an externally started -// quickstart container via NewRPCChainProvider. This exists because the CTF -// framework hardcodes the container platform to linux/amd64 -// (chainlink-testing-framework framework/components/blockchain/stellar.go) and -// CLDF's CTFChainProviderConfig does not expose the ImagePlatform knob — so on -// a native linux/arm64 host without amd64 emulation the CTF path fails with -// "exec /start: exec format error" even though the quickstart images ship an -// arm64 variant. On such hosts run the same image natively and point the test -// at it: -// -// docker run -d -p 8000:8000 stellar/quickstart:latest \ -// --local --enable-soroban-rpc --protocol-version 26 -// STELLAR_E2E_RPC_URL=http://127.0.0.1:8000/rpc \ -// STELLAR_E2E_FRIENDBOT_URL=http://127.0.0.1:8000/friendbot \ -// go test ./data-feeds/changeset/stellar/ -run TestStellarDataFeedsE2E -v -timeout 25m -func newE2EChain(t *testing.T, sel uint64) cldfchain.BlockChain { - t.Helper() - - if rpcURL := os.Getenv("STELLAR_E2E_RPC_URL"); rpcURL != "" { - // Passphrase for the quickstart `--local` standalone network — the same - // one the CTF path uses (blockchain.DefaultStellarNetworkPassphrase). - const localPassphrase = "Standalone Network ; February 2017" - p := stellarprovider.NewRPCChainProvider(sel, stellarprovider.RPCChainProviderConfig{ - SorobanRPCURL: rpcURL, - NetworkPassphrase: localPassphrase, - FriendbotURL: os.Getenv("STELLAR_E2E_FRIENDBOT_URL"), - DeployerKeypairGen: stellarprovider.KeypairRandom(), - }) - bc, err := p.Initialize(t.Context()) - require.NoError(t, err) - return bc - } - - p := stellarprovider.NewCTFChainProvider(t, sel, stellarprovider.CTFChainProviderConfig{ - DeployerKeypairGen: stellarprovider.KeypairRandom(), - Once: &e2eOnce, - // protocol 26 local network; see doc comment above. - Image: "stellar/quickstart:future", - }) - bc, err := p.Initialize(t.Context()) - require.NoError(t, err) - return bc -} - -// apply configures and applies a single changeset, requiring success, and -// returns the merged environment (datastore + address book threaded forward). -func apply[C any](t *testing.T, env cldf.Environment, cs cldf.ChangeSetV2[C], cfg C) cldf.Environment { - t.Helper() - out, err := commonchangeset.Apply(t, env, commonchangeset.Configure(cs, cfg)) - require.NoError(t, err) - return out -} - -// applyErr configures and applies a single changeset expected to fail, and -// returns the error (the environment is unchanged on failure). -func applyErr[C any](t *testing.T, env cldf.Environment, cs cldf.ChangeSetV2[C], cfg C) error { - t.Helper() - _, err := commonchangeset.Apply(t, env, commonchangeset.Configure(cs, cfg)) - return err -} - -// mustCall runs a read/query closure and requires it to return no error. -func mustCall(t *testing.T, name string, fn func() error) { - t.Helper() - require.NoErrorf(t, fn(), "call %s", name) -} - -// nextLiveUntil returns a pending-transfer expiry ledger valid for the current -// chain: strictly greater than the current ledger and comfortably below -// max_live_until_ledger. u32::MAX would trip InvalidLiveUntilLedger. -func nextLiveUntil(t *testing.T, ch cldfstellar.Chain) uint32 { - t.Helper() - resp, err := ch.Client.GetLatestLedger(t.Context()) - require.NoError(t, err) - return resp.Sequence + 100_000 -} - -// buildReportMetadata lays out the 64-byte on_report metadata header the cache -// decodes: [0:32) workflow_cid | [32:42) workflow_name | [42:62) workflow_owner -// | [62:64) report_id. Only workflow_name and workflow_owner participate in the -// permission hash, so the cid/report_id bytes are left zero. -func buildReportMetadata(name [10]byte, owner [20]byte) []byte { - md := make([]byte, 64) - copy(md[32:42], name[:]) - copy(md[42:62], owner[:]) - return md -} - -// buildReport builds the on_report `report` payload: the XDR encoding of a -// Soroban `Vec` with a single entry. The cache truncates the -// 32-byte wire data id to its high 16 bytes, so wireHi16 is placed in the top -// half and the rest left zero. -func buildReport(t *testing.T, wireHi16 [16]byte, answer *big.Int, ts uint64) []byte { - t.Helper() - var wire [32]byte - copy(wire[0:16], wireHi16[:]) - - // #[contracttype] structs serialize as an ScMap with symbol keys sorted - // alphabetically (answer < data_id < timestamp) — BuildStructScVal sorts. - entry, err := scval.BuildStructScVal(map[string]xdr.ScVal{ - "data_id": scval.Bytes32ToScVal(wire), - "answer": scval.MustToScVal(scval.I256ToScVal(answer)), - "timestamp": scval.Uint64ToScVal(ts), - }) - require.NoError(t, err) - - vec := xdr.ScVec{entry} - vecPtr := &vec - reportVal := xdr.ScVal{Type: xdr.ScValTypeScvVec, Vec: &vecPtr} - b, err := reportVal.MarshalBinary() - require.NoError(t, err) - return b -} - -// fundViaFriendbot funds addr through the CTF chain's friendbot, tolerating an -// already-funded account. Friendbot may not be ready the instant the container -// starts, so it retries. -func fundViaFriendbot(t *testing.T, friendbotURL, addr string) { - t.Helper() - require.NotEmpty(t, friendbotURL, "friendbot URL must be set to fund the deployer") - - target := friendbotURL + "?addr=" + url.QueryEscape(addr) - var lastErr error - for i := 0; i < 15; i++ { - req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, target, nil) //nolint:gosec // local CTF devnet URL - if err != nil { - lastErr = err - time.Sleep(2 * time.Second) - continue - } - resp, err := http.DefaultClient.Do(req) - if err != nil { - lastErr = err - time.Sleep(2 * time.Second) - continue - } - body, _ := io.ReadAll(resp.Body) - _ = resp.Body.Close() - if resp.StatusCode == http.StatusOK { - return - } - // Friendbot returns a 400 with op_already_exists when the account is - // already funded — that's a success for our purposes. - if bytes.Contains(body, []byte("already")) || bytes.Contains(body, []byte("exists")) { - return - } - lastErr = fmt.Errorf("friendbot status %d: %s", resp.StatusCode, string(body)) - time.Sleep(2 * time.Second) - } - // Don't fail here — let the first fee-bearing deploy surface a precise - // underfunded error if funding genuinely failed. - t.Logf("warning: friendbot funding did not confirm success: %v", lastErr) -} diff --git a/deployment/data-feeds/changeset/stellar/report_encoding_test.go b/deployment/data-feeds/changeset/stellar/report_encoding_test.go deleted file mode 100644 index 6213f6d8590..00000000000 --- a/deployment/data-feeds/changeset/stellar/report_encoding_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package stellar - -import ( - "math/big" - "testing" - - "github.com/stellar/go-stellar-sdk/xdr" - "github.com/stretchr/testify/require" -) - -// TestBuildReportMetadataLayout pins buildReportMetadata's 64-byte on_report -// metadata header: [0:32) workflow_cid zero | [32:42) workflow_name | -// [42:62) workflow_owner | [62:64) report_id zero. -func TestBuildReportMetadataLayout(t *testing.T) { - var name [10]byte - for i := range name { - name[i] = byte(i + 1) // 0x01..0x0a, distinguishable from zero padding - } - var owner [20]byte - for i := range owner { - owner[i] = byte(i + 100) // 0x64..0x77 - } - - got := buildReportMetadata(name, owner) - require.Len(t, got, 64) - - want := make([]byte, 64) - copy(want[32:42], name[:]) - copy(want[42:62], owner[:]) - require.Equal(t, want, got) - - require.Equal(t, make([]byte, 32), got[0:32], "workflow_cid bytes must be zero") - require.Equal(t, name[:], got[32:42], "workflow_name occupies [32:42)") - require.Equal(t, owner[:], got[42:62], "workflow_owner occupies [42:62)") - require.Equal(t, []byte{0, 0}, got[62:64], "report_id bytes must be zero") -} - -// TestBuildReportScMapKeyOrder pins that buildReport's ScMap struct fields -// decode back in alphabetical key order (answer < data_id < timestamp), per -// scval.BuildStructScVal's sort.Strings over field names. -func TestBuildReportScMapKeyOrder(t *testing.T) { - var wireHi16 [16]byte - for i := range wireHi16 { - wireHi16[i] = byte(i + 1) - } - answer := big.NewInt(123456789) - const ts = uint64(1700000000) - - b := buildReport(t, wireHi16, answer, ts) - - var decoded xdr.ScVal - require.NoError(t, decoded.UnmarshalBinary(b)) - require.Equal(t, xdr.ScValTypeScvVec, decoded.Type) - require.NotNil(t, decoded.Vec) - vec := *decoded.Vec - require.Len(t, *vec, 1) - - entry := (*vec)[0] - require.Equal(t, xdr.ScValTypeScvMap, entry.Type) - require.NotNil(t, entry.Map) - scMap := *entry.Map - require.Len(t, *scMap, 3) - - keys := make([]string, len(*scMap)) - for i, e := range *scMap { - require.Equal(t, xdr.ScValTypeScvSymbol, e.Key.Type) - require.NotNil(t, e.Key.Sym) - keys[i] = string(*e.Key.Sym) - } - require.Equal(t, []string{"answer", "data_id", "timestamp"}, keys) -} diff --git a/deployment/data-feeds/changeset/stellar/set_proxy_cache_test.go b/deployment/data-feeds/changeset/stellar/set_proxy_cache_test.go index 7888bd5752e..f10f9ce4779 100644 --- a/deployment/data-feeds/changeset/stellar/set_proxy_cache_test.go +++ b/deployment/data-feeds/changeset/stellar/set_proxy_cache_test.go @@ -44,7 +44,7 @@ func TestSetProxyCacheChangeset(t *testing.T) { } // CacheVersion resolves a cache recorded under a different version than the -// proxy; empty CacheVersion falls back to Version (covered above). +// proxy; empty CacheVersion falls back to Version. func TestSetProxyCacheChangeset_CrossVersion(t *testing.T) { env, inv, _ := newTestEnv(t) seedContractRefs(t, &env, diff --git a/deployment/data-feeds/changeset/stellar/state.go b/deployment/data-feeds/changeset/stellar/state.go deleted file mode 100644 index 5dcd53b0570..00000000000 --- a/deployment/data-feeds/changeset/stellar/state.go +++ /dev/null @@ -1,26 +0,0 @@ -package stellar - -import ( - "github.com/smartcontractkit/chainlink-deployments-framework/datastore" - cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" - cache "github.com/smartcontractkit/chainlink-stellar/bindings/contracts/data_feeds_cache" - proxy "github.com/smartcontractkit/chainlink-stellar/bindings/contracts/data_feeds_proxy" -) - -// LoadCacheClient returns a cache client bound to the resolved AddressRef. -func LoadCacheClient(env cldf.Environment, chainSel uint64, qualifier, version string) (*cache.DataFeedsCacheClient, datastore.AddressRef, error) { - d, ref, err := resolveContractDeps(env, chainSel, CacheContract, qualifier, version) - if err != nil { - return nil, datastore.AddressRef{}, err - } - return cache.NewDataFeedsCacheClient(d.deps.Invoker, d.contractID), ref, nil -} - -// LoadProxyClient is LoadCacheClient's counterpart for ProxyContract. -func LoadProxyClient(env cldf.Environment, chainSel uint64, qualifier, version string) (*proxy.DataFeedsProxyClient, datastore.AddressRef, error) { - d, ref, err := resolveContractDeps(env, chainSel, ProxyContract, qualifier, version) - if err != nil { - return nil, datastore.AddressRef{}, err - } - return proxy.NewDataFeedsProxyClient(d.deps.Invoker, d.contractID), ref, nil -} diff --git a/deployment/data-feeds/changeset/stellar/state_test.go b/deployment/data-feeds/changeset/stellar/state_test.go deleted file mode 100644 index b5a704eb93d..00000000000 --- a/deployment/data-feeds/changeset/stellar/state_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package stellar - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestLoadCacheClient(t *testing.T) { - env, _, _ := newTestEnv(t) - seedCacheRef(t, &env, testContractID, "test-cache", "1.0.0") - - client, ref, err := LoadCacheClient(env, testChainSel, "test-cache", "1.0.0") - require.NoError(t, err) - require.NotNil(t, client) - require.Equal(t, testContractID, ref.Address) - require.Equal(t, testContractID, client.ContractID()) - - // missing ref must fail - _, _, err = LoadCacheClient(env, testChainSel, "does-not-exist", "1.0.0") - require.Error(t, err) - - // unknown chain must fail - _, _, err = LoadCacheClient(env, 999999, "test-cache", "1.0.0") - require.Error(t, err) -} - -func TestLoadProxyClient(t *testing.T) { - env, _, _ := newTestEnv(t) - seedProxyRef(t, &env, testProxyAddress, "test-proxy", "1.0.0") - - client, ref, err := LoadProxyClient(env, testChainSel, "test-proxy", "1.0.0") - require.NoError(t, err) - require.NotNil(t, client) - require.Equal(t, testProxyAddress, ref.Address) - require.Equal(t, testProxyAddress, client.ContractID()) - - // missing ref must fail - _, _, err = LoadProxyClient(env, testChainSel, "does-not-exist", "1.0.0") - require.Error(t, err) - - // unknown chain must fail - _, _, err = LoadProxyClient(env, 999999, "test-proxy", "1.0.0") - require.Error(t, err) -} diff --git a/deployment/data-feeds/changeset/stellar/testutil_test.go b/deployment/data-feeds/changeset/stellar/test_util.go similarity index 89% rename from deployment/data-feeds/changeset/stellar/testutil_test.go rename to deployment/data-feeds/changeset/stellar/test_util.go index 23ef25a75de..4deee6000db 100644 --- a/deployment/data-feeds/changeset/stellar/testutil_test.go +++ b/deployment/data-feeds/changeset/stellar/test_util.go @@ -28,11 +28,8 @@ const ( testProxyAddress = "CBPROXY00000000000000000000000000000000000000000000000AA" ) -// testChainSel is a real chain selector so ChainMetadata methods resolve. var testChainSel = chainsel.STELLAR_TESTNET.Selector -// newTestEnv returns an Environment with one fake Stellar chain. It swaps the -// newStellarDeps seam to fakes and restores it on cleanup. func newTestEnv(t *testing.T) (cldf.Environment, *fakeInvoker, *fakeDeployer) { t.Helper() @@ -79,8 +76,6 @@ type contractRefSpec struct { version string } -// seedContractRefs replaces env.DataStore with one holding the given refs. -// Call once per test with every ref the test needs. func seedContractRefs(t *testing.T, env *cldf.Environment, refs ...contractRefSpec) { t.Helper() ds := datastore.NewMemoryDataStore() @@ -106,7 +101,6 @@ func seedProxyRef(t *testing.T, env *cldf.Environment, address, qualifier, versi seedContractRefs(t, env, contractRefSpec{ProxyContract, address, qualifier, version}) } -// seedContractMetadata merges a contract metadata record into env.DataStore. func seedContractMetadata(t *testing.T, env *cldf.Environment, address string, meta ContractMetadata) { t.Helper() ds := datastore.NewMemoryDataStore() @@ -119,7 +113,6 @@ func seedContractMetadata(t *testing.T, env *cldf.Environment, address string, m env.DataStore = ds.Seal() } -// outputMetadata returns the ContractMetadata a changeset recorded for address. func outputMetadata(t *testing.T, out cldf.ChangesetOutput, address string) ContractMetadata { t.Helper() rec, err := out.DataStore.Seal().ContractMetadata().Get(datastore.NewContractMetadataKey(testChainSel, address)) @@ -129,8 +122,6 @@ func outputMetadata(t *testing.T, out cldf.ChangesetOutput, address string) Cont return meta } -// writeDummyWasm writes a placeholder wasm file for VerifyPreconditions' path -// check; the contents are never read. func writeDummyWasm(t *testing.T, name string) string { t.Helper() path := filepath.Join(t.TempDir(), name) diff --git a/deployment/data-feeds/changeset/stellar/testdata/.gitignore b/deployment/data-feeds/changeset/stellar/testdata/.gitignore deleted file mode 100644 index 391b5033818..00000000000 --- a/deployment/data-feeds/changeset/stellar/testdata/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# The repo-root .gitignore ignores *.wasm as build artifacts. These two files are -# committed test fixtures (the e2e test deploys them to a local Soroban devnet), -# so re-include them here. See README.md for regeneration. -!data_feeds_cache.wasm -!data_feeds_proxy.wasm diff --git a/deployment/data-feeds/changeset/stellar/testdata/README.md b/deployment/data-feeds/changeset/stellar/testdata/README.md deleted file mode 100644 index 9946575c99d..00000000000 --- a/deployment/data-feeds/changeset/stellar/testdata/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# Stellar Data Feeds e2e test fixtures - -`data_feeds_cache.wasm` and `data_feeds_proxy.wasm` are the compiled Soroban -contracts under test by `e2e_test.go` (`TestStellarDataFeedsE2E`). They are the -byte-for-byte artifacts of `stellar contract build` on chainlink-stellar's -`contracts/data-feeds` workspace at commit `971e21ae` (PR #161/#162 heads; -adds `is_configured`/`is_frozen`/`set_feed_frozen` and optional -`decimals`/`description` returns). - -sha256: - -- `data_feeds_cache.wasm` = `c494264eaaa2cf5e5a745c0022ac791ffa07d1beb24a1d2ac67fcb6d7f28ef0f` -- `data_feeds_proxy.wasm` = `f670dc316877fac6f6c594133f411af25254d4a305038eeade28d34b9382fe6a` - -## Regeneration - -These are committed binaries (the e2e test needs a real WASM to deploy to a -local CTF Soroban devnet). To regenerate after a contract change, rebuild from -the contract source repo and copy the artifacts back: - -```sh -cd /contracts/data-feeds -stellar contract build # stellar CLI 27.0.0 -cp target/wasm32v1-none/release/data_feeds_cache.wasm / -cp target/wasm32v1-none/release/data_feeds_proxy.wasm / -``` - -(Older stellar CLI versions output to `target/wasm32-unknown-unknown/release/` -instead — check which target triple your `stellar contract build` uses.) - -If the contract ABI changes, regenerate the Go bindings -(`chainlink-stellar/bindings/contracts/data_feeds_{cache,proxy}`) as well, or the -e2e test will fail to compile / decode. diff --git a/deployment/data-feeds/changeset/stellar/testdata/data_feeds_cache.wasm b/deployment/data-feeds/changeset/stellar/testdata/data_feeds_cache.wasm deleted file mode 100644 index b7390ecfb8189e7969e719862ee7b59208dd51d4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23100 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 diff --git a/deployment/data-feeds/changeset/stellar/testdata/data_feeds_proxy.wasm b/deployment/data-feeds/changeset/stellar/testdata/data_feeds_proxy.wasm deleted file mode 100644 index 5cedfbb14dbeaf2e0325884d550ac893eb2ea8d6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9503 zcmb_idu$xXd7qixJ05RIX-Ow8ds6JVlM+*wMUfI;Vp4~Dv7`uoNt7G}b=|w=-BLXA zUfMfKW|5L~B}+~b2twncZCW>Ofz)o07%EaU{}hG{#7K%dsnE0z`c8_XaoscsS_DlC z6z=bv+1Wdav>iKHNjvXvzWKi2_t-fxzg6RmG42l(#o8KQD{^`<{Ik|nTdc3KqK&%l zU&jPiwAR_a&1;^!KFfUV8`Y?Gb5*bKBt}&Fr1r5}<>> z!c8SC&IXcMd!2h58#kD<&b)8@iY{Ndq|2LB_WUm&OxA*0BU}}%zfdSO>aBLTQffCs z_5s%~m4arw(6~?!!q#G?$@ZnnL8(&nt1WiGDF>}mSZTH^jXE3cJsU(5*_f(_jg@+t z9qz08?V!~z=;q{(Feo+72VtSzSPtqfHZ>Rqb<8XUUAxTmwm_`pmlgx|!NGRu*INr% z(qZF6ZhN&E6#RO*09q|#;OBZ)nrB159I$(POy7rj!Q$ycq3pN)LQpST!YLFM1HW0A z_gjHw^>Lnl_RisPF4o;$S?2D}amki*%wv*C{@|`0N{=01{$gHU&fAwHiz+V5I4S1d zmX}XGN0mWU8RmRV%({2vSYEhy+Ov{Mj@BFeV*cywR6l19b0l+)34-Mw=DFBwFYWOn zROBVxtiYmS!H&{m$@W<(H%A4_^SOSm7NZaI-KD+EaWF-4_dR)j3U&7mwV%j1)10qs z;ueTol1t%wm}-Hty_V+*s)NpymC7?`%0f#M9FG?YEEJMsh7?|E$>Z+9EJTxMG#i`h z0ma4dav|0Qon~}7$M-S?w*!o2ysJ1ARq_`%Ha4D3W-T$>Io&?X&4M*gF6D$i>0Zf) zh3w~&@3V%pHulHKhuluL8kn+%a3Fiia#2{|!}|)!j!1z|%YA>=a!?lB$g2g{g3ZfFb7;qw_?{ouf^Zn&(1j z&tvXg1cH7}wMa2;20O~UAg+`##=`mbW#JApr`UxHNRr5V{2Z{ry_YWHkbQ2pkJIV? zlv;p`gDDW2!i$m{;RWJLWuZL4=A;G0y$&w`ZIexUjU&q(NY6DaYNrwT4mz zKdm63aVRU?T~gc+%~-CL0T%AcGuO_jlUX>W$at5*5S_-m4xi%fcK5sYNXVbm^E~x{ zIjnoXo5)B8mG6aR>~y>z!6C&m*!Z-P!RF5Ey7P0u*qZ2oT1&>gE62gfol2$5Y(-wqPFg>9x_>_Va>SR;ObdDM=zz;y*z#Z9FUSw@LNH#$x zkhMLY6-WDlMlKV?f&w}W5*0<|c~YiD1_)&;GB`2xJOU{Z7T`MwLZnaI&A9Ia9hlD$ zwqe+YWs}&(&O%4=Hc4z_jF#oE{@{?U(|UJzB=2nZzdmtf}2i&=o% zSUy&evLz$wFy~%4$UL?o#?->EK@}kD_UEw_YgMr!s1x191aB{m*a?@utV#4+pu)-MfdBAIw7Ega+-- z^Dx<6&Jh(u&;_>vJW?DXhedIu8=aAK3w+Zj7>^R8vA3q0&gb_Ew z1UV>=d%|PVKEQ@@iCvh*kL}7OJaLSsA{^$Dv_eAV_OKY$>!%SWRQGVFn^P(SpMc5J zS%{g*#0=YwA~`9BP{1l(6y(}bL>_i_8FGhwzZ#n&VNm;`Vdaa+{|v{TXjFTmY{L6H zI&Yu5A3G|y^wHQYZEuY*LJ&-VZQ};r2?5z3VE1X{aN22l%95amn!=E(4TEtYRVbAb zcKXPpib5K?ZmW60TYvxc zUw`pW{_(#61#bxEkCf=3n^YJhzv}%E0zW{J`UZlpw?jp0#RjlolWh-cW+br(1N5G! zc@}IKxfR840+Wv41XIO~4z#hYDN{Sv6$=@K0CIXJ4l2Kge8Zx2fP$FhNC)WqgjiFd z=Y~#+rm+%|l>Es&n?sf21fJPn?xb`dql61XNa~zRJ4va`eXnzkg8`*MsZtylXvSu# zC7tA`xCk>vWmMQIHMCht?!qbY8a*=#z>O?{NW zFcd}mICZ4FfG|O>7w*-S-tTNglDPVxpZ|-$efcx*LplYllFeaDg2)dr1IY`FUTa0? z0(s&5xiCou&P`Du-X@BVN+A)t8vktrFEW zGBr3W5>=Ef$9hd^R$OO4ysot23h4Vpn(q;5nas$1s<=O!alS(S7$2qhEQN%6@2BXG z=+3hV#HA!z%#KUahNd%O9UCWLeef9n#E$i|43=2IM`hWLcx&J(+OlxSpQ*#wYL7txr7g2kU?Hl(pbF+XXuC0UmsDnE{nmB3;;J>PK|3h3 zI|gvw1$w1+P(NP_+WvW5->D~~-k;_>%!Ey&iLiw$COtDbHgaTg#H?d16}DRCWj%cG z(Buekw`Xi>!uQ9=4v$VQ9G*TrIx*!>m&yyJ>GAo=>GF}oW0RBP3#G&3fu_jVprTP| zHG@*2wdgNHUTTPLAQ?NoJyXdN=|OXK@X!>&eb0nHRhn1`rY0v2kIpZQ9~z$?pPKSZ z(_@DgN>dY4W8;(aN2W`qamK!UXH1p0Re~VAm?x!7=XB6U2gkqeHsOn&-Y&bL_HFcv zp5EBFbcY!;YpFNB!9$()pr?Nm+EthJeH2nvJFoc8FY2Q8c4wG%?kzjCd(^#PrHpBc zri%*JS^uWj_S{6dLwS$6z%L&SL)?j)=h|G*UJ2_hSv=jS1;d_3{Jx^BEXWvhveJ^c z$-GB4LRoyMQ4ekzh5@JNgP<;B+>WHvd!!ehU8x22b}OB}TNbhCfb?(?+iJ;nL$((K zdAboc=KZ<^YAa!*PIE`VSl1P-VN;#wHMESpX`t(RR?Ray*iIfaA!WQ9>f}=*87cWB z=F~MWO?yju(S)vBU1|~?^(6F7uc~+RNgbOmnb9_Dw&T_44tjiBUN&D#|5$Y2@p*5# zPFLqMvQB7sr~1s9-%1X*-kZ}xORZ)sVVpkNhL&hI)Tn7Eb%0QPPP7gMA0=cp+b!wK zruNKT@HMXX-J_D=tPDqC`O#u2#IFG}Ghw3|#EyJsW)96W(JjB4Ju~xABMKCzzxWu; z;KV{2_zwdzta>Bn*zigAp?|Va93MSzjHumcR!S`pC_b8ttutnz6A^Nc6=8DZ7S!B0 zKF!F;z#l6@Xj^|4=?K)+w*Ek&M+m>zj-ymp z;V5eO^#qOpUO*k>9$Bum{P`+Eg!HRbxqzQi=m(jWJPS#e<@{>8xaZMEy}GIc>|@2B zo-!Z4P{+UXte@OY&K#J;0lPt+lmO8EigcaKn- zttCq)kv^f_1?6m&g$fj-GjOtwN?i%M>A&0kA`grRLEjeJZ)buA6iAbdRg%fI{8}J= z?W=&B^OZ8#BS;m~9n07we=@_}?$=?mSQBSe-l^Z(Q|8_M18&S7a_HD{$%!`_ICND*}MOt27SbygfE5bqS=hm7r>^x{1N@D zHzl#OE_F*AU2mI1ldQUF?%4d1NF)^P?fTy;2cdPh<{{L#T`odAbjxL6OHM#5J97rM zBnWCa^_!|-?Dp+#hDKAr70r2XG-or;)nMu~(VU%UOkVPMlfnh9|82awdF|`=JQy=F zanNt#7p=o*mU;#a%}%JK{?vpXW;@k7uNva3V^x20rSQK(l#V7dQ-yAU`!=WjntqykNUn-bMzWxOVoNZJEO6; zvo(XiySF)8%o2H)&Sw&lRnK629+OOwiZottix(H-m1c~}H#?8v=N-w~MBgr1XJX!r dtUnT8Js3WAcX(`@|K}2u!X)Dh-b}vY{{bs+OcwwE diff --git a/deployment/data-feeds/changeset/stellar/validation.go b/deployment/data-feeds/changeset/stellar/validation.go index a17f9579117..9c4ba3706d7 100644 --- a/deployment/data-feeds/changeset/stellar/validation.go +++ b/deployment/data-feeds/changeset/stellar/validation.go @@ -1,11 +1,8 @@ package stellar import ( - "encoding/hex" "errors" "fmt" - "math/big" - "strings" "github.com/smartcontractkit/chainlink-deployments-framework/datastore" cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" @@ -46,46 +43,3 @@ func verifyFeedPreconditions(env cldf.Environment, chainSel uint64, qualifier, v _, err := dataIDsToBytes(dataIDs) return err } - -// dataIDsToBytes converts hex feed IDs to [16]byte. Data IDs are canonically -// left-aligned, so short values are left-justified with trailing zero padding. -func dataIDsToBytes(ids []string) ([][16]byte, error) { - out := make([][16]byte, 0, len(ids)) - for _, id := range ids { - v, ok := new(big.Int).SetString(id, 0) - if !ok { - return nil, fmt.Errorf("invalid data_id: %q", id) - } - if v.BitLen() > 128 { - return nil, fmt.Errorf("data_id too long: %q (%d bits)", id, v.BitLen()) - } - var b [16]byte - copy(b[:], v.Bytes()) - out = append(out, b) - } - return out, nil -} - -// workflowNameToBytes right-pads an ASCII workflow name into [10]byte. -func workflowNameToBytes(s string) ([10]byte, error) { - var out [10]byte - if len(s) > len(out) { - return out, fmt.Errorf("workflow name %q exceeds %d bytes", s, len(out)) - } - copy(out[:], s) - return out, nil -} - -// workflowOwnerToBytes decodes a 20-byte hex workflow owner. -func workflowOwnerToBytes(hexStr string) ([20]byte, error) { - var out [20]byte - b, err := hex.DecodeString(strings.TrimPrefix(hexStr, "0x")) - if err != nil { - return out, fmt.Errorf("invalid workflow owner %q: %w", hexStr, err) - } - if len(b) != len(out) { - return out, fmt.Errorf("workflow owner must be %d bytes, got %d", len(out), len(b)) - } - copy(out[:], b) - return out, nil -} diff --git a/deployment/data-feeds/changeset/stellar/validation_test.go b/deployment/data-feeds/changeset/stellar/validation_test.go deleted file mode 100644 index 972f911e8ac..00000000000 --- a/deployment/data-feeds/changeset/stellar/validation_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package stellar - -import ( - "encoding/hex" - "strings" - "testing" - - "github.com/stretchr/testify/require" -) - -// mustHex16 decodes s (<=32 hex chars) and left-justifies it into a [16]byte, -// matching the layout dataIDsToBytes is expected to produce. -func mustHex16(t *testing.T, s string) [16]byte { - t.Helper() - b, err := hex.DecodeString(s) - require.NoError(t, err) - require.LessOrEqual(t, len(b), 16) - var out [16]byte - copy(out[:], b) - return out -} - -func TestValidateAddress(t *testing.T) { - require.NoError(t, validateAddress("GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN7")) // account - require.NoError(t, validateAddress("CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA")) // contract - require.Error(t, validateAddress("not-a-key")) - require.Error(t, validateAddress("")) -} - -func TestDataIDsToBytes(t *testing.T) { - // Full-width (exactly 16-byte) input: exact round-trip check. NOTE: for a - // value this wide, left-justified copy(b[:], v.Bytes()) and - // right-justified v.FillBytes(b[:]) produce byte-identical output, so - // this case alone cannot catch a regression to FillBytes — see the short - // case below for that. - got, err := dataIDsToBytes([]string{"0x018e16c39e0003200000000000000000"}) - require.NoError(t, err) - require.Len(t, got, 1) - require.Equal(t, mustHex16(t, "018e16c39e0003200000000000000000"), got[0]) - - // Short (sub-16-byte) shorthand input: this is the discriminating case. - // Left-justified (copy), the nonzero prefix "18e16c39e0003200000" lands in - // the LEADING bytes b[0:10] with zero padding trailing at b[10:16]: - // 18 e1 6c 39 e0 00 32 00 00 00 00 00 00 00 00 00 - // Right-justified (FillBytes) would instead push the same nonzero bytes - // down to END at b[15], i.e.: - // 00 00 00 00 00 00 18 e1 6c 39 e0 00 32 00 00 00 - got, err = dataIDsToBytes([]string{"0x018e16c39e00032000000"}) - require.NoError(t, err) - require.Len(t, got, 1) - require.Equal(t, byte(0x18), got[0][0]) - require.Equal(t, mustHex16(t, "18e16c39e00032000000000000000000"), got[0]) - - // >128-bit input (17 bytes) must be rejected. - _, err = dataIDsToBytes([]string{"0x01" + strings.Repeat("00", 16)}) - require.Error(t, err) - - // Invalid hex string must be rejected. - _, err = dataIDsToBytes([]string{"zzz"}) - require.Error(t, err) -} - -func TestWorkflowFieldConversions(t *testing.T) { - n, err := workflowNameToBytes("abc") - require.NoError(t, err) - require.Equal(t, byte('a'), n[0]) - _, err = workflowNameToBytes("elevenchars!") - require.Error(t, err) - - o, err := workflowOwnerToBytes("0x0102030405060708090a0b0c0d0e0f1011121314") - require.NoError(t, err) - require.Equal(t, byte(0x14), o[19]) - _, err = workflowOwnerToBytes("0x01") - require.Error(t, err) -}