/
manager.go
245 lines (203 loc) · 7.89 KB
/
manager.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
package bindings
import (
"context"
"fmt"
"strings"
"sync"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/unpackdev/solgo/clients"
"github.com/unpackdev/solgo/standards"
"github.com/unpackdev/solgo/utils"
)
// Manager acts as a central registry for smart contract bindings. It enables the configuration, management, and
// interaction with smart contracts across different networks. The Manager maintains a client pool for network
// communications, a context for managing lifecycle events, and a mutex for thread-safe operation.
type Manager struct {
ctx context.Context // The context for managing the lifecycle of network requests.
clientPool *clients.ClientPool // A pool of blockchain network clients for executing RPC calls.
bindings map[utils.Network]map[BindingType]*Binding // A nested map storing contract bindings by network and type.
mu sync.RWMutex // A read/write mutex for thread-safe access to the bindings map.
}
// NewManager creates a new Manager instance with a specified context and client pool. It ensures that the contract
// standards are loaded before initialization. This constructor is suitable for production use where interaction with
// real network clients is required.
func NewManager(ctx context.Context, clientPool *clients.ClientPool) (*Manager, error) {
if !standards.StandardsLoaded() {
if err := standards.LoadStandards(); err != nil {
return nil, fmt.Errorf("failed to load standards: %w", err)
}
}
return &Manager{
ctx: ctx,
clientPool: clientPool,
bindings: make(map[utils.Network]map[BindingType]*Binding),
}, nil
}
// RegisterBinding adds a new contract binding to the Manager. It processes the contract's ABI and initializes
// a Binding struct, ensuring that each binding is uniquely registered within its network context.
func (m *Manager) RegisterBinding(network utils.Network, networkID utils.NetworkID, name BindingType, address common.Address, rawABI string) (*Binding, error) {
parsedABI, err := abi.JSON(strings.NewReader(rawABI))
if err != nil {
return nil, err
}
binding := &Binding{
network: network,
networkID: networkID,
Type: name,
Address: address,
RawABI: rawABI,
ABI: &parsedABI,
}
m.mu.RLock()
if _, ok := m.bindings[network]; !ok {
m.bindings[network] = make(map[BindingType]*Binding)
}
m.mu.RUnlock()
// We don't want to overwrite existing bindings and we don't want to register the same binding twice
if !m.BindingExist(network, name) {
m.bindings[network][name] = binding
}
return binding, nil
}
// GetClient returns the client pool associated with the Manager, allowing for direct network interactions.
func (m *Manager) GetClient() *clients.ClientPool {
return m.clientPool
}
// GetBinding retrieves a specific contract binding by its network and type, enabling contract interactions.
func (m *Manager) GetBinding(network utils.Network, name BindingType) (*Binding, error) {
m.mu.RLock()
defer m.mu.RUnlock()
if networkBindings, ok := m.bindings[network]; ok {
if binding, ok := networkBindings[name]; ok {
return binding, nil
}
}
return nil, fmt.Errorf("binding %s not found", name)
}
// GetBindings returns all bindings registered under a specific network, providing a comprehensive overview of
// available contract interactions.
func (m *Manager) GetBindings(network utils.Network) map[BindingType]*Binding {
m.mu.RLock()
defer m.mu.RUnlock()
return m.bindings[network]
}
// BindingExist checks whether a specific binding exists within the Manager, aiding in conditional logic and
// binding management.
func (m *Manager) BindingExist(network utils.Network, name BindingType) bool {
m.mu.RLock()
defer m.mu.RUnlock()
if networkBindings, ok := m.bindings[network]; ok {
if _, ok := networkBindings[name]; ok {
return true
}
}
return false
}
// WatchEvents establishes a subscription to listen for specific contract events, facilitating real-time
// application responses to on-chain activities.
func (m *Manager) WatchEvents(network utils.Network, bindingType BindingType, eventName string, ch chan<- types.Log) (ethereum.Subscription, error) {
m.mu.RLock()
defer m.mu.RUnlock()
binding, ok := m.bindings[network][bindingType]
if !ok {
return nil, fmt.Errorf("binding %s not found for network %s", bindingType, network)
}
query := ethereum.FilterQuery{
Addresses: []common.Address{binding.GetAddress()},
}
event, ok := binding.ABI.Events[eventName]
if !ok {
return nil, fmt.Errorf("event %s not found in ABI", eventName)
}
query.Topics = [][]common.Hash{{event.ID}}
client := m.clientPool.GetClientByGroup(network.String())
if client == nil {
return nil, fmt.Errorf("client not found for network %s", network)
}
return client.SubscribeFilterLogs(context.Background(), query, ch)
}
// CallContractMethod executes a method call on a smart contract, handling the data packing, RPC call execution,
// and results unpacking.
func (m *Manager) CallContractMethod(ctx context.Context, network utils.Network, bindingType BindingType, toAddr common.Address, methodName string, params ...interface{}) (any, error) {
m.mu.RLock()
defer m.mu.RUnlock()
binding, ok := m.bindings[network][bindingType]
if !ok {
return nil, fmt.Errorf("binding %s not found for network %s", bindingType, network)
}
method, ok := binding.ABI.Methods[methodName]
if !ok {
return nil, fmt.Errorf("binding %s method %s not found in ABI", bindingType, methodName)
}
data, err := method.Inputs.Pack(params...)
if err != nil {
return nil, err
}
destinationAddr := toAddr
if destinationAddr == utils.ZeroAddress {
destinationAddr = binding.Address
}
callMsg := ethereum.CallMsg{
To: &destinationAddr,
Data: append(method.ID, data...),
}
var result []byte
client := m.clientPool.GetClientByGroup(network.String())
if client == nil {
return nil, fmt.Errorf("client not found for network %s", network)
}
result, err = client.CallContract(context.Background(), callMsg, nil)
if err != nil {
return nil, fmt.Errorf("failed to call contract: %w", err)
}
var unpackedResults any
err = binding.ABI.UnpackIntoInterface(&unpackedResults, methodName, result)
if err != nil {
return nil, fmt.Errorf("failed to unpack results: %w", err)
}
return unpackedResults, nil
}
// CallContractMethodUnpackMap executes a contract method call and unpacks the results into a map, providing
// a flexible interface for handling contract outputs.
func (m *Manager) CallContractMethodUnpackMap(ctx context.Context, network utils.Network, bindingType BindingType, toAddr common.Address, methodName string, params ...interface{}) (map[string]any, error) {
m.mu.RLock()
defer m.mu.RUnlock()
binding, ok := m.bindings[network][bindingType]
if !ok {
return nil, fmt.Errorf("binding %s not found for network %s", bindingType, network)
}
method, ok := binding.ABI.Methods[methodName]
if !ok {
return nil, fmt.Errorf("binding %s method %s not found in ABI", bindingType, methodName)
}
data, err := method.Inputs.Pack(params...)
if err != nil {
return nil, err
}
destinationAddr := toAddr
if destinationAddr == utils.ZeroAddress {
destinationAddr = binding.Address
}
callMsg := ethereum.CallMsg{
To: &destinationAddr,
Data: append(method.ID, data...),
}
var result []byte
client := m.clientPool.GetClientByGroup(network.String())
if client == nil {
return nil, fmt.Errorf("client not found for network %s", network)
}
result, err = client.CallContract(context.Background(), callMsg, nil)
if err != nil {
return nil, fmt.Errorf("failed to call contract: %w", err)
}
unpackedResults := map[string]any{}
err = binding.ABI.UnpackIntoMap(unpackedResults, methodName, result)
if err != nil {
return nil, fmt.Errorf("failed to unpack results: %w", err)
}
return unpackedResults, nil
}