Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 159 additions & 0 deletions deployment/data-feeds/changeset/stellar/chain_util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package stellar

import (
"context"
"encoding/hex"
"errors"
"fmt"
"math/big"
"strings"

"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"
)

type StellarDeps struct {
Deploy SorobanContractDeployer
Invoker bindings.Invoker
}

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)
}

type void struct{}

var opVersion = semver.MustParse("1.0.0")

// generated cache and proxy clients.
type contractAdmin interface {
TransferOwnership(ctx context.Context, newOwner string, liveUntilLedger uint32) error
AcceptOwnership(ctx context.Context) error
Upgrade(ctx context.Context, newWasmHash [32]byte) error
RecoverTokens(ctx context.Context, token, to string, amount int64) error
}

func adminClient(d StellarDeps, contractID string, isProxy bool) contractAdmin {
if isProxy {
return proxy.NewDataFeedsProxyClient(d.Invoker, contractID)
}
return cache.NewDataFeedsCacheClient(d.Invoker, contractID)
}

var newStellarDeps = func(ch cldfstellar.Chain) (StellarDeps, error) {
d, err := stellardeploy.NewDeployerFromChain(ch)
if err != nil {
return StellarDeps{}, err
}
return StellarDeps{Deploy: d, Invoker: d}, nil
}

func chainDeps(env cldf.Environment, chainSel uint64) (cldfstellar.Chain, StellarDeps, error) {
ch, ok := env.BlockChains.StellarChains()[chainSel]
if !ok {
return cldfstellar.Chain{}, StellarDeps{}, fmt.Errorf("stellar chain not found for chain selector %d", chainSel)
}
deps, err := newStellarDeps(ch)
return ch, deps, err
}

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
}

type stellarApplyDeps struct {
deps StellarDeps
contractID string
}

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)
}
_, err := getAddressRef(env, chainSel, contractType, qualifier, version)
return err
}

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 datastore.AddressRef{}, fmt.Errorf("invalid version %q: %w", version, 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 ref, nil
}

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 {
return stellarApplyDeps{}, datastore.AddressRef{}, err
}
ref, err := getAddressRef(env, chainSel, contractType, qualifier, version)
if err != nil {
return stellarApplyDeps{}, datastore.AddressRef{}, err
}
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
}
9 changes: 9 additions & 0 deletions deployment/data-feeds/changeset/stellar/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package stellar

import "github.com/smartcontractkit/chainlink-deployments-framework/datastore"

// Datastore contract types for the two DF contracts.
const (
CacheContract datastore.ContractType = "DataFeedsCache"
ProxyContract datastore.ContractType = "DataFeedsProxy"
)
223 changes: 223 additions & 0 deletions deployment/data-feeds/changeset/stellar/deploy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
package stellar

import (
"errors"
"fmt"
"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"
)

// 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 deploys the cache contract and records its address.
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)
}
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, deployCacheOp, deps, 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)
}

// 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",
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 is
// resolved from the datastore by CacheQualifier and must already be recorded.
type DeployProxyRequest struct {
ChainSel uint64
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.
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 := 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 {
return fmt.Errorf("wasm path: %w", err)
}
if req.Owner != "" {
if err := validateAddress(req.Owner); err != nil {
return err
}
}
if req.CacheQualifier == "" {
return errors.New("cache qualifier must be set")
}
return nil
}

func (DeployProxy) Apply(env cldf.Environment, req *DeployProxyRequest) (cldf.ChangesetOutput, error) {
var out cldf.ChangesetOutput
ch, deps, err := chainDeps(env, req.ChainSel)
if err != nil {
return out, err
}
cacheRef, err := getAddressRef(env, req.ChainSel, CacheContract, req.CacheQualifier, req.cacheVersion())
if err != nil {
return out, err
}
owner, err := ownerOrSigner(ch, req.Owner)
if err != nil {
return out, err
}

salt := stellardeploy.GenerateDeterministicSalt(owner, "data_feeds_proxy-"+req.Qualifier)
report, err := operations.ExecuteOperation(env.OperationsBundle, deployProxyOp, deps, deployProxyInput{
WasmPath: req.WasmPath,
Salt: salt,
Owner: owner,
Cache: cacheRef.Address,
})
if err != nil {
return out, err
}
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(
"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
},
)
Loading