Skip to content

Commit 810c31a

Browse files
feat(utils): introduce merge sequence
Introduced ExecuteOnChainSequenceAndMerge util which originated from ccip tooling api [here](https://github.com/smartcontractkit/chainlink-ccip/blob/298ed1c38d5cda61a688beb2494a96a5409daf3d/deployment/utils/sequences/sequences.go#L46) This utils allows user to execute multiple sequence and accumulate their results into an object call OnChainOutput JIRA: https://smartcontract-it.atlassian.net/browse/CLD-2464
1 parent 7332038 commit 810c31a

3 files changed

Lines changed: 225 additions & 0 deletions

File tree

.changeset/strong-spiders-end.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"chainlink-deployments-framework": patch
3+
---
4+
5+
feat(utils): new ExecuteOnChainSequenceAndMerge util
6+
7+
Execute sequence and merge output into an aggregate OnChainOutput.

changeset/sequenceutils/merge.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package sequenceutils
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/smartcontractkit/chainlink-deployments-framework/deployment"
7+
"github.com/smartcontractkit/chainlink-deployments-framework/operations"
8+
)
9+
10+
// ExecuteOnChainSequenceAndMerge executes a sequence and merges the output into the given OnChainOutput.
11+
// On error, the accumulated agg is returned unchanged.
12+
func ExecuteOnChainSequenceAndMerge[IN any, DEP any](
13+
b operations.Bundle,
14+
deps DEP,
15+
seq *operations.Sequence[IN, OnChainOutput, DEP],
16+
input IN,
17+
agg OnChainOutput,
18+
) (OnChainOutput, error) {
19+
if seq == nil {
20+
return agg, fmt.Errorf("%w: sequence is required", deployment.ErrInvalidConfig)
21+
}
22+
report, err := operations.ExecuteSequence(b, seq, deps, input)
23+
if err != nil {
24+
return agg, fmt.Errorf("failed to execute %s: %w", seq.ID(), err)
25+
}
26+
if report.Output.Metadata.Env != nil && agg.Metadata.Env != nil &&
27+
report.Output.Metadata.Env != agg.Metadata.Env {
28+
return agg, fmt.Errorf("conflicting env metadata from sequence %s", seq.ID())
29+
}
30+
agg.BatchOps = append(agg.BatchOps, report.Output.BatchOps...)
31+
agg.Metadata.Addresses = append(agg.Metadata.Addresses, report.Output.Metadata.Addresses...)
32+
agg.Metadata.Contracts = append(agg.Metadata.Contracts, report.Output.Metadata.Contracts...)
33+
agg.Metadata.Chains = append(agg.Metadata.Chains, report.Output.Metadata.Chains...)
34+
if report.Output.Metadata.Env != nil {
35+
agg.Metadata.Env = report.Output.Metadata.Env
36+
}
37+
38+
return agg, nil
39+
}
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
package sequenceutils
2+
3+
import (
4+
"errors"
5+
"testing"
6+
7+
"github.com/Masterminds/semver/v3"
8+
mcms_types "github.com/smartcontractkit/mcms/types"
9+
"github.com/stretchr/testify/require"
10+
11+
"github.com/smartcontractkit/chainlink-deployments-framework/datastore"
12+
"github.com/smartcontractkit/chainlink-deployments-framework/deployment"
13+
"github.com/smartcontractkit/chainlink-deployments-framework/operations"
14+
)
15+
16+
func TestExecuteOnChainSequenceAndMerge_success(t *testing.T) {
17+
t.Parallel()
18+
19+
env := testEnvironment(t)
20+
seq := testSequence(t, func(_ operations.Bundle, _ struct{}, _ struct{}) (OnChainOutput, error) {
21+
return OnChainOutput{
22+
BatchOps: []mcms_types.BatchOperation{sampleBatchOp()},
23+
Metadata: datastore.MetadataBundle{
24+
Addresses: []datastore.AddressRef{{
25+
Address: "0xabc",
26+
ChainSelector: 1,
27+
Type: "Timelock",
28+
Version: semver.MustParse("1.0.0"),
29+
}},
30+
Chains: []datastore.ChainMetadata{{ChainSelector: 1, Metadata: "chain-a"}},
31+
},
32+
}, nil
33+
})
34+
35+
agg, err := ExecuteOnChainSequenceAndMerge(env.OperationsBundle, struct{}{}, seq, struct{}{}, OnChainOutput{})
36+
require.NoError(t, err)
37+
require.Len(t, agg.BatchOps, 1)
38+
require.Len(t, agg.Metadata.Addresses, 1)
39+
require.Len(t, agg.Metadata.Chains, 1)
40+
}
41+
42+
func TestExecuteOnChainSequenceAndMerge_preservesAggOnExecuteFailure(t *testing.T) {
43+
t.Parallel()
44+
45+
env := testEnvironment(t)
46+
seqErr := errors.New("sequence failed")
47+
48+
okSeq := testSequence(t, func(_ operations.Bundle, _ struct{}, _ struct{}) (OnChainOutput, error) {
49+
return OnChainOutput{
50+
Metadata: datastore.MetadataBundle{
51+
Addresses: []datastore.AddressRef{{
52+
Address: "0xabc",
53+
ChainSelector: 1,
54+
Type: "Timelock",
55+
Version: semver.MustParse("1.0.0"),
56+
}},
57+
},
58+
}, nil
59+
})
60+
failSeq := operations.NewSequence(
61+
"test-seq-fail",
62+
semver.MustParse("1.0.0"),
63+
"test sequence fail",
64+
func(_ operations.Bundle, _ struct{}, _ struct{}) (OnChainOutput, error) {
65+
return OnChainOutput{}, seqErr
66+
},
67+
)
68+
69+
agg, err := ExecuteOnChainSequenceAndMerge(env.OperationsBundle, struct{}{}, okSeq, struct{}{}, OnChainOutput{})
70+
require.NoError(t, err)
71+
require.Len(t, agg.Metadata.Addresses, 1)
72+
73+
agg, err = ExecuteOnChainSequenceAndMerge(env.OperationsBundle, struct{}{}, failSeq, struct{}{}, agg)
74+
require.Error(t, err)
75+
require.ErrorContains(t, err, seqErr.Error())
76+
require.Len(t, agg.Metadata.Addresses, 1)
77+
}
78+
79+
func TestExecuteOnChainSequenceAndMerge_appendsChainsWithoutDeduping(t *testing.T) {
80+
t.Parallel()
81+
82+
env := testEnvironment(t)
83+
seq := testSequence(t, func(_ operations.Bundle, _ struct{}, _ struct{}) (OnChainOutput, error) {
84+
return OnChainOutput{
85+
Metadata: datastore.MetadataBundle{
86+
Chains: []datastore.ChainMetadata{{ChainSelector: 1, Metadata: "a"}},
87+
},
88+
}, nil
89+
})
90+
91+
agg := OnChainOutput{}
92+
agg, err := ExecuteOnChainSequenceAndMerge(env.OperationsBundle, struct{}{}, seq, struct{}{}, agg)
93+
require.NoError(t, err)
94+
agg, err = ExecuteOnChainSequenceAndMerge(env.OperationsBundle, struct{}{}, seq, struct{}{}, agg)
95+
require.NoError(t, err)
96+
require.Len(t, agg.Metadata.Chains, 2)
97+
require.Equal(t, uint64(1), agg.Metadata.Chains[0].ChainSelector)
98+
require.Equal(t, uint64(1), agg.Metadata.Chains[1].ChainSelector)
99+
}
100+
101+
func TestExecuteOnChainSequenceAndMerge_nilSequence(t *testing.T) {
102+
t.Parallel()
103+
104+
env := testEnvironment(t)
105+
agg := OnChainOutput{Metadata: datastore.MetadataBundle{
106+
Addresses: []datastore.AddressRef{{Address: "0xabc", ChainSelector: 1, Type: "Timelock", Version: semver.MustParse("1.0.0")}},
107+
}}
108+
109+
out, err := ExecuteOnChainSequenceAndMerge(env.OperationsBundle, struct{}{}, nil, struct{}{}, agg)
110+
require.Error(t, err)
111+
require.ErrorIs(t, err, deployment.ErrInvalidConfig)
112+
require.ErrorContains(t, err, "sequence is required")
113+
require.Equal(t, agg, out)
114+
}
115+
116+
func TestExecuteOnChainSequenceAndMerge_sameEnvPointer(t *testing.T) {
117+
t.Parallel()
118+
119+
env := testEnvironment(t)
120+
envMeta := &datastore.EnvMetadata{Metadata: "shared"}
121+
seq1 := operations.NewSequence(
122+
"test-seq-env-shared-1",
123+
semver.MustParse("1.0.0"),
124+
"test sequence shared env 1",
125+
func(_ operations.Bundle, _ struct{}, _ struct{}) (OnChainOutput, error) {
126+
return OnChainOutput{Metadata: datastore.MetadataBundle{Env: envMeta}}, nil
127+
},
128+
)
129+
seq2 := operations.NewSequence(
130+
"test-seq-env-shared-2",
131+
semver.MustParse("1.0.0"),
132+
"test sequence shared env 2",
133+
func(_ operations.Bundle, _ struct{}, _ struct{}) (OnChainOutput, error) {
134+
return OnChainOutput{Metadata: datastore.MetadataBundle{Env: envMeta}}, nil
135+
},
136+
)
137+
138+
agg, err := ExecuteOnChainSequenceAndMerge(env.OperationsBundle, struct{}{}, seq1, struct{}{}, OnChainOutput{})
139+
require.NoError(t, err)
140+
require.Same(t, envMeta, agg.Metadata.Env)
141+
142+
agg, err = ExecuteOnChainSequenceAndMerge(env.OperationsBundle, struct{}{}, seq2, struct{}{}, agg)
143+
require.NoError(t, err)
144+
require.Same(t, envMeta, agg.Metadata.Env)
145+
}
146+
147+
func TestExecuteOnChainSequenceAndMerge_envConflict(t *testing.T) {
148+
t.Parallel()
149+
150+
env := testEnvironment(t)
151+
envMeta := &datastore.EnvMetadata{Metadata: "staging"}
152+
seq := operations.NewSequence(
153+
"test-seq-env-conflict",
154+
semver.MustParse("1.0.0"),
155+
"test sequence env conflict",
156+
func(_ operations.Bundle, _ struct{}, _ struct{}) (OnChainOutput, error) {
157+
return OnChainOutput{
158+
BatchOps: []mcms_types.BatchOperation{sampleBatchOp()},
159+
Metadata: datastore.MetadataBundle{
160+
Env: envMeta,
161+
Addresses: []datastore.AddressRef{{
162+
Address: "0xdef",
163+
ChainSelector: 2,
164+
Type: "Timelock",
165+
Version: semver.MustParse("1.0.0"),
166+
}},
167+
},
168+
}, nil
169+
},
170+
)
171+
172+
agg := OnChainOutput{Metadata: datastore.MetadataBundle{Env: &datastore.EnvMetadata{Metadata: "prod"}}}
173+
agg, err := ExecuteOnChainSequenceAndMerge(env.OperationsBundle, struct{}{}, seq, struct{}{}, agg)
174+
require.Error(t, err)
175+
require.ErrorContains(t, err, "conflicting env metadata")
176+
require.Equal(t, "prod", agg.Metadata.Env.Metadata)
177+
require.Empty(t, agg.BatchOps)
178+
require.Empty(t, agg.Metadata.Addresses)
179+
}

0 commit comments

Comments
 (0)