@@ -42,12 +42,15 @@ import (
4242 "gopkg.in/yaml.v2"
4343)
4444
45+ type SyncAddressCallback func (context.Context , ethtypes.Address0xHex ) error
46+
4547// Wallet is a directory containing a set of KeystoreV3 files, conforming
4648// to the ethsigner.Wallet interface and providing notifications when new
4749// keys are added to the wallet (via FS listener).
4850type Wallet interface {
4951 ethsigner.WalletTypedData
5052 GetWalletFile (ctx context.Context , addr ethtypes.Address0xHex ) (keystorev3.WalletFile , error )
53+ SetSyncAddressCallback (SyncAddressCallback )
5154 AddListener (listener chan <- ethtypes.Address0xHex )
5255}
5356
@@ -99,6 +102,7 @@ type fsWallet struct {
99102 metadataKeyFileProperty * template.Template
100103 metadataPasswordFileProperty * template.Template
101104 primaryMatchRegex * regexp.Regexp
105+ syncAddressCallback SyncAddressCallback
102106
103107 mux sync.Mutex
104108 addressToFileMap map [ethtypes.Address0xHex ]string // map for lookup to filename
@@ -139,12 +143,29 @@ func (w *fsWallet) Initialize(ctx context.Context) error {
139143 return w .Refresh (ctx )
140144}
141145
146+ // Asynchronously listen for all addresses as they are detected - during startup, or after startup
142147func (w * fsWallet ) AddListener (listener chan <- ethtypes.Address0xHex ) {
143148 w .mux .Lock ()
144149 defer w .mux .Unlock ()
145150 w .listeners = append (w .listeners , listener )
146151}
147152
153+ // As an alternative to registering a listener are able to supply a single *synchronous* callback
154+ // that will process all of the addresses that exist on-disk at the time of refresh in-line.
155+ // This is very useful if you want to be sure your application using this module does not advertise it
156+ // is available until it has built a lookup map for all of the files that existed before it started.
157+ //
158+ // This will (by definition) delay initialize/refresh while that processing happens.
159+ //
160+ // Note there is no guarantee that this callback will be called on a single go-routine, as no
161+ // locks are held while calling it. So if refresh is called on two goroutines concurrently, it
162+ // will be called concurrently. However, it is guaranteed to be called in-line with the
163+ // initialize/refresh so you know once that calls returns all new keys detected by it have
164+ // driven the callback.
165+ func (w * fsWallet ) SetSyncAddressCallback (callback SyncAddressCallback ) {
166+ w .syncAddressCallback = callback
167+ }
168+
148169// GetAccounts returns the currently cached list of known addresses
149170func (w * fsWallet ) GetAccounts (_ context.Context ) ([]* ethtypes.Address0xHex , error ) {
150171 w .mux .Lock ()
@@ -197,17 +218,14 @@ func (w *fsWallet) Refresh(ctx context.Context) error {
197218 files = append (files , fi )
198219 }
199220 }
200- if len (files ) > 0 {
201- w .notifyNewFiles (ctx , files ... )
202- }
203- return nil
221+ return w .notifyNewFiles (ctx , files ... )
204222}
205223
206- func (w * fsWallet ) notifyNewFiles (ctx context.Context , files ... fs.FileInfo ) {
224+ func (w * fsWallet ) processNewFiles (ctx context.Context , files ... fs.FileInfo ) ( listeners [] chan <- ethtypes. Address0xHex , newAddresses [] * ethtypes. Address0xHex ) {
207225 // Lock now we have the list
208226 w .mux .Lock ()
209227 defer w .mux .Unlock ()
210- newAddresses : = make ([]* ethtypes.Address0xHex , 0 )
228+ newAddresses = make ([]* ethtypes.Address0xHex , 0 )
211229 for _ , f := range files {
212230 addr := w .matchFilename (ctx , f )
213231 if addr != nil {
@@ -221,17 +239,43 @@ func (w *fsWallet) notifyNewFiles(ctx context.Context, files ...fs.FileInfo) {
221239 }
222240 }
223241 }
224- listeners : = make ([]chan <- ethtypes.Address0xHex , len (w .listeners ))
242+ listeners = make ([]chan <- ethtypes.Address0xHex , len (w .listeners ))
225243 copy (listeners , w .listeners )
226244 log .L (ctx ).Debugf ("Processed %d files. Found %d new addresses" , len (files ), len (newAddresses ))
227- // Avoid holding the lock while calling the listeners, by using a go-routine
228- go func () {
229- for _ , l := range w .listeners {
245+ return listeners , newAddresses
246+ }
247+
248+ func (w * fsWallet ) notifyNewFiles (ctx context.Context , files ... fs.FileInfo ) error {
249+
250+ // This function takes the lock and releases with a copy of the listeners, and a list of new addresses
251+ listeners , newAddresses := w .processNewFiles (ctx , files ... )
252+
253+ if len (newAddresses ) > 0 {
254+
255+ if w .syncAddressCallback != nil {
256+ // Sync callbacks are called here in-line, but outside the lock.
230257 for _ , addr := range newAddresses {
231- l <- * addr
258+ if err := w .syncAddressCallback (ctx , * addr ); err != nil {
259+ log .L (ctx ).Errorf ("sync listener returned error for address %s: %s" , addr , err )
260+ return err
261+ }
232262 }
233263 }
234- }()
264+
265+ if len (listeners ) > 0 {
266+ // Avoid any blocking of this routine using a separate go-routine that will deliver async callbacks
267+ go func () {
268+ for _ , l := range listeners {
269+ for _ , addr := range newAddresses {
270+ l <- * addr
271+ }
272+ }
273+ }()
274+ }
275+
276+ }
277+
278+ return nil
235279}
236280
237281func (w * fsWallet ) Close () error {
0 commit comments