decred.org/dcrdex@v1.0.5/tatanka/chain/utxo/dcr.go (about) 1 // This code is available on the terms of the project LICENSE.md file, 2 // also available online at https://blueoakcouncil.org/license/1.0.0. 3 4 package utxo 5 6 import ( 7 "context" 8 "encoding/json" 9 "errors" 10 "fmt" 11 "math" 12 "os" 13 "sync" 14 "sync/atomic" 15 "time" 16 17 "decred.org/dcrdex/dex" 18 "decred.org/dcrdex/tatanka/chain" 19 "decred.org/dcrdex/tatanka/tanka" 20 "github.com/decred/dcrd/chaincfg/chainhash" 21 "github.com/decred/dcrd/dcrutil/v4" 22 chainjson "github.com/decred/dcrd/rpc/jsonrpc/types/v4" 23 "github.com/decred/dcrd/rpcclient/v8" 24 "github.com/decred/dcrd/wire" 25 ) 26 27 const ( 28 ChainID = 42 29 ) 30 31 var ( 32 compatibleNodeRPCVersions = []dex.Semver{ 33 {Major: 8, Minor: 0, Patch: 0}, // 1.8-pre, just dropped unused ticket RPCs 34 {Major: 7, Minor: 0, Patch: 0}, // 1.7 release, new gettxout args 35 } 36 ) 37 38 func init() { 39 chain.RegisterChainConstructor(42, NewDecred) 40 } 41 42 type DecredConfigFile struct { 43 RPCUser string `json:"rpcuser"` 44 RPCPass string `json:"rpcpass"` 45 RPCListen string `json:"rpclisten"` 46 RPCCert string `json:"rpccert"` 47 NodeRelay string `json:"nodeRelay"` 48 } 49 50 type decredChain struct { 51 cfg *DecredConfigFile 52 net dex.Network 53 log dex.Logger 54 fees chan uint64 55 56 cl *rpcclient.Client 57 connected atomic.Bool 58 } 59 60 func NewDecred(rawConfig json.RawMessage, log dex.Logger, net dex.Network) (chain.Chain, error) { 61 var cfg DecredConfigFile 62 if err := json.Unmarshal(rawConfig, &cfg); err != nil { 63 return nil, fmt.Errorf("error parsing configuration: %w", err) 64 } 65 return &decredChain{ 66 cfg: &cfg, 67 net: net, 68 log: log, 69 fees: make(chan uint64, 1), 70 }, nil 71 } 72 73 func (c *decredChain) Connect(ctx context.Context) (_ *sync.WaitGroup, err error) { 74 cfg := c.cfg 75 if cfg.NodeRelay == "" { 76 c.cl, err = connectNodeRPC(cfg.RPCListen, cfg.RPCUser, cfg.RPCPass, cfg.RPCCert) 77 } else { 78 c.cl, err = connectLocalHTTP(cfg.NodeRelay, cfg.RPCUser, cfg.RPCPass) 79 } 80 if err != nil { 81 return nil, fmt.Errorf("error connecting RPC client: %w", err) 82 } 83 84 if err = c.initialize(ctx); err != nil { 85 return nil, err 86 } 87 88 c.connected.Store(true) 89 defer c.connected.Store(false) 90 91 var wg sync.WaitGroup 92 wg.Add(1) 93 go func() { 94 defer wg.Done() 95 c.monitorFees(ctx) 96 }() 97 98 return &wg, nil 99 } 100 101 func (c *decredChain) initialize(ctx context.Context) error { 102 net, err := c.cl.GetCurrentNet(ctx) 103 if err != nil { 104 return fmt.Errorf("getcurrentnet failure: %w", err) 105 } 106 var wantCurrencyNet wire.CurrencyNet 107 switch c.net { 108 case dex.Testnet: 109 wantCurrencyNet = wire.TestNet3 110 case dex.Mainnet: 111 wantCurrencyNet = wire.MainNet 112 case dex.Regtest: // dex.Simnet 113 wantCurrencyNet = wire.SimNet 114 } 115 if net != wantCurrencyNet { 116 return fmt.Errorf("wrong net %v", net.String()) 117 } 118 119 // Check the required API versions. 120 versions, err := c.cl.Version(ctx) 121 if err != nil { 122 return fmt.Errorf("DCR node version fetch error: %w", err) 123 } 124 125 ver, exists := versions["dcrdjsonrpcapi"] 126 if !exists { 127 return fmt.Errorf("dcrd.Version response missing 'dcrdjsonrpcapi'") 128 } 129 nodeSemver := dex.NewSemver(ver.Major, ver.Minor, ver.Patch) 130 if !dex.SemverCompatibleAny(compatibleNodeRPCVersions, nodeSemver) { 131 return fmt.Errorf("dcrd has an incompatible JSON-RPC version %s, require one of %s", 132 nodeSemver, compatibleNodeRPCVersions) 133 } 134 135 // Verify dcrd has tx index enabled (required for getrawtransaction). 136 info, err := c.cl.GetInfo(ctx) 137 if err != nil { 138 return fmt.Errorf("dcrd getinfo check failed: %w", err) 139 } 140 if !info.TxIndex { 141 return errors.New("dcrd does not have transaction index enabled (specify --txindex)") 142 } 143 144 // Prime the cache with the best block. 145 tip, err := c.cl.GetBestBlockHash(ctx) 146 if err != nil { 147 return fmt.Errorf("error getting best block from dcrd: %w", err) 148 } 149 if tip == nil { 150 return fmt.Errorf("nil best block hash?") 151 } 152 // if bestHash != nil { 153 // _, err := dcr.getDcrBlock(ctx, bestHash) 154 // if err != nil { 155 // return nil, fmt.Errorf("error priming the cache: %w", err) 156 // } 157 // } 158 159 // if _, err = c.cl.FeeRate(ctx); err != nil { 160 // c.log.Warnf("Decred backend started without fee estimation available: %v", err) 161 // } 162 return nil 163 } 164 165 type query struct { 166 Method string `json:"method"` 167 Args []json.RawMessage `json:"args"` 168 } 169 170 // func (c *decredChain) Query(ctx context.Context, rawQuery chain.Query) (chain.Result, error) { 171 // var q query 172 // if err := json.Unmarshal(rawQuery, &q); err != nil { 173 // return nil, chain.BadQueryError(fmt.Errorf("error parsing raw query: %w", err)) 174 // } 175 176 // if q.Method == "" { 177 // return nil, chain.BadQueryError(errors.New("invalid query parameters. no method")) 178 // } 179 180 // res, err := c.cl.RawRequest(ctx, q.Method, q.Args) 181 // if err != nil { 182 // // Could potentially try to parse certain errors here 183 184 // return nil, fmt.Errorf("error performing query: %w", err) 185 // } 186 187 // return chain.Result(res), nil 188 // } 189 190 func (c *decredChain) Connected() bool { 191 return c.connected.Load() 192 } 193 194 func (c *decredChain) FeeChannel() <-chan uint64 { 195 return c.fees 196 } 197 198 func (c *decredChain) monitorFees(ctx context.Context) { 199 tick := time.NewTicker(feeMonitorTick) 200 var tip *chainhash.Hash 201 for { 202 select { 203 case <-tick.C: 204 case <-ctx.Done(): 205 return 206 } 207 208 newTip, err := c.cl.GetBestBlockHash(ctx) 209 if err != nil { 210 c.connected.Store(false) 211 c.log.Errorf("Decred is not connected: %w", err) 212 continue 213 } 214 if newTip == nil { // sanity check 215 c.log.Error("nil tip hash?") 216 continue 217 } 218 if tip != nil && *tip == *newTip { 219 continue 220 } 221 tip = newTip 222 c.connected.Store(true) 223 // estimatesmartfee 1 returns extremely high rates on DCR. 224 estimateFeeResult, err := c.cl.EstimateSmartFee(ctx, 2, chainjson.EstimateSmartFeeConservative) 225 if err != nil { 226 c.log.Errorf("estimatesmartfee error: %w", err) 227 continue 228 } 229 atomsPerKB, err := dcrutil.NewAmount(estimateFeeResult.FeeRate) 230 if err != nil { 231 c.log.Errorf("NewAmount error: %w", err) 232 continue 233 } 234 atomsPerB := uint64(math.Round(float64(atomsPerKB) / 1000)) 235 if atomsPerB == 0 { 236 atomsPerB = 1 237 } 238 select { 239 case c.fees <- atomsPerB: 240 case <-time.After(time.Second * 5): 241 c.log.Errorf("fee channel is blocking") 242 } 243 } 244 } 245 246 func (c *decredChain) CheckBond(b *tanka.Bond) error { 247 248 // TODO: Validate bond 249 250 return nil 251 } 252 253 func (c *decredChain) AuditHTLC(*tanka.HTLCAudit) (bool, error) { 254 255 // TODO: Perform the audit 256 257 return true, nil 258 } 259 260 // connectNodeRPC attempts to create a new websocket connection to a dcrd node 261 // with the given credentials and notification handlers. 262 func connectNodeRPC(host, user, pass, cert string) (*rpcclient.Client, error) { 263 dcrdCerts, err := os.ReadFile(cert) 264 if err != nil { 265 return nil, fmt.Errorf("TLS certificate read error: %w", err) 266 } 267 268 config := &rpcclient.ConnConfig{ 269 Host: host, 270 Endpoint: "ws", // websocket 271 User: user, 272 Pass: pass, 273 Certificates: dcrdCerts, 274 } 275 276 dcrdClient, err := rpcclient.New(config, nil) 277 if err != nil { 278 return nil, fmt.Errorf("failed to start dcrd RPC client: %w", err) 279 } 280 281 return dcrdClient, nil 282 } 283 284 func connectLocalHTTP(host, user, pass string) (*rpcclient.Client, error) { 285 config := &rpcclient.ConnConfig{ 286 Host: host, 287 HTTPPostMode: true, 288 DisableTLS: true, 289 User: user, 290 Pass: pass, 291 } 292 293 dcrdClient, err := rpcclient.New(config, nil) 294 if err != nil { 295 return nil, fmt.Errorf("failed to start dcrd RPC client: %w", err) 296 } 297 298 return dcrdClient, nil 299 }