-
Notifications
You must be signed in to change notification settings - Fork 14
/
wallet.go
181 lines (162 loc) · 6.3 KB
/
wallet.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
package replication
import (
"context"
"crypto/rand"
"math/big"
"strconv"
"time"
"github.com/cockroachdb/errors"
"github.com/data-preservation-programs/singularity/model"
"github.com/data-preservation-programs/singularity/util"
logging "github.com/ipfs/go-log/v2"
"github.com/jellydator/ttlcache/v3"
"github.com/ybbus/jsonrpc/v3"
"gorm.io/gorm"
)
var logger = logging.Logger("replication")
type WalletChooser interface {
Choose(ctx context.Context, wallets []model.Wallet) (model.Wallet, error)
}
type RandomWalletChooser struct{}
var ErrNoWallet = errors.New("no wallets to choose from")
var ErrNoDatacap = errors.New("no wallets have enough datacap")
// Choose selects a random Wallet from the provided slice of Wallets.
//
// The Choose function of the RandomWalletChooser type randomly selects
// a Wallet from a given slice of Wallets. If the slice is empty, the function
// returns an error. It uses a cryptographically secure random number generator
// to make the selection.
//
// Parameters:
// - ctx context.Context: The context to use for cancellation and deadlines,
// although it is not used in this implementation.
// - wallets []model.Wallet: A slice of Wallet objects from which a random Wallet
// will be chosen.
//
// Returns:
// - model.Wallet: The randomly chosen Wallet object from the provided slice.
// - error: An error that will be returned if any issues were encountered while trying
// to choose a Wallet. This includes the case when the input slice is empty,
// in which case ErrNoWallet will be returned, or if there is an issue generating
// a random number.
func (w RandomWalletChooser) Choose(ctx context.Context, wallets []model.Wallet) (model.Wallet, error) {
// Check if the wallets slice is empty
if len(wallets) == 0 {
return model.Wallet{}, ErrNoWallet
}
randomPick, err := rand.Int(rand.Reader, big.NewInt(int64(len(wallets))))
if err != nil {
return model.Wallet{}, errors.WithStack(err)
}
chosenWallet := wallets[randomPick.Int64()]
return chosenWallet, nil
}
type DatacapWalletChooser struct {
db *gorm.DB
cache *ttlcache.Cache[string, int64]
lotusClient jsonrpc.RPCClient
min uint64
}
func NewDatacapWalletChooser(db *gorm.DB, cacheTTL time.Duration,
lotusAPI string, lotusToken string, min uint64) DatacapWalletChooser {
cache := ttlcache.New[string, int64](
ttlcache.WithTTL[string, int64](cacheTTL),
ttlcache.WithDisableTouchOnHit[string, int64]())
lotusClient := util.NewLotusClient(lotusAPI, lotusToken)
return DatacapWalletChooser{
db: db,
cache: cache,
lotusClient: lotusClient,
min: min,
}
}
func (w DatacapWalletChooser) getDatacap(ctx context.Context, wallet model.Wallet) (int64, error) {
var result string
err := w.lotusClient.CallFor(ctx, &result, "Filecoin.StateMarketBalance", wallet.Address, nil)
if err != nil {
return 0, errors.WithStack(err)
}
return strconv.ParseInt(result, 10, 64)
}
func (w DatacapWalletChooser) getDatacapCached(ctx context.Context, wallet model.Wallet) (int64, error) {
file := w.cache.Get(wallet.Address)
if file != nil && !file.IsExpired() {
return file.Value(), nil
}
datacap, err := w.getDatacap(ctx, wallet)
if err != nil {
logger.Errorf("failed to get datacap for wallet %s: %s", wallet.Address, err)
if file != nil {
return file.Value(), nil
}
return 0, errors.WithStack(err)
}
w.cache.Set(wallet.Address, datacap, ttlcache.DefaultTTL)
return datacap, nil
}
func (w DatacapWalletChooser) getPendingDeals(ctx context.Context, wallet model.Wallet) (int64, error) {
var totalPieceSize int64
err := w.db.WithContext(ctx).Model(&model.Deal{}).
Select("COALESCE(SUM(piece_size), 0)").
Where("client_id = ? AND verified AND state = ?", wallet.ID, model.DealProposed).
Scan(&totalPieceSize).
Error
if err != nil {
logger.Errorf("failed to get pending deals for wallet %s: %s", wallet.Address, err)
return 0, errors.WithStack(err)
}
return totalPieceSize, nil
}
// Choose selects a random Wallet from the provided slice of Wallets based on certain criteria.
//
// The Choose function of the DatacapWalletChooser type filters the given slice of Wallets
// based on a specific criterion, which is whether the datacap for the wallet minus
// the pending deals for the wallet is greater or equal to a minimum threshold (w.min).
// From the filtered eligible Wallets, the function then randomly selects one Wallet.
// It uses a cryptographically secure random number generator to make the selection.
// If the initial slice of Wallets is empty, or if no Wallets meet the criteria,
// the function returns an error.
//
// Parameters:
// - ctx context.Context: The context to use for cancellation and deadlines, used
// in the datacap and pending deals fetching operations.
// - wallets []model.Wallet: A slice of Wallet objects from which a random Wallet
// will be chosen based on the criteria.
//
// Returns:
// - model.Wallet: The randomly chosen Wallet object from the filtered eligible Wallets.
// - error: An error that will be returned if any issues were encountered while trying
// to choose a Wallet. This includes the case when the input slice is empty,
// in which case ErrNoWallet will be returned, when no Wallets meet the criteria,
// in which case ErrNoDatacap will be returned, or if there is an issue generating
// a random number.
func (w DatacapWalletChooser) Choose(ctx context.Context, wallets []model.Wallet) (model.Wallet, error) {
if len(wallets) == 0 {
return model.Wallet{}, ErrNoWallet
}
var eligibleWallets []model.Wallet
for _, wallet := range wallets {
datacap, err := w.getDatacapCached(ctx, wallet)
if err != nil {
logger.Errorw("failed to get datacap for wallet", "wallet", wallet.Address, "error", err)
continue
}
pendingDeals, err := w.getPendingDeals(ctx, wallet)
if err != nil {
logger.Errorw("failed to get pending deals for wallet", "wallet", wallet.Address, "error", err)
continue
}
if datacap-pendingDeals >= int64(w.min) {
eligibleWallets = append(eligibleWallets, wallet)
}
}
if len(eligibleWallets) == 0 {
return model.Wallet{}, ErrNoDatacap
}
randomPick, err := rand.Int(rand.Reader, big.NewInt(int64(len(eligibleWallets))))
if err != nil {
return model.Wallet{}, errors.WithStack(err)
}
chosenWallet := eligibleWallets[randomPick.Int64()]
return chosenWallet, nil
}