decred.org/dcrdex@v1.0.5/client/asset/dcr/dcr_test.go (about) 1 //go:build !harness && !vspd 2 3 package dcr 4 5 import ( 6 "bytes" 7 "context" 8 "crypto/sha256" 9 "encoding/hex" 10 "encoding/json" 11 "errors" 12 "fmt" 13 "math" 14 "math/rand" 15 "os" 16 "reflect" 17 "sort" 18 "strings" 19 "sync" 20 "sync/atomic" 21 "testing" 22 "time" 23 24 "decred.org/dcrdex/client/asset" 25 "decred.org/dcrdex/dex" 26 "decred.org/dcrdex/dex/calc" 27 "decred.org/dcrdex/dex/config" 28 "decred.org/dcrdex/dex/encode" 29 dexdcr "decred.org/dcrdex/dex/networks/dcr" 30 "decred.org/dcrwallet/v5/rpc/client/dcrwallet" 31 walletjson "decred.org/dcrwallet/v5/rpc/jsonrpc/types" 32 "github.com/decred/dcrd/chaincfg/chainhash" 33 "github.com/decred/dcrd/chaincfg/v3" 34 "github.com/decred/dcrd/dcrec" 35 "github.com/decred/dcrd/dcrec/secp256k1/v4" 36 "github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa" 37 "github.com/decred/dcrd/dcrjson/v4" 38 "github.com/decred/dcrd/dcrutil/v4" 39 "github.com/decred/dcrd/gcs/v4" 40 "github.com/decred/dcrd/gcs/v4/blockcf2" 41 chainjson "github.com/decred/dcrd/rpc/jsonrpc/types/v4" 42 "github.com/decred/dcrd/txscript/v4" 43 "github.com/decred/dcrd/txscript/v4/stdaddr" 44 "github.com/decred/dcrd/wire" 45 ) 46 47 var ( 48 tLogger dex.Logger 49 tCtx context.Context 50 tLotSize uint64 = 1e7 51 tRateStep uint64 = 100 52 tDCR = &dex.Asset{ 53 ID: 42, 54 Symbol: "dcr", 55 Version: version, 56 MaxFeeRate: 24, // FundOrder and swap/redeem fallback when estimation fails 57 SwapConf: 1, 58 } 59 optimalFeeRate uint64 = 22 60 tErr = fmt.Errorf("test error") 61 tTxID = "308e9a3675fc3ea3862b7863eeead08c621dcc37ff59de597dd3cdab41450ad9" 62 tTxHash *chainhash.Hash 63 tPKHAddr stdaddr.Address 64 tP2PKHScript []byte 65 tChainParams = chaincfg.MainNetParams() 66 feeSuggestion uint64 = 10 67 tAcctName = "tAcctName" 68 ) 69 70 func randBytes(l int) []byte { 71 b := make([]byte, l) 72 rand.Read(b) 73 return b 74 } 75 76 func makeGetTxOutRes(confs, lots int64, pkScript []byte) *chainjson.GetTxOutResult { 77 val := dcrutil.Amount(lots * int64(tLotSize)).ToCoin() 78 return &chainjson.GetTxOutResult{ 79 Confirmations: confs, 80 Value: val, 81 ScriptPubKey: chainjson.ScriptPubKeyResult{ 82 Hex: hex.EncodeToString(pkScript), 83 }, 84 } 85 } 86 87 func makeRawTx(inputs []*wire.TxIn, outputScripts []dex.Bytes) *wire.MsgTx { 88 tx := wire.NewMsgTx() 89 for _, pkScript := range outputScripts { 90 tx.TxOut = append(tx.TxOut, &wire.TxOut{ 91 PkScript: pkScript, 92 }) 93 } 94 tx.TxIn = inputs 95 return tx 96 } 97 98 func makeTxHex(inputs []*wire.TxIn, pkScripts []dex.Bytes) (string, error) { 99 msgTx := wire.NewMsgTx() 100 msgTx.TxIn = inputs 101 for _, pkScript := range pkScripts { 102 txOut := wire.NewTxOut(100000000, pkScript) 103 msgTx.AddTxOut(txOut) 104 } 105 txBuf := bytes.NewBuffer(make([]byte, 0, msgTx.SerializeSize())) 106 err := msgTx.Serialize(txBuf) 107 if err != nil { 108 return "", err 109 } 110 return hex.EncodeToString(txBuf.Bytes()), nil 111 } 112 113 func makeRPCVin(txHash *chainhash.Hash, vout uint32, sigScript []byte) *wire.TxIn { 114 return &wire.TxIn{ 115 PreviousOutPoint: *wire.NewOutPoint(txHash, vout, 0), 116 SignatureScript: sigScript, 117 } 118 } 119 120 func newTxOutResult(script []byte, value uint64, confs int64) *chainjson.GetTxOutResult { 121 return &chainjson.GetTxOutResult{ 122 Confirmations: confs, 123 Value: float64(value) / 1e8, 124 ScriptPubKey: chainjson.ScriptPubKeyResult{ 125 Hex: hex.EncodeToString(script), 126 }, 127 } 128 } 129 130 func dummyInput() *wire.TxIn { 131 return wire.NewTxIn(wire.NewOutPoint(&chainhash.Hash{0x01}, 0, 0), 0, nil) 132 } 133 134 func dummyTx() *wire.MsgTx { 135 return makeRawTx([]*wire.TxIn{dummyInput()}, []dex.Bytes{}) 136 } 137 138 // sometimes we want to not start monitor blocks to avoid a race condition in 139 // case we replace the wallet. 140 func tNewWalletMonitorBlocks(monitorBlocks bool) (*ExchangeWallet, *tRPCClient, func()) { 141 client := newTRPCClient() 142 log := tLogger.SubLogger("trpc") 143 walletCfg := &asset.WalletConfig{ 144 Emit: asset.NewWalletEmitter(client.emitC, BipID, log), 145 PeersChange: func(uint32, error) {}, 146 } 147 walletCtx, shutdown := context.WithCancel(tCtx) 148 149 wallet, err := unconnectedWallet(walletCfg, &walletConfig{}, tChainParams, tLogger, dex.Simnet) 150 if err != nil { 151 shutdown() 152 panic(err.Error()) 153 } 154 wallet.connected.Store(true) 155 rpcw := &rpcWallet{ 156 rpcClient: client, 157 log: log, 158 } 159 rpcw.accountsV.Store(XCWalletAccounts{ 160 PrimaryAccount: tAcctName, 161 }) 162 wallet.wallet = rpcw 163 wallet.ctx = walletCtx 164 165 // Initialize the best block. 166 tip, _ := wallet.getBestBlock(walletCtx) 167 wallet.currentTip.Store(tip) 168 169 if monitorBlocks { 170 go wallet.monitorBlocks(walletCtx) 171 } 172 173 return wallet, client, shutdown 174 175 } 176 177 func tNewWallet() (*ExchangeWallet, *tRPCClient, func()) { 178 return tNewWalletMonitorBlocks(true) 179 } 180 181 func signFunc(msgTx *wire.MsgTx, scriptSize int) (*wire.MsgTx, bool, error) { 182 for i := range msgTx.TxIn { 183 msgTx.TxIn[i].SignatureScript = randBytes(scriptSize) 184 } 185 return msgTx, true, nil 186 } 187 188 type tRPCClient struct { 189 sendRawHash *chainhash.Hash 190 sendRawErr error 191 sentRawTx *wire.MsgTx 192 txOutRes map[outPoint]*chainjson.GetTxOutResult 193 txOutErr error 194 bestBlockErr error 195 mempoolErr error 196 rawTxErr error 197 unspent []walletjson.ListUnspentResult 198 unspentErr error 199 balanceResult *walletjson.GetBalanceResult 200 balanceErr error 201 lockUnspentErr error 202 changeAddr stdaddr.Address 203 changeAddrErr error 204 newAddr stdaddr.Address 205 newAddrErr error 206 signFunc func(tx *wire.MsgTx) (*wire.MsgTx, bool, error) 207 privWIF *dcrutil.WIF 208 privWIFErr error 209 walletTxFn func() (*walletjson.GetTransactionResult, error) 210 lockErr error 211 passErr error 212 disconnected bool 213 rawRes map[string]json.RawMessage 214 rawErr map[string]error 215 blockchain *tBlockchain 216 lluCoins []walletjson.ListUnspentResult // Returned from ListLockUnspent 217 lockedCoins []*wire.OutPoint // Last submitted to LockUnspent 218 listLockedErr error 219 estFeeErr error 220 emitC chan asset.WalletNotification 221 // tickets 222 purchasedTickets [][]*chainhash.Hash 223 purchaseTicketsErr error 224 stakeInfo walletjson.GetStakeInfoResult 225 validateAddress map[string]*walletjson.ValidateAddressResult 226 } 227 228 type wireTxWithHeight struct { 229 tx *wire.MsgTx 230 height int64 231 } 232 233 type tBlockchain struct { 234 mtx sync.RWMutex 235 rawTxs map[chainhash.Hash]*wireTxWithHeight 236 mainchain map[int64]*chainhash.Hash 237 verboseBlocks map[chainhash.Hash]*wire.MsgBlock 238 blockHeaders map[chainhash.Hash]*wire.BlockHeader 239 v2CFilterBuilders map[chainhash.Hash]*tV2CFilterBuilder 240 } 241 242 func (blockchain *tBlockchain) addRawTx(blockHeight int64, tx *wire.MsgTx) (*chainhash.Hash, *wire.MsgBlock) { 243 blockchain.mtx.Lock() 244 defer blockchain.mtx.Unlock() 245 246 blockchain.rawTxs[tx.TxHash()] = &wireTxWithHeight{tx, blockHeight} 247 248 if blockHeight < 0 { 249 return nil, nil 250 } 251 252 prevBlockHash := blockchain.mainchain[blockHeight-1] 253 254 // Mined tx. Add to block. 255 block := blockchain.blockAt(blockHeight) 256 if block == nil { 257 block = &wire.MsgBlock{ 258 Header: wire.BlockHeader{ 259 PrevBlock: *prevBlockHash, 260 Height: uint32(blockHeight), 261 VoteBits: 1, 262 Timestamp: time.Now(), 263 }, 264 } 265 } 266 block.Transactions = append(block.Transactions, tx) 267 blockHash := block.BlockHash() 268 blockFilterBuilder := blockchain.v2CFilterBuilders[blockHash] 269 blockchain.mainchain[blockHeight] = &blockHash 270 blockchain.verboseBlocks[blockHash] = block 271 272 blockchain.blockHeaders[blockHash] = &block.Header 273 274 // Save prevout and output scripts in block cfilters. 275 if blockFilterBuilder == nil { 276 blockFilterBuilder = &tV2CFilterBuilder{} 277 copy(blockFilterBuilder.key[:], randBytes(16)) 278 279 } 280 blockchain.v2CFilterBuilders[blockHash] = blockFilterBuilder 281 for _, txIn := range tx.TxIn { 282 prevOut := &txIn.PreviousOutPoint 283 prevTx, found := blockchain.rawTxs[prevOut.Hash] 284 if !found || len(prevTx.tx.TxOut) <= int(prevOut.Index) { 285 continue 286 } 287 blockFilterBuilder.data.AddRegularPkScript(prevTx.tx.TxOut[prevOut.Index].PkScript) 288 } 289 for _, txOut := range tx.TxOut { 290 blockFilterBuilder.data.AddRegularPkScript(txOut.PkScript) 291 } 292 293 return &blockHash, block 294 } 295 296 // blockchain.mtx lock should be held for writes. 297 func (blockchain *tBlockchain) blockAt(height int64) *wire.MsgBlock { 298 blkHash, found := blockchain.mainchain[height] 299 if found { 300 return blockchain.verboseBlocks[*blkHash] 301 } 302 303 return nil 304 } 305 306 type tV2CFilterBuilder struct { 307 data blockcf2.Entries 308 key [gcs.KeySize]byte 309 } 310 311 func (filterBuilder *tV2CFilterBuilder) build() (*gcs.FilterV2, error) { 312 return gcs.NewFilterV2(blockcf2.B, blockcf2.M, filterBuilder.key, filterBuilder.data) 313 } 314 315 func defaultSignFunc(tx *wire.MsgTx) (*wire.MsgTx, bool, error) { return tx, true, nil } 316 317 func newTRPCClient() *tRPCClient { 318 // setup genesis block, required by bestblock polling goroutine 319 var newHash chainhash.Hash 320 copy(newHash[:], randBytes(32)) 321 return &tRPCClient{ 322 txOutRes: make(map[outPoint]*chainjson.GetTxOutResult), 323 blockchain: &tBlockchain{ 324 rawTxs: map[chainhash.Hash]*wireTxWithHeight{}, 325 mainchain: map[int64]*chainhash.Hash{ 326 0: &newHash, 327 }, 328 verboseBlocks: map[chainhash.Hash]*wire.MsgBlock{ 329 newHash: {}, 330 }, 331 blockHeaders: map[chainhash.Hash]*wire.BlockHeader{ 332 newHash: {}, 333 }, 334 v2CFilterBuilders: map[chainhash.Hash]*tV2CFilterBuilder{}, 335 }, 336 signFunc: defaultSignFunc, 337 rawRes: make(map[string]json.RawMessage), 338 rawErr: make(map[string]error), 339 emitC: make(chan asset.WalletNotification, 128), 340 } 341 } 342 343 func (c *tRPCClient) GetCurrentNet(context.Context) (wire.CurrencyNet, error) { 344 return tChainParams.Net, nil 345 } 346 347 func (c *tRPCClient) EstimateSmartFee(_ context.Context, confirmations int64, mode chainjson.EstimateSmartFeeMode) (*chainjson.EstimateSmartFeeResult, error) { 348 if c.estFeeErr != nil { 349 return nil, c.estFeeErr 350 } 351 optimalRate := float64(optimalFeeRate) * 1e-5 // optimalFeeRate: 22 atoms/byte = 0.00022 DCR/KB * 1e8 atoms/DCR * 1e-3 KB/Byte 352 // fmt.Println((float64(optimalFeeRate)*1e-5)-0.00022) 353 return &chainjson.EstimateSmartFeeResult{FeeRate: optimalRate}, nil 354 } 355 356 func (c *tRPCClient) SendRawTransaction(_ context.Context, tx *wire.MsgTx, allowHighFees bool) (*chainhash.Hash, error) { 357 c.sentRawTx = tx 358 if c.sendRawErr == nil && c.sendRawHash == nil { 359 h := tx.TxHash() 360 return &h, nil 361 } 362 return c.sendRawHash, c.sendRawErr 363 } 364 365 func (c *tRPCClient) GetTxOut(_ context.Context, txHash *chainhash.Hash, vout uint32, tree int8, mempool bool) (*chainjson.GetTxOutResult, error) { 366 return c.txOutRes[newOutPoint(txHash, vout)], c.txOutErr 367 } 368 369 func (c *tRPCClient) GetBestBlock(_ context.Context) (*chainhash.Hash, int64, error) { 370 if c.bestBlockErr != nil { 371 return nil, -1, c.bestBlockErr 372 } 373 bestHash, bestBlkHeight := c.getBestBlock() 374 return bestHash, bestBlkHeight, nil 375 } 376 377 func (c *tRPCClient) getBestBlock() (*chainhash.Hash, int64) { 378 c.blockchain.mtx.RLock() 379 defer c.blockchain.mtx.RUnlock() 380 var bestHash *chainhash.Hash 381 var bestBlkHeight int64 382 for height, hash := range c.blockchain.mainchain { 383 if height >= bestBlkHeight { 384 bestBlkHeight = height 385 bestHash = hash 386 } 387 } 388 return bestHash, bestBlkHeight 389 } 390 391 func (c *tRPCClient) GetBlockHash(_ context.Context, blockHeight int64) (*chainhash.Hash, error) { 392 c.blockchain.mtx.RLock() 393 defer c.blockchain.mtx.RUnlock() 394 h, found := c.blockchain.mainchain[blockHeight] 395 if !found { 396 return nil, fmt.Errorf("no test block at height %d", blockHeight) 397 } 398 return h, nil 399 } 400 401 func (c *tRPCClient) GetBlock(_ context.Context, blockHash *chainhash.Hash) (*wire.MsgBlock, error) { 402 c.blockchain.mtx.RLock() 403 defer c.blockchain.mtx.RUnlock() 404 blk, found := c.blockchain.verboseBlocks[*blockHash] 405 if !found { 406 return nil, fmt.Errorf("no test block found for %s", blockHash) 407 } 408 return blk, nil 409 } 410 411 func (c *tRPCClient) GetBlockHeaderVerbose(_ context.Context, blockHash *chainhash.Hash) (*chainjson.GetBlockHeaderVerboseResult, error) { 412 c.blockchain.mtx.RLock() 413 defer c.blockchain.mtx.RUnlock() 414 hdr, found := c.blockchain.blockHeaders[*blockHash] 415 if !found { 416 return nil, fmt.Errorf("no test block header found for %s", blockHash) 417 } 418 return &chainjson.GetBlockHeaderVerboseResult{ 419 Height: hdr.Height, 420 Hash: blockHash.String(), 421 PreviousHash: hdr.PrevBlock.String(), 422 Confirmations: 1, // just not -1, which indicates side chain 423 NextHash: "", // empty string signals that it is tip 424 }, nil 425 } 426 427 func (c *tRPCClient) GetBlockHeader(_ context.Context, blockHash *chainhash.Hash) (*wire.BlockHeader, error) { 428 c.blockchain.mtx.RLock() 429 defer c.blockchain.mtx.RUnlock() 430 hdr, found := c.blockchain.blockHeaders[*blockHash] 431 if !found { 432 return nil, fmt.Errorf("no test block header found for %s", blockHash) 433 } 434 return hdr, nil 435 } 436 437 func (c *tRPCClient) GetRawMempool(_ context.Context, txType chainjson.GetRawMempoolTxTypeCmd) ([]*chainhash.Hash, error) { 438 if c.mempoolErr != nil { 439 return nil, c.mempoolErr 440 } 441 c.blockchain.mtx.RLock() 442 defer c.blockchain.mtx.RUnlock() 443 txHashes := make([]*chainhash.Hash, 0) 444 for _, tx := range c.blockchain.rawTxs { 445 if tx.height < 0 { 446 txHash := tx.tx.TxHash() 447 txHashes = append(txHashes, &txHash) 448 } 449 } 450 return txHashes, nil 451 } 452 453 func (c *tRPCClient) GetBalanceMinConf(_ context.Context, account string, minConfirms int) (*walletjson.GetBalanceResult, error) { 454 return c.balanceResult, c.balanceErr 455 } 456 457 func (c *tRPCClient) LockUnspent(_ context.Context, unlock bool, ops []*wire.OutPoint) error { 458 if unlock == false { 459 if c.lockedCoins == nil { 460 c.lockedCoins = ops 461 } else { 462 c.lockedCoins = append(c.lockedCoins, ops...) 463 } 464 } 465 return c.lockUnspentErr 466 } 467 468 func (c *tRPCClient) GetRawChangeAddress(_ context.Context, account string, net stdaddr.AddressParams) (stdaddr.Address, error) { 469 return c.changeAddr, c.changeAddrErr 470 } 471 472 func (c *tRPCClient) GetNewAddressGapPolicy(_ context.Context, account string, gapPolicy dcrwallet.GapPolicy) (stdaddr.Address, error) { 473 return c.newAddr, c.newAddrErr 474 } 475 476 func (c *tRPCClient) DumpPrivKey(_ context.Context, address stdaddr.Address) (*dcrutil.WIF, error) { 477 return c.privWIF, c.privWIFErr 478 } 479 480 func (c *tRPCClient) GetTransaction(_ context.Context, txHash *chainhash.Hash) (*walletjson.GetTransactionResult, error) { 481 if c.walletTxFn != nil { 482 return c.walletTxFn() 483 } 484 c.blockchain.mtx.RLock() 485 defer c.blockchain.mtx.RUnlock() 486 if rawTx, has := c.blockchain.rawTxs[*txHash]; has { 487 b, err := rawTx.tx.Bytes() 488 if err != nil { 489 return nil, err 490 } 491 walletTx := &walletjson.GetTransactionResult{ 492 Hex: hex.EncodeToString(b), 493 } 494 return walletTx, nil 495 } 496 return nil, dcrjson.NewRPCError(dcrjson.ErrRPCNoTxInfo, "no test transaction") 497 } 498 499 func (c *tRPCClient) AccountUnlocked(_ context.Context, acct string) (*walletjson.AccountUnlockedResult, error) { 500 return &walletjson.AccountUnlockedResult{}, nil // go the walletlock/walletpassphrase route 501 } 502 503 func (c *tRPCClient) LockAccount(_ context.Context, acct string) error { return nil } 504 func (c *tRPCClient) UnlockAccount(_ context.Context, acct, pw string) error { return nil } 505 506 func (c *tRPCClient) WalletLock(_ context.Context) error { 507 return c.lockErr 508 } 509 510 func (c *tRPCClient) WalletPassphrase(_ context.Context, passphrase string, timeoutSecs int64) error { 511 return c.passErr 512 } 513 514 func (c *tRPCClient) WalletInfo(_ context.Context) (*walletjson.WalletInfoResult, error) { 515 return &walletjson.WalletInfoResult{ 516 Unlocked: true, 517 }, nil 518 } 519 520 func (c *tRPCClient) ValidateAddress(_ context.Context, address stdaddr.Address) (*walletjson.ValidateAddressWalletResult, error) { 521 if c.validateAddress != nil { 522 if c.validateAddress[address.String()] != nil { 523 return c.validateAddress[address.String()], nil 524 } 525 return &walletjson.ValidateAddressWalletResult{}, nil 526 } 527 528 return &walletjson.ValidateAddressWalletResult{ 529 IsMine: true, 530 Account: tAcctName, 531 }, nil 532 } 533 534 func (c *tRPCClient) Disconnected() bool { 535 return c.disconnected 536 } 537 538 func (c *tRPCClient) GetStakeInfo(ctx context.Context) (*walletjson.GetStakeInfoResult, error) { 539 return &c.stakeInfo, nil 540 } 541 542 func (c *tRPCClient) PurchaseTicket(ctx context.Context, fromAccount string, minConf *int, 543 numTickets *int, 544 expiry *int, ticketChange *bool, ticketFee *dcrutil.Amount) (tix []*chainhash.Hash, _ error) { 545 546 if c.purchaseTicketsErr != nil { 547 return nil, c.purchaseTicketsErr 548 } 549 550 if len(c.purchasedTickets) > 0 { 551 tix = c.purchasedTickets[0] 552 c.purchasedTickets = c.purchasedTickets[1:] 553 } 554 555 return tix, nil 556 } 557 558 func (c *tRPCClient) GetTickets(ctx context.Context, includeImmature bool) ([]*chainhash.Hash, error) { 559 return nil, nil 560 } 561 562 func (c *tRPCClient) GetVoteChoices(ctx context.Context) (*walletjson.GetVoteChoicesResult, error) { 563 return nil, nil 564 } 565 566 func (c *tRPCClient) SetVoteChoice(ctx context.Context, agendaID, choiceID string) error { 567 return nil 568 } 569 570 func (c *tRPCClient) RawRequest(_ context.Context, method string, params []json.RawMessage) (json.RawMessage, error) { 571 if rr, found := c.rawRes[method]; found { 572 return rr, c.rawErr[method] // err probably should be nil, but respect the config 573 } 574 if re, found := c.rawErr[method]; found { 575 return nil, re 576 } 577 578 switch method { 579 case methodGetPeerInfo: 580 return json.Marshal([]*walletjson.GetPeerInfoResult{ 581 { 582 Addr: "127.0.0.1", 583 }, 584 }) 585 case methodGetCFilterV2: 586 if len(params) != 1 { 587 return nil, fmt.Errorf("getcfilterv2 requires 1 param, got %d", len(params)) 588 } 589 590 var hashStr string 591 json.Unmarshal(params[0], &hashStr) 592 blkHash, _ := chainhash.NewHashFromStr(hashStr) 593 594 c.blockchain.mtx.RLock() 595 defer c.blockchain.mtx.RUnlock() 596 blockFilterBuilder := c.blockchain.v2CFilterBuilders[*blkHash] 597 if blockFilterBuilder == nil { 598 return nil, fmt.Errorf("cfilters builder not found for block %s", blkHash) 599 } 600 v2CFilter, err := blockFilterBuilder.build() 601 if err != nil { 602 return nil, err 603 } 604 res := &walletjson.GetCFilterV2Result{ 605 BlockHash: blkHash.String(), 606 Filter: hex.EncodeToString(v2CFilter.Bytes()), 607 Key: hex.EncodeToString(blockFilterBuilder.key[:]), 608 } 609 return json.Marshal(res) 610 611 case methodListUnspent: 612 if c.unspentErr != nil { 613 return nil, c.unspentErr 614 } 615 616 var acct string 617 if len(params) > 3 { 618 // filter with provided acct param 619 _ = json.Unmarshal(params[3], &acct) 620 } 621 allAccts := acct == "" || acct == "*" 622 623 var unspents []walletjson.ListUnspentResult 624 for _, unspent := range c.unspent { 625 if allAccts || unspent.Account == acct { 626 unspents = append(unspents, unspent) 627 } 628 } 629 630 response, _ := json.Marshal(unspents) 631 return response, nil 632 633 case methodListLockUnspent: 634 if c.listLockedErr != nil { 635 return nil, c.listLockedErr 636 } 637 638 var acct string 639 if len(params) > 0 { 640 _ = json.Unmarshal(params[0], &acct) 641 } 642 allAccts := acct == "" || acct == "*" 643 644 var locked []chainjson.TransactionInput 645 for _, utxo := range c.lluCoins { 646 if allAccts || utxo.Account == acct { 647 locked = append(locked, chainjson.TransactionInput{ 648 Txid: utxo.TxID, 649 Amount: utxo.Amount, 650 Vout: utxo.Vout, 651 Tree: utxo.Tree, 652 }) 653 } 654 } 655 response, _ := json.Marshal(locked) 656 return response, nil 657 658 case methodSignRawTransaction: 659 if len(params) != 1 { 660 return nil, fmt.Errorf("needed 1 param") 661 } 662 663 var msgTxHex string 664 err := json.Unmarshal(params[0], &msgTxHex) 665 if err != nil { 666 return nil, err 667 } 668 669 msgTx, err := msgTxFromHex(msgTxHex) 670 if err != nil { 671 res := walletjson.SignRawTransactionResult{ 672 Hex: msgTxHex, 673 Errors: []walletjson.SignRawTransactionError{ 674 { 675 TxID: msgTx.CachedTxHash().String(), 676 Error: err.Error(), 677 }, 678 }, 679 // Complete stays false. 680 } 681 return json.Marshal(&res) 682 } 683 684 if c.signFunc == nil { 685 return nil, fmt.Errorf("no signFunc configured") 686 } 687 688 signedTx, complete, err := c.signFunc(msgTx) 689 if err != nil { 690 res := walletjson.SignRawTransactionResult{ 691 Hex: msgTxHex, 692 Errors: []walletjson.SignRawTransactionError{ 693 { 694 TxID: msgTx.CachedTxHash().String(), 695 Error: err.Error(), 696 }, 697 }, 698 // Complete stays false. 699 } 700 return json.Marshal(&res) 701 } 702 703 txHex, err := msgTxToHex(signedTx) 704 if err != nil { 705 return nil, fmt.Errorf("failed to encode MsgTx: %w", err) 706 } 707 708 res := walletjson.SignRawTransactionResult{ 709 Hex: txHex, 710 Complete: complete, 711 } 712 return json.Marshal(&res) 713 714 case methodWalletInfo: 715 return json.Marshal(new(walletjson.WalletInfoResult)) 716 } 717 718 return nil, fmt.Errorf("method %v not implemented by (*tRPCClient).RawRequest", method) 719 } 720 721 func (c *tRPCClient) SetTxFee(ctx context.Context, fee dcrutil.Amount) error { 722 return nil 723 } 724 725 func (c *tRPCClient) GetReceivedByAddressMinConf(ctx context.Context, address stdaddr.Address, minConfs int) (dcrutil.Amount, error) { 726 return 0, nil 727 } 728 729 func (c *tRPCClient) ListSinceBlock(ctx context.Context, hash *chainhash.Hash) (*walletjson.ListSinceBlockResult, error) { 730 return nil, nil 731 } 732 733 func TestMain(m *testing.M) { 734 tChainParams = chaincfg.MainNetParams() 735 tPKHAddr, _ = stdaddr.DecodeAddress("DsTya4cCFBgtofDLiRhkyPYEQjgs3HnarVP", tChainParams) 736 tLogger = dex.StdOutLogger("TEST", dex.LevelTrace) 737 var shutdown func() 738 tCtx, shutdown = context.WithCancel(context.Background()) 739 tTxHash, _ = chainhash.NewHashFromStr(tTxID) 740 tP2PKHScript, _ = hex.DecodeString("76a9148fc02268f208a61767504fe0b48d228641ba81e388ac") 741 // tP2SH, _ = hex.DecodeString("76a91412a9abf5c32392f38bd8a1f57d81b1aeecc5699588ac") 742 doIt := func() int { 743 // Not counted as coverage, must test Archiver constructor explicitly. 744 defer shutdown() 745 return m.Run() 746 } 747 os.Exit(doIt()) 748 } 749 750 func TestMaxFundingFees(t *testing.T) { 751 wallet, _, shutdown := tNewWallet() 752 defer shutdown() 753 754 maxFeeRate := uint64(100) 755 756 useSplitOptions := map[string]string{ 757 multiSplitKey: "true", 758 } 759 noSplitOptions := map[string]string{ 760 multiSplitKey: "false", 761 } 762 763 maxFundingFees := wallet.MaxFundingFees(3, maxFeeRate, useSplitOptions) 764 expectedFees := maxFeeRate * (dexdcr.P2PKHInputSize*12 + dexdcr.P2PKHOutputSize*4 + dexdcr.MsgTxOverhead) 765 if maxFundingFees != expectedFees { 766 t.Fatalf("unexpected max funding fees. expected %d, got %d", expectedFees, maxFundingFees) 767 } 768 769 maxFundingFees = wallet.MaxFundingFees(3, maxFeeRate, noSplitOptions) 770 if maxFundingFees != 0 { 771 t.Fatalf("unexpected max funding fees. expected 0, got %d", maxFundingFees) 772 } 773 } 774 775 func TestAvailableFund(t *testing.T) { 776 wallet, node, shutdown := tNewWallet() 777 defer shutdown() 778 779 // With an empty list returned, there should be no error, but the value zero 780 // should be returned. 781 unspents := make([]walletjson.ListUnspentResult, 0) 782 node.unspent = unspents 783 balanceResult := &walletjson.GetBalanceResult{ 784 Balances: []walletjson.GetAccountBalanceResult{ 785 { 786 AccountName: tAcctName, 787 }, 788 }, 789 } 790 node.balanceResult = balanceResult 791 bal, err := wallet.Balance() 792 if err != nil { 793 t.Fatalf("error for zero utxos: %v", err) 794 } 795 if bal.Available != 0 { 796 t.Fatalf("expected available = 0, got %d", bal.Available) 797 } 798 if bal.Immature != 0 { 799 t.Fatalf("expected unconf = 0, got %d", bal.Immature) 800 } 801 802 var vout uint32 803 addUtxo := func(atomAmt uint64, confs int64, lock bool) { 804 utxo := walletjson.ListUnspentResult{ 805 TxID: tTxID, 806 Vout: vout, 807 Address: tPKHAddr.String(), 808 Account: tAcctName, 809 Amount: float64(atomAmt) / 1e8, 810 Confirmations: confs, 811 ScriptPubKey: hex.EncodeToString(tP2PKHScript), 812 Spendable: true, 813 } 814 if lock { 815 node.lluCoins = append(node.lluCoins, utxo) 816 } else { 817 unspents = append(unspents, utxo) 818 node.unspent = unspents 819 } 820 // update balance 821 balanceResult.Balances[0].Spendable += utxo.Amount 822 vout++ 823 } 824 825 // Add 1 unspent output and check balance 826 var littleLots uint64 = 6 827 littleOrder := tLotSize * littleLots 828 littleFunds := calc.RequiredOrderFunds(littleOrder, dexdcr.P2PKHInputSize, littleLots, dexdcr.InitTxSizeBase, dexdcr.InitTxSize, tDCR.MaxFeeRate) 829 addUtxo(littleFunds, 0, false) 830 bal, err = wallet.Balance() 831 if err != nil { 832 t.Fatalf("error for 1 utxo: %v", err) 833 } 834 if bal.Available != littleFunds { 835 t.Fatalf("expected available = %d for confirmed utxos, got %d", littleFunds, bal.Available) 836 } 837 if bal.Immature != 0 { 838 t.Fatalf("expected immature = %d, got %d", 0, bal.Immature) 839 } 840 if bal.Locked != 0 { 841 t.Fatalf("expected locked = %d, got %d", 0, bal.Locked) 842 } 843 844 // Add a second utxo, lock it and check balance. 845 lockedBit := tLotSize * 2 846 addUtxo(lockedBit, 1, true) 847 bal, err = wallet.Balance() 848 if err != nil { 849 t.Fatalf("error for 2 utxos: %v", err) 850 } 851 // Available balance should exclude locked utxo amount. 852 if bal.Available != littleFunds { 853 t.Fatalf("expected available = %d for confirmed utxos, got %d", littleFunds, bal.Available) 854 } 855 if bal.Immature != 0 { 856 t.Fatalf("expected immature = %d, got %d", 0, bal.Immature) 857 } 858 if bal.Locked != lockedBit { 859 t.Fatalf("expected locked = %d, got %d", lockedBit, bal.Locked) 860 } 861 862 // Add a third utxo. 863 var lottaLots uint64 = 100 864 lottaOrder := tLotSize * 100 865 // Add funding for an extra input to accommodate the later combined tests. 866 lottaFunds := calc.RequiredOrderFunds(lottaOrder, 2*dexdcr.P2PKHInputSize, lottaLots, dexdcr.InitTxSizeBase, dexdcr.InitTxSize, tDCR.MaxFeeRate) 867 addUtxo(lottaFunds, 1, false) 868 bal, err = wallet.Balance() 869 if err != nil { 870 t.Fatalf("error for 3 utxos: %v", err) 871 } 872 if bal.Available != littleFunds+lottaFunds { 873 t.Fatalf("expected available = %d for 2 outputs, got %d", littleFunds+lottaFunds, bal.Available) 874 } 875 if bal.Immature != 0 { 876 t.Fatalf("expected unconf = 0 for 2 outputs, got %d", bal.Immature) 877 } 878 // locked balance should remain same as utxo2 amount. 879 if bal.Locked != lockedBit { 880 t.Fatalf("expected locked = %d, got %d", lockedBit, bal.Locked) 881 } 882 883 ord := &asset.Order{ 884 Version: version, 885 Value: 0, 886 MaxSwapCount: 1, 887 MaxFeeRate: tDCR.MaxFeeRate, 888 FeeSuggestion: feeSuggestion, 889 } 890 891 setOrderValue := func(v uint64) { 892 ord.Value = v 893 ord.MaxSwapCount = v / tLotSize 894 } 895 896 // Zero value 897 _, _, _, err = wallet.FundOrder(ord) 898 if err == nil { 899 t.Fatalf("no funding error for zero value") 900 } 901 902 // Nothing to spend 903 node.unspent = nil 904 setOrderValue(littleOrder) 905 _, _, _, err = wallet.FundOrder(ord) 906 if err == nil { 907 t.Fatalf("no error for zero utxos") 908 } 909 node.unspent = unspents 910 911 // RPC error 912 node.unspentErr = tErr 913 _, _, _, err = wallet.FundOrder(ord) 914 if err == nil { 915 t.Fatalf("no funding error for rpc error") 916 } 917 node.unspentErr = nil 918 919 // Negative response when locking outputs. 920 node.lockUnspentErr = tErr 921 _, _, _, err = wallet.FundOrder(ord) 922 if err == nil { 923 t.Fatalf("no error for lockunspent result = false: %v", err) 924 } 925 node.lockUnspentErr = nil 926 927 // Fund a little bit, but small output is unconfirmed. 928 spendables, _, _, err := wallet.FundOrder(ord) 929 if err != nil { 930 t.Fatalf("error funding small amount: %v", err) 931 } 932 if len(spendables) != 1 { 933 t.Fatalf("expected 1 spendable, got %d", len(spendables)) 934 } 935 v := spendables[0].Value() 936 if v != lottaFunds { 937 t.Fatalf("expected spendable of value %d, got %d", lottaFunds, v) 938 } 939 940 // Now confirm the little bit and have it selected. 941 unspents[0].Confirmations++ 942 spendables, _, fees, err := wallet.FundOrder(ord) 943 if err != nil { 944 t.Fatalf("error funding small amount: %v", err) 945 } 946 if len(spendables) != 1 { 947 t.Fatalf("expected 1 spendable, got %d", len(spendables)) 948 } 949 if fees != 0 { 950 t.Fatalf("expected zero fees, got %d", fees) 951 } 952 v = spendables[0].Value() 953 if v != littleFunds { 954 t.Fatalf("expected spendable of value %d, got %d", littleFunds, v) 955 } 956 957 // Fund a lotta bit. 958 setOrderValue(lottaOrder) 959 spendables, _, _, err = wallet.FundOrder(ord) 960 if err != nil { 961 t.Fatalf("error funding large amount: %v", err) 962 } 963 if len(spendables) != 1 { 964 t.Fatalf("expected 1 spendable, got %d", len(spendables)) 965 } 966 if fees != 0 { 967 t.Fatalf("expected zero fees, got %d", fees) 968 } 969 v = spendables[0].Value() 970 if v != lottaFunds { 971 t.Fatalf("expected spendable of value %d, got %d", lottaFunds, v) 972 } 973 974 extraLottaOrder := littleOrder + lottaOrder 975 extraLottaLots := littleLots + lottaLots 976 // Prepare for a split transaction. 977 baggageFees := tDCR.MaxFeeRate * splitTxBaggage 978 node.newAddr = tPKHAddr 979 node.changeAddr = tPKHAddr 980 wallet.config().useSplitTx = true 981 // No split performed due to economics is not an error. 982 setOrderValue(extraLottaOrder) 983 coins, _, _, err := wallet.FundOrder(ord) 984 if err != nil { 985 t.Fatalf("error for no-split split: %v", err) 986 } 987 if fees != 0 { 988 t.Fatalf("expected zero fees, got %d", fees) 989 } 990 // Should be both coins. 991 if len(coins) != 2 { 992 t.Fatalf("no-split split didn't return both coins") 993 } 994 995 // Not enough to cover transaction fees. 996 tweak := float64(littleFunds+lottaFunds-calc.RequiredOrderFunds(extraLottaOrder, 2*dexdcr.P2PKHInputSize, extraLottaLots, dexdcr.InitTxSizeBase, dexdcr.InitTxSize, tDCR.MaxFeeRate)+1) / 1e8 997 node.unspent[0].Amount -= tweak 998 setOrderValue(extraLottaOrder) 999 _, _, _, err = wallet.FundOrder(ord) 1000 if err == nil { 1001 t.Fatalf("no error when not enough to cover tx fees") 1002 } 1003 if fees != 0 { 1004 t.Fatalf("expected zero fees, got %d", fees) 1005 } 1006 1007 node.unspent[0].Amount += tweak 1008 1009 // No split because not standing order. 1010 ord.Immediate = true 1011 coins, _, _, err = wallet.FundOrder(ord) 1012 if err != nil { 1013 t.Fatalf("error for no-split split: %v", err) 1014 } 1015 if fees != 0 { 1016 t.Fatalf("expected zero fees, got %d", fees) 1017 } 1018 ord.Immediate = false 1019 if len(coins) != 2 { 1020 t.Fatalf("no-split split didn't return both coins") 1021 } 1022 1023 // With a little more locked, the split should be performed. 1024 node.unspent[1].Amount += float64(baggageFees) / 1e8 1025 coins, _, fees, err = wallet.FundOrder(ord) 1026 if err != nil { 1027 t.Fatalf("error for split tx: %v", err) 1028 } 1029 1030 inputSize := dexdcr.P2PKHInputSize - dexdcr.P2PKHSigScriptSize // no sig script 1031 splitTxSize := dexdcr.MsgTxOverhead + (2 * inputSize) + 2*dexdcr.P2PKHOutputSize 1032 expectedFees := uint64(splitTxSize) * feeSuggestion 1033 if fees != expectedFees { 1034 t.Fatalf("expected fees of %d, got %d", expectedFees, fees) 1035 } 1036 1037 // Should be just one coin. 1038 if len(coins) != 1 { 1039 t.Fatalf("split failed - coin count != 1") 1040 } 1041 if node.sentRawTx == nil { 1042 t.Fatalf("split failed - no tx sent") 1043 } 1044 1045 // Hit some error paths. 1046 1047 // GetNewAddressGapPolicy error 1048 node.newAddrErr = tErr 1049 _, _, _, err = wallet.FundOrder(ord) 1050 if err == nil { 1051 t.Fatalf("no error for split tx change addr error") 1052 } 1053 node.newAddrErr = nil 1054 1055 // GetRawChangeAddress error 1056 node.changeAddrErr = tErr 1057 _, _, _, err = wallet.FundOrder(ord) 1058 if err == nil { 1059 t.Fatalf("no error for split tx change addr error") 1060 } 1061 node.changeAddrErr = nil 1062 1063 // SendRawTx error 1064 node.sendRawErr = tErr 1065 _, _, _, err = wallet.FundOrder(ord) 1066 if err == nil { 1067 t.Fatalf("no error for split tx send error") 1068 } 1069 node.sendRawErr = nil 1070 1071 // Success again. 1072 _, _, _, err = wallet.FundOrder(ord) 1073 if err != nil { 1074 t.Fatalf("error for split tx recovery run") 1075 } 1076 1077 // Not enough funds, because littleUnspent is a different account. 1078 unspents[0].Account = "wrong account" 1079 setOrderValue(extraLottaOrder) 1080 _, _, _, err = wallet.FundOrder(ord) 1081 if err == nil { 1082 t.Fatalf("no error for wrong account") 1083 } 1084 node.unspent[0].Account = tAcctName 1085 1086 // Place locked utxo in different account, check locked balance. 1087 node.lluCoins[0].Account = "wrong account" 1088 bal, err = wallet.Balance() 1089 if err != nil { 1090 t.Fatalf("error for 3 utxos, with locked utxo in wrong acct: %v", err) 1091 } 1092 if bal.Locked != 0 { 1093 t.Fatalf("expected locked = %d, got %d", 0, bal.Locked) 1094 } 1095 } 1096 1097 // Since ReturnCoins takes the wallet.Coin interface, make sure any interface 1098 // is acceptable. 1099 type tCoin struct{ id []byte } 1100 1101 func (c *tCoin) ID() dex.Bytes { 1102 if len(c.id) > 0 { 1103 return c.id 1104 } 1105 return make([]byte, 36) 1106 } 1107 func (c *tCoin) String() string { return hex.EncodeToString(c.id) } 1108 func (c *tCoin) Value() uint64 { return 100 } 1109 func (c *tCoin) Confirmations(ctx context.Context) (uint32, error) { return 2, nil } 1110 func (c *tCoin) TxID() string { return hex.EncodeToString(c.id) } 1111 1112 func TestReturnCoins(t *testing.T) { 1113 wallet, node, shutdown := tNewWallet() 1114 defer shutdown() 1115 1116 // Test it with the local output type. 1117 coins := asset.Coins{ 1118 newOutput(tTxHash, 0, 1, wire.TxTreeRegular), 1119 } 1120 err := wallet.ReturnCoins(coins) 1121 if err != nil { 1122 t.Fatalf("error with output type coins: %v", err) 1123 } 1124 1125 // Should error for no coins. 1126 err = wallet.ReturnCoins(asset.Coins{}) 1127 if err == nil { 1128 t.Fatalf("no error for zero coins") 1129 } 1130 1131 // nil unlocks all 1132 wallet.fundingCoins[outPoint{*tTxHash, 0}] = &fundingCoin{} 1133 err = wallet.ReturnCoins(nil) 1134 if err != nil { 1135 t.Fatalf("error for nil coins: %v", err) 1136 } 1137 if len(wallet.fundingCoins) != 0 { 1138 t.Errorf("all funding coins not unlocked") 1139 } 1140 1141 // Have the RPC return negative response. 1142 node.lockUnspentErr = tErr 1143 err = wallet.ReturnCoins(coins) 1144 if err == nil { 1145 t.Fatalf("no error for RPC failure") 1146 } 1147 node.lockUnspentErr = nil 1148 1149 // ReturnCoins should accept any type that implements wallet.Coin. 1150 1151 // Test the convertCoin method while we're here. 1152 badID := []byte{0x01, 0x02} 1153 badCoins := asset.Coins{&tCoin{id: badID}, &tCoin{id: badID}} 1154 1155 err = wallet.ReturnCoins(badCoins) 1156 if err == nil { 1157 t.Fatalf("no error for bad coins") 1158 } 1159 1160 coinID := toCoinID(tTxHash, 0) 1161 coins = asset.Coins{&tCoin{id: coinID}, &tCoin{id: coinID}} 1162 err = wallet.ReturnCoins(coins) 1163 if err != nil { 1164 t.Fatalf("error with custom coin type: %v", err) 1165 } 1166 } 1167 1168 func TestFundingCoins(t *testing.T) { 1169 wallet, node, shutdown := tNewWallet() 1170 defer shutdown() 1171 1172 vout := uint32(123) 1173 coinID := toCoinID(tTxHash, vout) 1174 p2pkhUnspent := walletjson.ListUnspentResult{ 1175 TxID: tTxID, 1176 Vout: vout, 1177 Address: tPKHAddr.String(), 1178 Spendable: true, 1179 Account: tAcctName, 1180 } 1181 1182 node.unspent = []walletjson.ListUnspentResult{p2pkhUnspent} 1183 coinIDs := []dex.Bytes{coinID} 1184 1185 ensureGood := func() { 1186 t.Helper() 1187 coins, err := wallet.FundingCoins(coinIDs) 1188 if err != nil { 1189 t.Fatalf("FundingCoins error: %v", err) 1190 } 1191 if len(coins) != 1 { 1192 t.Fatalf("expected 1 coin, got %d", len(coins)) 1193 } 1194 } 1195 1196 // Check initial success. 1197 ensureGood() 1198 1199 // Clear the RPC coins, but add a coin to the cache. 1200 node.unspent = nil 1201 opID := newOutPoint(tTxHash, vout) 1202 wallet.fundingCoins[opID] = &fundingCoin{ 1203 op: newOutput(tTxHash, vout, 0, 0), 1204 } 1205 ensureGood() 1206 1207 ensureErr := func(tag string) { 1208 _, err := wallet.FundingCoins(coinIDs) 1209 if err == nil { 1210 t.Fatalf("%s: no error", tag) 1211 } 1212 } 1213 1214 // No coins 1215 delete(wallet.fundingCoins, opID) 1216 ensureErr("no coins") 1217 node.unspent = []walletjson.ListUnspentResult{p2pkhUnspent} 1218 1219 // Bad coin ID 1220 ogIDs := coinIDs 1221 coinIDs = []dex.Bytes{randBytes(35)} 1222 ensureErr("bad coin ID") 1223 coinIDs = ogIDs 1224 1225 // listunspent error 1226 node.unspentErr = tErr 1227 ensureErr("listunpent") 1228 node.unspentErr = nil 1229 1230 ensureGood() 1231 } 1232 1233 func checkMaxOrder(t *testing.T, wallet *ExchangeWallet, lots, swapVal, maxFees, estWorstCase, estBestCase uint64) { 1234 t.Helper() 1235 _, maxOrder, err := wallet.maxOrder(tLotSize, feeSuggestion, tDCR.MaxFeeRate) 1236 if err != nil { 1237 t.Fatalf("MaxOrder error: %v", err) 1238 } 1239 checkSwapEstimate(t, maxOrder, lots, swapVal, maxFees, estWorstCase, estBestCase) 1240 } 1241 1242 func checkSwapEstimate(t *testing.T, est *asset.SwapEstimate, lots, swapVal, maxFees, estWorstCase, estBestCase uint64) { 1243 t.Helper() 1244 if est.Lots != lots { 1245 t.Fatalf("MaxOrder has wrong Lots. wanted %d, got %d", lots, est.Lots) 1246 } 1247 if est.Value != swapVal { 1248 t.Fatalf("est has wrong Value. wanted %d, got %d", swapVal, est.Value) 1249 } 1250 if est.MaxFees != maxFees { 1251 t.Fatalf("est has wrong MaxFees. wanted %d, got %d", maxFees, est.MaxFees) 1252 } 1253 if est.RealisticWorstCase != estWorstCase { 1254 t.Fatalf("MaxOrder has wrong RealisticWorstCase. wanted %d, got %d", estWorstCase, est.RealisticWorstCase) 1255 } 1256 if est.RealisticBestCase != estBestCase { 1257 t.Fatalf("MaxOrder has wrong RealisticBestCase. wanted %d, got %d", estBestCase, est.RealisticBestCase) 1258 } 1259 } 1260 1261 func TestFundMultiOrder(t *testing.T) { 1262 wallet, node, shutdown := tNewWallet() 1263 defer shutdown() 1264 1265 maxFeeRate := uint64(80) 1266 feeSuggestion := uint64(60) 1267 1268 txIDs := make([]string, 0, 5) 1269 txHashes := make([]chainhash.Hash, 0, 5) 1270 1271 addresses := []string{ 1272 tPKHAddr.String(), 1273 tPKHAddr.String(), 1274 tPKHAddr.String(), 1275 tPKHAddr.String(), 1276 } 1277 scriptPubKeys := []string{ 1278 hex.EncodeToString(tP2PKHScript), 1279 hex.EncodeToString(tP2PKHScript), 1280 hex.EncodeToString(tP2PKHScript), 1281 hex.EncodeToString(tP2PKHScript), 1282 } 1283 for i := 0; i < 5; i++ { 1284 txIDs = append(txIDs, hex.EncodeToString(encode.RandomBytes(32))) 1285 h, _ := chainhash.NewHashFromStr(txIDs[i]) 1286 txHashes = append(txHashes, *h) 1287 } 1288 1289 expectedSplitFee := func(numInputs, numOutputs uint64) uint64 { 1290 inputSize := uint64(dexdcr.P2PKHInputSize) 1291 outputSize := uint64(dexdcr.P2PKHOutputSize) 1292 return (dexdcr.MsgTxOverhead + numInputs*inputSize + numOutputs*outputSize) * feeSuggestion 1293 } 1294 1295 requiredForOrder := func(value, maxSwapCount uint64) int64 { 1296 inputSize := uint64(dexdcr.P2PKHInputSize) 1297 return int64(calc.RequiredOrderFunds(value, inputSize, maxSwapCount, 1298 dexdcr.InitTxSizeBase, dexdcr.InitTxSize, maxFeeRate)) 1299 } 1300 1301 type test struct { 1302 name string 1303 multiOrder *asset.MultiOrder 1304 allOrNothing bool 1305 maxLock uint64 1306 utxos []walletjson.ListUnspentResult 1307 bondReserves uint64 1308 balance uint64 1309 1310 // if expectedCoins is nil, all the coins are from 1311 // the split output. If any of the coins are nil, 1312 // than that output is from the split output. 1313 expectedCoins []asset.Coins 1314 expectedRedeemScripts [][]dex.Bytes 1315 expectSendRawTx bool 1316 expectedSplitFee uint64 1317 expectedInputs []*wire.TxIn 1318 expectedOutputs []*wire.TxOut 1319 expectedChange uint64 1320 expectedLockedCoins []*wire.OutPoint 1321 expectErr bool 1322 } 1323 1324 tests := []*test{ 1325 { // "split not allowed, utxos like split previously done" 1326 name: "split not allowed, utxos like split previously done", 1327 multiOrder: &asset.MultiOrder{ 1328 Values: []*asset.MultiOrderValue{ 1329 { 1330 Value: 1e6, 1331 MaxSwapCount: 1, 1332 }, 1333 { 1334 Value: 2e6, 1335 MaxSwapCount: 2, 1336 }, 1337 }, 1338 MaxFeeRate: maxFeeRate, 1339 FeeSuggestion: feeSuggestion, 1340 Options: map[string]string{ 1341 "swapsplit": "false", 1342 }, 1343 }, 1344 utxos: []walletjson.ListUnspentResult{ 1345 { 1346 Confirmations: 1, 1347 Spendable: true, 1348 TxID: txIDs[0], 1349 Account: tAcctName, 1350 ScriptPubKey: scriptPubKeys[0], 1351 Address: addresses[0], 1352 Amount: 19e5 / 1e8, 1353 Vout: 0, 1354 }, 1355 { 1356 Confirmations: 1, 1357 Spendable: true, 1358 TxID: txIDs[1], 1359 Account: tAcctName, 1360 ScriptPubKey: scriptPubKeys[1], 1361 Address: addresses[1], 1362 Amount: 35e5 / 1e8, 1363 Vout: 0, 1364 }, 1365 }, 1366 balance: 35e5, 1367 expectedCoins: []asset.Coins{ 1368 {newOutput(&txHashes[0], 0, 19e5, wire.TxTreeRegular)}, 1369 {newOutput(&txHashes[1], 0, 35e5, wire.TxTreeRegular)}, 1370 }, 1371 expectedRedeemScripts: [][]dex.Bytes{ 1372 {nil}, 1373 {nil}, 1374 }, 1375 }, 1376 { // "split not allowed, require multiple utxos per order" 1377 name: "split not allowed, require multiple utxos per order", 1378 multiOrder: &asset.MultiOrder{ 1379 Values: []*asset.MultiOrderValue{ 1380 { 1381 Value: 1e6, 1382 MaxSwapCount: 1, 1383 }, 1384 { 1385 Value: 2e6, 1386 MaxSwapCount: 2, 1387 }, 1388 }, 1389 MaxFeeRate: maxFeeRate, 1390 FeeSuggestion: feeSuggestion, 1391 Options: map[string]string{ 1392 "swapsplit": "false", 1393 }, 1394 }, 1395 utxos: []walletjson.ListUnspentResult{ 1396 { 1397 Confirmations: 1, 1398 Spendable: true, 1399 TxID: txIDs[0], 1400 Account: tAcctName, 1401 ScriptPubKey: scriptPubKeys[0], 1402 Address: addresses[0], 1403 Amount: 6e5 / 1e8, 1404 Vout: 0, 1405 }, 1406 { 1407 Confirmations: 1, 1408 Spendable: true, 1409 TxID: txIDs[1], 1410 Account: tAcctName, 1411 ScriptPubKey: scriptPubKeys[1], 1412 Address: addresses[1], 1413 Amount: 5e5 / 1e8, 1414 Vout: 0, 1415 }, 1416 { 1417 Confirmations: 1, 1418 Spendable: true, 1419 TxID: txIDs[2], 1420 Account: tAcctName, 1421 ScriptPubKey: scriptPubKeys[2], 1422 Address: addresses[2], 1423 Amount: 22e5 / 1e8, 1424 Vout: 0, 1425 }, 1426 }, 1427 balance: 33e5, 1428 expectedCoins: []asset.Coins{ 1429 {newOutput(&txHashes[0], 0, 6e5, wire.TxTreeRegular), newOutput(&txHashes[1], 0, 5e5, wire.TxTreeRegular)}, 1430 {newOutput(&txHashes[2], 0, 22e5, wire.TxTreeRegular)}, 1431 }, 1432 expectedRedeemScripts: [][]dex.Bytes{ 1433 {nil, nil}, 1434 {nil}, 1435 }, 1436 expectedLockedCoins: []*wire.OutPoint{ 1437 wire.NewOutPoint(&txHashes[0], 0, wire.TxTreeRegular), 1438 wire.NewOutPoint(&txHashes[1], 0, wire.TxTreeRegular), 1439 wire.NewOutPoint(&txHashes[2], 0, wire.TxTreeRegular), 1440 }, 1441 }, 1442 { // "split not allowed, can only fund first order and respect maxLock" 1443 name: "split not allowed, can only fund first order and respect maxLock", 1444 multiOrder: &asset.MultiOrder{ 1445 Values: []*asset.MultiOrderValue{ 1446 { 1447 Value: 1e6, 1448 MaxSwapCount: 1, 1449 }, 1450 { 1451 Value: 2e6, 1452 MaxSwapCount: 2, 1453 }, 1454 }, 1455 MaxFeeRate: maxFeeRate, 1456 FeeSuggestion: feeSuggestion, 1457 Options: map[string]string{ 1458 "swapsplit": "false", 1459 }, 1460 }, 1461 maxLock: 32e5, 1462 utxos: []walletjson.ListUnspentResult{ 1463 { 1464 Confirmations: 1, 1465 Spendable: true, 1466 TxID: txIDs[2], 1467 Account: tAcctName, 1468 ScriptPubKey: scriptPubKeys[2], 1469 Address: addresses[2], 1470 Amount: 1e6 / 1e8, 1471 Vout: 0, 1472 }, 1473 { 1474 Confirmations: 1, 1475 Spendable: true, 1476 TxID: txIDs[0], 1477 Account: tAcctName, 1478 ScriptPubKey: scriptPubKeys[0], 1479 Address: addresses[0], 1480 Amount: 11e5 / 1e8, 1481 Vout: 0, 1482 }, 1483 { 1484 Confirmations: 1, 1485 Spendable: true, 1486 TxID: txIDs[1], 1487 Account: tAcctName, 1488 ScriptPubKey: scriptPubKeys[1], 1489 Address: addresses[1], 1490 Amount: 25e5 / 1e8, 1491 Vout: 0, 1492 }, 1493 }, 1494 balance: 46e5, 1495 expectedCoins: []asset.Coins{ 1496 {newOutput(&txHashes[0], 0, 11e5, wire.TxTreeRegular)}, 1497 }, 1498 expectedRedeemScripts: [][]dex.Bytes{ 1499 {nil}, 1500 }, 1501 expectedLockedCoins: []*wire.OutPoint{ 1502 wire.NewOutPoint(&txHashes[0], 0, wire.TxTreeRegular), 1503 }, 1504 }, 1505 { // "split not allowed, can only fund first order and respect bond reserves" 1506 name: "no split allowed, can only fund first order and respect bond reserves", 1507 multiOrder: &asset.MultiOrder{ 1508 Values: []*asset.MultiOrderValue{ 1509 { 1510 Value: 1e6, 1511 MaxSwapCount: 1, 1512 }, 1513 { 1514 Value: 2e6, 1515 MaxSwapCount: 2, 1516 }, 1517 }, 1518 MaxFeeRate: maxFeeRate, 1519 FeeSuggestion: feeSuggestion, 1520 Options: map[string]string{ 1521 multiSplitKey: "false", 1522 }, 1523 }, 1524 maxLock: 46e5, 1525 bondReserves: 12e5, 1526 utxos: []walletjson.ListUnspentResult{ 1527 { 1528 Confirmations: 1, 1529 Spendable: true, 1530 TxID: txIDs[2], 1531 Account: tAcctName, 1532 ScriptPubKey: scriptPubKeys[2], 1533 Address: addresses[2], 1534 Amount: 1e6 / 1e8, 1535 Vout: 0, 1536 }, 1537 { 1538 Confirmations: 1, 1539 Spendable: true, 1540 TxID: txIDs[0], 1541 Account: tAcctName, 1542 ScriptPubKey: scriptPubKeys[0], 1543 Address: addresses[0], 1544 Amount: 11e5 / 1e8, 1545 Vout: 0, 1546 }, 1547 { 1548 Confirmations: 1, 1549 Spendable: true, 1550 TxID: txIDs[1], 1551 Account: tAcctName, 1552 ScriptPubKey: scriptPubKeys[1], 1553 Address: addresses[1], 1554 Amount: 25e5 / 1e8, 1555 Vout: 0, 1556 }, 1557 }, 1558 balance: 46e5, 1559 expectedCoins: []asset.Coins{ 1560 {newOutput(&txHashes[0], 0, 11e5, wire.TxTreeRegular)}, 1561 }, 1562 expectedRedeemScripts: [][]dex.Bytes{ 1563 {nil}, 1564 }, 1565 expectedLockedCoins: []*wire.OutPoint{ 1566 wire.NewOutPoint(&txHashes[0], 0, wire.TxTreeRegular), 1567 }, 1568 }, 1569 { // "split not allowed, need to fund in increasing order" 1570 name: "no split, need to fund in increasing order", 1571 multiOrder: &asset.MultiOrder{ 1572 Values: []*asset.MultiOrderValue{ 1573 { 1574 Value: 2e6, 1575 MaxSwapCount: 2, 1576 }, 1577 { 1578 Value: 11e5, 1579 MaxSwapCount: 1, 1580 }, 1581 { 1582 Value: 9e5, 1583 MaxSwapCount: 1, 1584 }, 1585 }, 1586 MaxFeeRate: maxFeeRate, 1587 FeeSuggestion: feeSuggestion, 1588 Options: map[string]string{ 1589 multiSplitKey: "false", 1590 }, 1591 }, 1592 maxLock: 50e5, 1593 utxos: []walletjson.ListUnspentResult{ 1594 { 1595 Confirmations: 1, 1596 Spendable: true, 1597 TxID: txIDs[0], 1598 Account: tAcctName, 1599 ScriptPubKey: scriptPubKeys[0], 1600 Address: addresses[0], 1601 Amount: 11e5 / 1e8, 1602 Vout: 0, 1603 }, 1604 { 1605 Confirmations: 1, 1606 Spendable: true, 1607 TxID: txIDs[1], 1608 Account: tAcctName, 1609 ScriptPubKey: scriptPubKeys[1], 1610 Address: addresses[1], 1611 Amount: 13e5 / 1e8, 1612 Vout: 0, 1613 }, 1614 { 1615 Confirmations: 1, 1616 Spendable: true, 1617 TxID: txIDs[2], 1618 Account: tAcctName, 1619 ScriptPubKey: scriptPubKeys[2], 1620 Address: addresses[2], 1621 Amount: 26e5 / 1e8, 1622 Vout: 0, 1623 }, 1624 }, 1625 balance: 50e5, 1626 expectedCoins: []asset.Coins{ 1627 {newOutput(&txHashes[2], 0, 26e5, wire.TxTreeRegular)}, 1628 {newOutput(&txHashes[1], 0, 13e5, wire.TxTreeRegular)}, 1629 {newOutput(&txHashes[0], 0, 11e5, wire.TxTreeRegular)}, 1630 }, 1631 expectedRedeemScripts: [][]dex.Bytes{ 1632 {nil}, 1633 {nil}, 1634 {nil}, 1635 }, 1636 expectedLockedCoins: []*wire.OutPoint{ 1637 wire.NewOutPoint(&txHashes[0], 0, wire.TxTreeRegular), 1638 wire.NewOutPoint(&txHashes[1], 0, wire.TxTreeRegular), 1639 wire.NewOutPoint(&txHashes[2], 0, wire.TxTreeRegular), 1640 }, 1641 }, 1642 { // "split allowed, no split required" 1643 name: "split allowed, no split required", 1644 multiOrder: &asset.MultiOrder{ 1645 Values: []*asset.MultiOrderValue{ 1646 { 1647 Value: 1e6, 1648 MaxSwapCount: 1, 1649 }, 1650 { 1651 Value: 2e6, 1652 MaxSwapCount: 2, 1653 }, 1654 }, 1655 MaxFeeRate: maxFeeRate, 1656 FeeSuggestion: feeSuggestion, 1657 Options: map[string]string{ 1658 multiSplitKey: "true", 1659 }, 1660 }, 1661 allOrNothing: false, 1662 maxLock: 43e5, 1663 utxos: []walletjson.ListUnspentResult{ 1664 { 1665 Confirmations: 1, 1666 Spendable: true, 1667 TxID: txIDs[2], 1668 Account: tAcctName, 1669 ScriptPubKey: scriptPubKeys[2], 1670 Address: addresses[2], 1671 Amount: 1e6 / 1e8, 1672 Vout: 0, 1673 }, 1674 { 1675 Confirmations: 1, 1676 Spendable: true, 1677 TxID: txIDs[0], 1678 Account: tAcctName, 1679 ScriptPubKey: scriptPubKeys[0], 1680 Address: addresses[0], 1681 Amount: 11e5 / 1e8, 1682 Vout: 0, 1683 }, 1684 { 1685 Confirmations: 1, 1686 Spendable: true, 1687 TxID: txIDs[1], 1688 Account: tAcctName, 1689 ScriptPubKey: scriptPubKeys[1], 1690 Address: addresses[1], 1691 Amount: 22e5 / 1e8, 1692 Vout: 0, 1693 }, 1694 }, 1695 balance: 43e5, 1696 expectedCoins: []asset.Coins{ 1697 {newOutput(&txHashes[0], 0, 11e5, wire.TxTreeRegular)}, 1698 {newOutput(&txHashes[1], 0, 22e5, wire.TxTreeRegular)}, 1699 }, 1700 expectedRedeemScripts: [][]dex.Bytes{ 1701 {nil}, 1702 {nil}, 1703 }, 1704 expectedLockedCoins: []*wire.OutPoint{ 1705 wire.NewOutPoint(&txHashes[1], 0, wire.TxTreeRegular), 1706 wire.NewOutPoint(&txHashes[2], 0, wire.TxTreeRegular), 1707 }, 1708 }, 1709 { // "split allowed, can fund both with split" 1710 name: "split allowed, can fund both with split", 1711 multiOrder: &asset.MultiOrder{ 1712 Values: []*asset.MultiOrderValue{ 1713 { 1714 Value: 15e5, 1715 MaxSwapCount: 2, 1716 }, 1717 { 1718 Value: 15e5, 1719 MaxSwapCount: 2, 1720 }, 1721 }, 1722 MaxFeeRate: maxFeeRate, 1723 FeeSuggestion: feeSuggestion, 1724 Options: map[string]string{ 1725 multiSplitKey: "true", 1726 }, 1727 }, 1728 utxos: []walletjson.ListUnspentResult{ 1729 { 1730 Confirmations: 1, 1731 Spendable: true, 1732 TxID: txIDs[0], 1733 Account: tAcctName, 1734 ScriptPubKey: scriptPubKeys[0], 1735 Address: addresses[0], 1736 Amount: 1e6 / 1e8, 1737 Vout: 0, 1738 }, 1739 { 1740 Confirmations: 1, 1741 Spendable: true, 1742 TxID: txIDs[1], 1743 Account: tAcctName, 1744 ScriptPubKey: scriptPubKeys[1], 1745 Address: addresses[1], 1746 Amount: (2*float64(requiredForOrder(15e5, 2)) + float64(expectedSplitFee(2, 2)) - 1e6) / 1e8, 1747 Vout: 0, 1748 }, 1749 }, 1750 maxLock: 2*uint64(requiredForOrder(15e5, 2)) + expectedSplitFee(2, 2), 1751 balance: 2*uint64(requiredForOrder(15e5, 2)) + expectedSplitFee(2, 2), 1752 expectSendRawTx: true, 1753 expectedInputs: []*wire.TxIn{ 1754 { 1755 PreviousOutPoint: wire.OutPoint{ 1756 Hash: txHashes[1], 1757 Index: 0, 1758 }, 1759 }, 1760 { 1761 PreviousOutPoint: wire.OutPoint{ 1762 Hash: txHashes[0], 1763 Index: 0, 1764 }, 1765 }, 1766 }, 1767 expectedOutputs: []*wire.TxOut{ 1768 wire.NewTxOut(requiredForOrder(15e5, 2), []byte{}), 1769 wire.NewTxOut(requiredForOrder(15e5, 2), []byte{}), 1770 }, 1771 expectedSplitFee: expectedSplitFee(2, 2), 1772 expectedRedeemScripts: [][]dex.Bytes{ 1773 {nil}, 1774 {nil}, 1775 }, 1776 }, 1777 { // "split allowed, cannot fund both with split" 1778 name: "split allowed, cannot fund both with split", 1779 multiOrder: &asset.MultiOrder{ 1780 Values: []*asset.MultiOrderValue{ 1781 { 1782 Value: 15e5, 1783 MaxSwapCount: 2, 1784 }, 1785 { 1786 Value: 15e5, 1787 MaxSwapCount: 2, 1788 }, 1789 }, 1790 MaxFeeRate: maxFeeRate, 1791 FeeSuggestion: feeSuggestion, 1792 Options: map[string]string{ 1793 multiSplitKey: "true", 1794 }, 1795 }, 1796 utxos: []walletjson.ListUnspentResult{ 1797 { 1798 Confirmations: 1, 1799 Spendable: true, 1800 TxID: txIDs[0], 1801 Account: tAcctName, 1802 ScriptPubKey: scriptPubKeys[0], 1803 Address: addresses[0], 1804 Amount: 1e6 / 1e8, 1805 Vout: 0, 1806 }, 1807 { 1808 Confirmations: 1, 1809 Spendable: true, 1810 TxID: txIDs[1], 1811 Account: tAcctName, 1812 ScriptPubKey: scriptPubKeys[1], 1813 Address: addresses[1], 1814 Amount: (2*float64(requiredForOrder(15e5, 2)) + float64(expectedSplitFee(2, 2)) - 1e6) / 1e8, 1815 Vout: 0, 1816 }, 1817 }, 1818 maxLock: 2*uint64(requiredForOrder(15e5, 2)) + expectedSplitFee(2, 2) - 1, 1819 balance: 2*uint64(requiredForOrder(15e5, 2)) + expectedSplitFee(2, 2) - 1, 1820 expectErr: true, 1821 }, 1822 { // "can fund both with split and respect maxLock" 1823 name: "can fund both with split and respect maxLock", 1824 multiOrder: &asset.MultiOrder{ 1825 Values: []*asset.MultiOrderValue{ 1826 { 1827 Value: 15e5, 1828 MaxSwapCount: 2, 1829 }, 1830 { 1831 Value: 15e5, 1832 MaxSwapCount: 2, 1833 }, 1834 }, 1835 MaxFeeRate: maxFeeRate, 1836 FeeSuggestion: feeSuggestion, 1837 Options: map[string]string{ 1838 multiSplitKey: "true", 1839 }, 1840 }, 1841 utxos: []walletjson.ListUnspentResult{ 1842 { 1843 Confirmations: 1, 1844 Spendable: true, 1845 TxID: txIDs[0], 1846 Account: tAcctName, 1847 ScriptPubKey: scriptPubKeys[0], 1848 Address: addresses[0], 1849 Amount: float64(50e5) / 1e8, 1850 Vout: 0, 1851 }, 1852 }, 1853 balance: 50e5, 1854 maxLock: 2*uint64(requiredForOrder(15e5, 2)) + expectedSplitFee(1, 2), 1855 expectSendRawTx: true, 1856 expectedInputs: []*wire.TxIn{ 1857 { 1858 PreviousOutPoint: wire.OutPoint{ 1859 Hash: txHashes[0], 1860 Index: 0, 1861 }, 1862 }, 1863 }, 1864 expectedOutputs: []*wire.TxOut{ 1865 wire.NewTxOut(requiredForOrder(15e5, 2), []byte{}), 1866 wire.NewTxOut(requiredForOrder(15e5, 2), []byte{}), 1867 }, 1868 expectedChange: 50e5 - (2*uint64(requiredForOrder(15e5, 2)) + expectedSplitFee(1, 3)), 1869 expectedSplitFee: expectedSplitFee(1, 3), 1870 expectedRedeemScripts: [][]dex.Bytes{ 1871 {nil}, 1872 {nil}, 1873 }, 1874 }, 1875 { // "cannot fund both with split and respect maxLock" 1876 name: "cannot fund both with split and respect maxLock", 1877 multiOrder: &asset.MultiOrder{ 1878 Values: []*asset.MultiOrderValue{ 1879 { 1880 Value: 15e5, 1881 MaxSwapCount: 2, 1882 }, 1883 { 1884 Value: 15e5, 1885 MaxSwapCount: 2, 1886 }, 1887 }, 1888 MaxFeeRate: maxFeeRate, 1889 FeeSuggestion: feeSuggestion, 1890 Options: map[string]string{ 1891 multiSplitKey: "true", 1892 }, 1893 }, 1894 utxos: []walletjson.ListUnspentResult{ 1895 { 1896 Confirmations: 1, 1897 Spendable: true, 1898 TxID: txIDs[0], 1899 Account: tAcctName, 1900 ScriptPubKey: scriptPubKeys[0], 1901 Address: addresses[0], 1902 Amount: float64(50e5) / 1e8, 1903 Vout: 0, 1904 }, 1905 }, 1906 balance: 50e5, 1907 maxLock: 2*uint64(requiredForOrder(15e5, 2)) + expectedSplitFee(1, 2) - 1, 1908 expectErr: true, 1909 }, 1910 { // "split allowed, can fund both with split with bond reserves" 1911 name: "split allowed, can fund both with split with bond reserves", 1912 multiOrder: &asset.MultiOrder{ 1913 Values: []*asset.MultiOrderValue{ 1914 { 1915 Value: 15e5, 1916 MaxSwapCount: 2, 1917 }, 1918 { 1919 Value: 15e5, 1920 MaxSwapCount: 2, 1921 }, 1922 }, 1923 MaxFeeRate: maxFeeRate, 1924 FeeSuggestion: feeSuggestion, 1925 Options: map[string]string{ 1926 multiSplitKey: "true", 1927 }, 1928 }, 1929 bondReserves: 2e6, 1930 utxos: []walletjson.ListUnspentResult{ 1931 { 1932 Confirmations: 1, 1933 Spendable: true, 1934 TxID: txIDs[0], 1935 Account: tAcctName, 1936 ScriptPubKey: scriptPubKeys[0], 1937 Address: addresses[0], 1938 Amount: (2*float64(requiredForOrder(15e5, 2)) + 2e6 + float64(expectedSplitFee(1, 3))) / 1e8, 1939 Vout: 0, 1940 }, 1941 }, 1942 balance: 2e6 + 2*uint64(requiredForOrder(15e5, 2)) + expectedSplitFee(1, 3), 1943 maxLock: 2e6 + 2*uint64(requiredForOrder(15e5, 2)) + expectedSplitFee(1, 3), 1944 expectSendRawTx: true, 1945 expectedInputs: []*wire.TxIn{ 1946 { 1947 PreviousOutPoint: wire.OutPoint{ 1948 Hash: txHashes[0], 1949 Index: 0, 1950 }, 1951 }, 1952 }, 1953 expectedOutputs: []*wire.TxOut{ 1954 wire.NewTxOut(requiredForOrder(15e5, 2), []byte{}), 1955 wire.NewTxOut(requiredForOrder(15e5, 2), []byte{}), 1956 }, 1957 expectedChange: 2e6, 1958 expectedSplitFee: expectedSplitFee(1, 3), 1959 expectedRedeemScripts: [][]dex.Bytes{ 1960 {nil}, 1961 {nil}, 1962 }, 1963 }, 1964 { // "split allowed, cannot fund both with split and keep and bond reserves" 1965 name: "split allowed, cannot fund both with split and keep and bond reserves", 1966 multiOrder: &asset.MultiOrder{ 1967 Values: []*asset.MultiOrderValue{ 1968 { 1969 Value: 15e5, 1970 MaxSwapCount: 2, 1971 }, 1972 { 1973 Value: 15e5, 1974 MaxSwapCount: 2, 1975 }, 1976 }, 1977 MaxFeeRate: maxFeeRate, 1978 FeeSuggestion: feeSuggestion, 1979 Options: map[string]string{ 1980 multiSplitKey: "true", 1981 }, 1982 }, 1983 bondReserves: 2e6, 1984 utxos: []walletjson.ListUnspentResult{ 1985 { 1986 Confirmations: 1, 1987 Spendable: true, 1988 TxID: txIDs[0], 1989 Account: tAcctName, 1990 ScriptPubKey: scriptPubKeys[0], 1991 Address: addresses[0], 1992 Amount: ((2*float64(requiredForOrder(15e5, 2)) + 2e6 + float64(expectedSplitFee(1, 3))) / 1e8) - 1/1e8, 1993 Vout: 0, 1994 }, 1995 }, 1996 balance: 2e6 + 2*uint64(requiredForOrder(15e5, 2)) + expectedSplitFee(1, 3) - 1, 1997 maxLock: 2e6 + 2*uint64(requiredForOrder(15e5, 2)) + expectedSplitFee(1, 3) - 1, 1998 expectErr: true, 1999 }, 2000 { // "split with buffer" 2001 name: "split with buffer", 2002 multiOrder: &asset.MultiOrder{ 2003 Values: []*asset.MultiOrderValue{ 2004 { 2005 Value: 15e5, 2006 MaxSwapCount: 2, 2007 }, 2008 { 2009 Value: 15e5, 2010 MaxSwapCount: 2, 2011 }, 2012 }, 2013 MaxFeeRate: maxFeeRate, 2014 FeeSuggestion: feeSuggestion, 2015 Options: map[string]string{ 2016 multiSplitKey: "true", 2017 multiSplitBufferKey: "10", 2018 }, 2019 }, 2020 utxos: []walletjson.ListUnspentResult{ 2021 { 2022 Confirmations: 1, 2023 Spendable: true, 2024 TxID: txIDs[0], 2025 Account: tAcctName, 2026 ScriptPubKey: scriptPubKeys[0], 2027 Address: addresses[0], 2028 Amount: (2*float64(requiredForOrder(15e5, 2)*110/100) + float64(expectedSplitFee(1, 2))) / 1e8, 2029 Vout: 0, 2030 }, 2031 }, 2032 balance: 2*uint64(requiredForOrder(15e5, 2)*110/100) + expectedSplitFee(1, 2), 2033 maxLock: 2*uint64(requiredForOrder(15e5, 2)*110/100) + expectedSplitFee(1, 2), 2034 expectSendRawTx: true, 2035 expectedInputs: []*wire.TxIn{ 2036 { 2037 PreviousOutPoint: wire.OutPoint{ 2038 Hash: txHashes[0], 2039 Index: 0, 2040 }, 2041 }, 2042 }, 2043 expectedOutputs: []*wire.TxOut{ 2044 wire.NewTxOut(requiredForOrder(15e5, 2)*110/100, []byte{}), 2045 wire.NewTxOut(requiredForOrder(15e5, 2)*110/100, []byte{}), 2046 }, 2047 expectedSplitFee: expectedSplitFee(1, 2), 2048 expectedRedeemScripts: [][]dex.Bytes{ 2049 {nil}, 2050 {nil}, 2051 }, 2052 }, 2053 { // "split, maxLock too low to fund buffer" 2054 name: "split, maxLock too low to fund buffer", 2055 multiOrder: &asset.MultiOrder{ 2056 Values: []*asset.MultiOrderValue{ 2057 { 2058 Value: 15e5, 2059 MaxSwapCount: 2, 2060 }, 2061 { 2062 Value: 15e5, 2063 MaxSwapCount: 2, 2064 }, 2065 }, 2066 MaxFeeRate: maxFeeRate, 2067 FeeSuggestion: feeSuggestion, 2068 Options: map[string]string{ 2069 multiSplitKey: "true", 2070 multiSplitBufferKey: "10", 2071 }, 2072 }, 2073 utxos: []walletjson.ListUnspentResult{ 2074 { 2075 Confirmations: 1, 2076 Spendable: true, 2077 TxID: txIDs[0], 2078 Account: tAcctName, 2079 ScriptPubKey: scriptPubKeys[0], 2080 Address: addresses[0], 2081 Amount: (2*float64(requiredForOrder(15e5, 2)*110/100) + float64(expectedSplitFee(1, 2))) / 1e8, 2082 Vout: 0, 2083 }, 2084 }, 2085 balance: 2*uint64(requiredForOrder(15e5, 2)*110/100) + expectedSplitFee(1, 2), 2086 maxLock: 2*uint64(requiredForOrder(15e5, 2)*110/100) + expectedSplitFee(1, 2) - 1, 2087 expectErr: true, 2088 }, 2089 { // "only one order needs a split, rest can be funded without" 2090 name: "only one order needs a split, rest can be funded without", 2091 multiOrder: &asset.MultiOrder{ 2092 Values: []*asset.MultiOrderValue{ 2093 { 2094 Value: 1e6, 2095 MaxSwapCount: 2, 2096 }, 2097 { 2098 Value: 1e6, 2099 MaxSwapCount: 2, 2100 }, 2101 { 2102 Value: 1e6, 2103 MaxSwapCount: 2, 2104 }, 2105 }, 2106 MaxFeeRate: maxFeeRate, 2107 FeeSuggestion: feeSuggestion, 2108 Options: map[string]string{ 2109 multiSplitKey: "true", 2110 }, 2111 }, 2112 utxos: []walletjson.ListUnspentResult{ 2113 { 2114 Confirmations: 1, 2115 Spendable: true, 2116 TxID: txIDs[0], 2117 Account: tAcctName, 2118 ScriptPubKey: scriptPubKeys[0], 2119 Address: addresses[0], 2120 Amount: 12e5 / 1e8, 2121 Vout: 0, 2122 }, 2123 { 2124 Confirmations: 1, 2125 Spendable: true, 2126 TxID: txIDs[1], 2127 Account: tAcctName, 2128 ScriptPubKey: scriptPubKeys[1], 2129 Address: addresses[1], 2130 Amount: 12e5 / 1e8, 2131 Vout: 0, 2132 }, 2133 { 2134 Confirmations: 1, 2135 Spendable: true, 2136 TxID: txIDs[2], 2137 Account: tAcctName, 2138 ScriptPubKey: scriptPubKeys[2], 2139 Address: addresses[2], 2140 Amount: 120e5 / 1e8, 2141 Vout: 0, 2142 }, 2143 }, 2144 maxLock: 50e5, 2145 balance: 144e5, 2146 expectSendRawTx: true, 2147 expectedInputs: []*wire.TxIn{ 2148 { 2149 PreviousOutPoint: wire.OutPoint{ 2150 Hash: txHashes[2], 2151 Index: 0, 2152 }, 2153 }, 2154 }, 2155 expectedOutputs: []*wire.TxOut{ 2156 wire.NewTxOut(requiredForOrder(1e6, 2), []byte{}), 2157 wire.NewTxOut(120e5-requiredForOrder(1e6, 2)-int64(expectedSplitFee(1, 2)), []byte{}), 2158 }, 2159 expectedSplitFee: expectedSplitFee(1, 2), 2160 expectedRedeemScripts: [][]dex.Bytes{ 2161 {nil}, 2162 {nil}, 2163 {nil}, 2164 }, 2165 expectedCoins: []asset.Coins{ 2166 {newOutput(&txHashes[0], 0, 12e5, wire.TxTreeRegular)}, 2167 {newOutput(&txHashes[1], 0, 12e5, wire.TxTreeRegular)}, 2168 nil, 2169 }, 2170 }, 2171 { // "only one order needs a split due to bond reserves, rest funded without" 2172 name: "only one order needs a split due to bond reserves, rest funded without", 2173 multiOrder: &asset.MultiOrder{ 2174 Values: []*asset.MultiOrderValue{ 2175 { 2176 Value: 1e6, 2177 MaxSwapCount: 2, 2178 }, 2179 { 2180 Value: 1e6, 2181 MaxSwapCount: 2, 2182 }, 2183 { 2184 Value: 1e6, 2185 MaxSwapCount: 2, 2186 }, 2187 }, 2188 MaxFeeRate: maxFeeRate, 2189 FeeSuggestion: feeSuggestion, 2190 Options: map[string]string{ 2191 multiSplitKey: "true", 2192 }, 2193 }, 2194 utxos: []walletjson.ListUnspentResult{ 2195 { 2196 Confirmations: 1, 2197 Spendable: true, 2198 TxID: txIDs[0], 2199 Account: tAcctName, 2200 ScriptPubKey: scriptPubKeys[0], 2201 Address: addresses[0], 2202 Amount: 12e5 / 1e8, 2203 Vout: 0, 2204 }, 2205 { 2206 Confirmations: 1, 2207 Spendable: true, 2208 TxID: txIDs[1], 2209 Account: tAcctName, 2210 ScriptPubKey: scriptPubKeys[1], 2211 Address: addresses[1], 2212 Amount: 12e5 / 1e8, 2213 Vout: 0, 2214 }, 2215 { 2216 Confirmations: 1, 2217 Spendable: true, 2218 TxID: txIDs[2], 2219 Account: tAcctName, 2220 ScriptPubKey: scriptPubKeys[2], 2221 Address: addresses[2], 2222 Amount: 120e5 / 1e8, 2223 Vout: 0, 2224 }, 2225 }, 2226 maxLock: 0, 2227 bondReserves: 1e6, 2228 balance: 144e5, 2229 expectSendRawTx: true, 2230 expectedInputs: []*wire.TxIn{ 2231 { 2232 PreviousOutPoint: wire.OutPoint{ 2233 Hash: txHashes[2], 2234 Index: 0, 2235 }, 2236 }, 2237 }, 2238 expectedOutputs: []*wire.TxOut{ 2239 wire.NewTxOut(requiredForOrder(1e6, 2), []byte{}), 2240 wire.NewTxOut(120e5-requiredForOrder(1e6, 2)-int64(expectedSplitFee(1, 2)), []byte{}), 2241 }, 2242 expectedSplitFee: expectedSplitFee(1, 2), 2243 expectedRedeemScripts: [][]dex.Bytes{ 2244 {nil}, 2245 {nil}, 2246 {nil}, 2247 }, 2248 expectedCoins: []asset.Coins{ 2249 {newOutput(&txHashes[0], 0, 12e5, wire.TxTreeRegular)}, 2250 {newOutput(&txHashes[1], 0, 12e5, wire.TxTreeRegular)}, 2251 nil, 2252 }, 2253 }, 2254 { // "only one order needs a split due to maxLock, rest funded without" 2255 name: "only one order needs a split due to maxLock, rest funded without", 2256 multiOrder: &asset.MultiOrder{ 2257 Values: []*asset.MultiOrderValue{ 2258 { 2259 Value: 1e6, 2260 MaxSwapCount: 2, 2261 }, 2262 { 2263 Value: 1e6, 2264 MaxSwapCount: 2, 2265 }, 2266 { 2267 Value: 1e6, 2268 MaxSwapCount: 2, 2269 }, 2270 }, 2271 MaxFeeRate: maxFeeRate, 2272 FeeSuggestion: feeSuggestion, 2273 Options: map[string]string{ 2274 multiSplitKey: "true", 2275 }, 2276 }, 2277 utxos: []walletjson.ListUnspentResult{ 2278 { 2279 Confirmations: 1, 2280 Spendable: true, 2281 TxID: txIDs[0], 2282 Account: tAcctName, 2283 ScriptPubKey: scriptPubKeys[0], 2284 Address: addresses[0], 2285 Amount: 12e5 / 1e8, 2286 Vout: 0, 2287 }, 2288 { 2289 Confirmations: 1, 2290 Spendable: true, 2291 TxID: txIDs[1], 2292 Account: tAcctName, 2293 ScriptPubKey: scriptPubKeys[1], 2294 Address: addresses[1], 2295 Amount: 12e5 / 1e8, 2296 Vout: 0, 2297 }, 2298 { 2299 Confirmations: 1, 2300 Spendable: true, 2301 TxID: txIDs[2], 2302 Account: tAcctName, 2303 ScriptPubKey: scriptPubKeys[2], 2304 Address: addresses[2], 2305 Amount: 9e5 / 1e8, 2306 Vout: 0, 2307 }, 2308 { 2309 Confirmations: 1, 2310 Spendable: true, 2311 TxID: txIDs[3], 2312 Account: tAcctName, 2313 ScriptPubKey: scriptPubKeys[3], 2314 Address: addresses[3], 2315 Amount: 9e5 / 1e8, 2316 Vout: 0, 2317 }, 2318 }, 2319 maxLock: 35e5, 2320 bondReserves: 0, 2321 balance: 42e5, 2322 expectSendRawTx: true, 2323 expectedInputs: []*wire.TxIn{ 2324 { 2325 PreviousOutPoint: wire.OutPoint{ 2326 Hash: txHashes[3], 2327 Index: 0, 2328 }, 2329 }, 2330 { 2331 PreviousOutPoint: wire.OutPoint{ 2332 Hash: txHashes[2], 2333 Index: 0, 2334 }, 2335 }, 2336 }, 2337 expectedOutputs: []*wire.TxOut{ 2338 wire.NewTxOut(requiredForOrder(1e6, 2), []byte{}), 2339 wire.NewTxOut(18e5-requiredForOrder(1e6, 2)-int64(expectedSplitFee(2, 2)), []byte{}), 2340 }, 2341 expectedSplitFee: expectedSplitFee(2, 2), 2342 expectedRedeemScripts: [][]dex.Bytes{ 2343 {nil}, 2344 {nil}, 2345 {nil}, 2346 }, 2347 expectedCoins: []asset.Coins{ 2348 {newOutput(&txHashes[0], 0, 12e5, wire.TxTreeRegular)}, 2349 {newOutput(&txHashes[1], 0, 12e5, wire.TxTreeRegular)}, 2350 nil, 2351 }, 2352 }, 2353 } 2354 2355 for _, test := range tests { 2356 node.unspent = test.utxos 2357 node.newAddr = tPKHAddr 2358 node.changeAddr = tPKHAddr 2359 node.signFunc = func(msgTx *wire.MsgTx) (*wire.MsgTx, bool, error) { 2360 return signFunc(msgTx, dexdcr.P2PKHSigScriptSize) 2361 } 2362 node.sentRawTx = nil 2363 node.lockedCoins = nil 2364 node.balanceResult = &walletjson.GetBalanceResult{ 2365 Balances: []walletjson.GetAccountBalanceResult{ 2366 { 2367 AccountName: tAcctName, 2368 Spendable: toDCR(test.balance), 2369 }, 2370 }, 2371 } 2372 wallet.fundingCoins = make(map[outPoint]*fundingCoin) 2373 wallet.bondReserves.Store(test.bondReserves) 2374 2375 allCoins, _, splitFee, err := wallet.FundMultiOrder(test.multiOrder, test.maxLock) 2376 if test.expectErr { 2377 if err == nil { 2378 t.Fatalf("%s: no error returned", test.name) 2379 } 2380 if strings.Contains(err.Error(), "insufficient funds") { 2381 t.Fatalf("%s: unexpected insufficient funds error", test.name) 2382 } 2383 continue 2384 } 2385 if err != nil { 2386 t.Fatalf("%s: unexpected error: %v", test.name, err) 2387 } 2388 2389 if !test.expectSendRawTx { // no split 2390 if node.sentRawTx != nil { 2391 t.Fatalf("%s: unexpected transaction sent", test.name) 2392 } 2393 if len(allCoins) != len(test.expectedCoins) { 2394 t.Fatalf("%s: expected %d coins, got %d", test.name, len(test.expectedCoins), len(allCoins)) 2395 } 2396 for i := range allCoins { 2397 if len(allCoins[i]) != len(test.expectedCoins[i]) { 2398 t.Fatalf("%s: expected %d coins in set %d, got %d", test.name, len(test.expectedCoins[i]), i, len(allCoins[i])) 2399 } 2400 actual := allCoins[i] 2401 expected := test.expectedCoins[i] 2402 sort.Slice(actual, func(i, j int) bool { 2403 return bytes.Compare(actual[i].ID(), actual[j].ID()) < 0 2404 }) 2405 sort.Slice(expected, func(i, j int) bool { 2406 return bytes.Compare(expected[i].ID(), expected[j].ID()) < 0 2407 }) 2408 for j := range actual { 2409 if !bytes.Equal(actual[j].ID(), expected[j].ID()) { 2410 t.Fatalf("%s: unexpected coin in set %d. expected %s, got %s", test.name, i, expected[j].ID(), actual[j].ID()) 2411 } 2412 if actual[j].Value() != expected[j].Value() { 2413 t.Fatalf("%s: unexpected coin value in set %d. expected %d, got %d", test.name, i, expected[j].Value(), actual[j].Value()) 2414 } 2415 } 2416 } 2417 } else { // expectSplit 2418 if node.sentRawTx == nil { 2419 t.Fatalf("%s: SendRawTransaction not called", test.name) 2420 } 2421 if len(node.sentRawTx.TxIn) != len(test.expectedInputs) { 2422 t.Fatalf("%s: expected %d inputs, got %d", test.name, len(test.expectedInputs), len(node.sentRawTx.TxIn)) 2423 } 2424 for i, actualIn := range node.sentRawTx.TxIn { 2425 expectedIn := test.expectedInputs[i] 2426 if !bytes.Equal(actualIn.PreviousOutPoint.Hash[:], expectedIn.PreviousOutPoint.Hash[:]) { 2427 t.Fatalf("%s: unexpected input %d hash. expected %s, got %s", test.name, i, expectedIn.PreviousOutPoint.Hash, actualIn.PreviousOutPoint.Hash) 2428 } 2429 if actualIn.PreviousOutPoint.Index != expectedIn.PreviousOutPoint.Index { 2430 t.Fatalf("%s: unexpected input %d index. expected %d, got %d", test.name, i, expectedIn.PreviousOutPoint.Index, actualIn.PreviousOutPoint.Index) 2431 } 2432 } 2433 expectedNumOutputs := len(test.expectedOutputs) 2434 if test.expectedChange > 0 { 2435 expectedNumOutputs++ 2436 } 2437 if len(node.sentRawTx.TxOut) != expectedNumOutputs { 2438 t.Fatalf("%s: expected %d outputs, got %d", test.name, expectedNumOutputs, len(node.sentRawTx.TxOut)) 2439 } 2440 2441 for i, expectedOut := range test.expectedOutputs { 2442 actualOut := node.sentRawTx.TxOut[i] 2443 if actualOut.Value != expectedOut.Value { 2444 t.Fatalf("%s: unexpected output %d value. expected %d, got %d", test.name, i, expectedOut.Value, actualOut.Value) 2445 } 2446 } 2447 if test.expectedChange > 0 { 2448 actualOut := node.sentRawTx.TxOut[len(node.sentRawTx.TxOut)-1] 2449 if uint64(actualOut.Value) != test.expectedChange { 2450 t.Fatalf("%s: unexpected change value. expected %d, got %d", test.name, test.expectedChange, actualOut.Value) 2451 } 2452 } 2453 2454 if len(test.multiOrder.Values) != len(allCoins) { 2455 t.Fatalf("%s: expected %d coins, got %d", test.name, len(test.multiOrder.Values), len(allCoins)) 2456 } 2457 splitTxID := node.sentRawTx.TxHash() 2458 2459 // This means all coins are split outputs 2460 if test.expectedCoins == nil { 2461 for i, actualCoin := range allCoins { 2462 actualOut := actualCoin[0].(*output) 2463 expectedOut := node.sentRawTx.TxOut[i] 2464 if uint64(expectedOut.Value) != actualOut.value { 2465 t.Fatalf("%s: unexpected output %d value. expected %d, got %d", test.name, i, expectedOut.Value, actualOut.value) 2466 } 2467 if !bytes.Equal(actualOut.pt.txHash[:], splitTxID[:]) { 2468 t.Fatalf("%s: unexpected output %d txid. expected %s, got %s", test.name, i, splitTxID, actualOut.pt.txHash) 2469 } 2470 } 2471 } else { 2472 var splitTxOutputIndex int 2473 for i := range allCoins { 2474 actual := allCoins[i] 2475 expected := test.expectedCoins[i] 2476 2477 // This means the coins are the split outputs 2478 if expected == nil { 2479 actualOut := actual[0].(*output) 2480 expectedOut := node.sentRawTx.TxOut[splitTxOutputIndex] 2481 if uint64(expectedOut.Value) != actualOut.value { 2482 t.Fatalf("%s: unexpected output %d value. expected %d, got %d", test.name, i, expectedOut.Value, actualOut.value) 2483 } 2484 if !bytes.Equal(actualOut.pt.txHash[:], splitTxID[:]) { 2485 t.Fatalf("%s: unexpected output %d txid. expected %s, got %s", test.name, i, splitTxID, actualOut.pt.txHash) 2486 } 2487 splitTxOutputIndex++ 2488 continue 2489 } 2490 2491 if len(actual) != len(expected) { 2492 t.Fatalf("%s: expected %d coins in set %d, got %d", test.name, len(test.expectedCoins[i]), i, len(allCoins[i])) 2493 } 2494 sort.Slice(actual, func(i, j int) bool { 2495 return bytes.Compare(actual[i].ID(), actual[j].ID()) < 0 2496 }) 2497 sort.Slice(expected, func(i, j int) bool { 2498 return bytes.Compare(expected[i].ID(), expected[j].ID()) < 0 2499 }) 2500 for j := range actual { 2501 if !bytes.Equal(actual[j].ID(), expected[j].ID()) { 2502 t.Fatalf("%s: unexpected coin in set %d. expected %s, got %s", test.name, i, expected[j].ID(), actual[j].ID()) 2503 } 2504 if actual[j].Value() != expected[j].Value() { 2505 t.Fatalf("%s: unexpected coin value in set %d. expected %d, got %d", test.name, i, expected[j].Value(), actual[j].Value()) 2506 } 2507 } 2508 } 2509 } 2510 2511 // Each split output should be locked 2512 if len(node.lockedCoins) != len(allCoins) { 2513 t.Fatalf("%s: expected %d locked coins, got %d", test.name, len(allCoins), len(node.lockedCoins)) 2514 } 2515 2516 } 2517 2518 // Check that the right coins are locked and in the fundingCoins map 2519 var totalNumCoins int 2520 for _, coins := range allCoins { 2521 totalNumCoins += len(coins) 2522 } 2523 if totalNumCoins != len(wallet.fundingCoins) { 2524 t.Fatalf("%s: expected %d funding coins in wallet, got %d", test.name, totalNumCoins, len(wallet.fundingCoins)) 2525 } 2526 //totalNumCoins += len(test.expectedInputs) 2527 if totalNumCoins != len(node.lockedCoins) { 2528 t.Fatalf("%s: expected %d locked coins, got %d", test.name, totalNumCoins, len(node.lockedCoins)) 2529 } 2530 lockedCoins := make(map[wire.OutPoint]any) 2531 for _, coin := range node.lockedCoins { 2532 lockedCoins[*coin] = true 2533 } 2534 checkLockedCoin := func(txHash chainhash.Hash, vout uint32) { 2535 if _, ok := lockedCoins[wire.OutPoint{Hash: txHash, Index: vout, Tree: wire.TxTreeRegular}]; !ok { 2536 t.Fatalf("%s: expected locked coin %s:%d not found", test.name, txHash, vout) 2537 } 2538 } 2539 checkFundingCoin := func(txHash chainhash.Hash, vout uint32) { 2540 if _, ok := wallet.fundingCoins[outPoint{txHash: txHash, vout: vout}]; !ok { 2541 t.Fatalf("%s: expected locked coin %s:%d not found in wallet", test.name, txHash, vout) 2542 } 2543 } 2544 for _, coins := range allCoins { 2545 for _, coin := range coins { 2546 // decode coin to output 2547 out := coin.(*output) 2548 checkLockedCoin(out.pt.txHash, out.pt.vout) 2549 checkFundingCoin(out.pt.txHash, out.pt.vout) 2550 } 2551 } 2552 //for _, expectedIn := range test.expectedInputs { 2553 // checkLockedCoin(expectedIn.PreviousOutPoint.Hash, expectedIn.PreviousOutPoint.Index) 2554 //} 2555 2556 if test.expectedSplitFee != splitFee { 2557 t.Fatalf("%s: unexpected split fee. expected %d, got %d", test.name, test.expectedSplitFee, splitFee) 2558 } 2559 } 2560 } 2561 2562 func TestFundEdges(t *testing.T) { 2563 wallet, node, shutdown := tNewWallet() 2564 defer shutdown() 2565 2566 swapVal := uint64(1e8) 2567 lots := swapVal / tLotSize 2568 2569 // Swap fees 2570 // 2571 // fee_rate: 24 atoms / byte (dex MaxFeeRate) 2572 // swap_size: 251 bytes 2573 // swap_size_base: 85 bytes (251 - 166 p2pkh input) 2574 // lot_size: 1e7 2575 // swap_value: 1e8 2576 // lots = swap_size / lot_size = 10 2577 // base_tx_bytes = (lots - 1) * swap_size + swap_size_base = 9 * 251 + 85 = 2344 2578 // base_fees = 56256 2579 // backing_bytes: 1x P2PKH inputs = dexdcr.P2PKHInputSize = 166 bytes 2580 // backing_fees: 166 * fee_rate(24 atoms/byte) = 3984 atoms 2581 // total_bytes = base_tx_bytes + backing_bytes = 2344 + 166 = 2510 2582 // total_fees: base_fees + backing_fees = 56256 + 3984 = 60240 atoms 2583 // OR total_bytes * fee_rate = 2510 * 24 = 60240 2584 // base_best_case_bytes = swap_size_base + (lots - 1) * swap_output_size (P2SHOutputSize) + backing_bytes 2585 // = 85 + 9*34 + 166 = 557 2586 const swapSize = 251 2587 const totalBytes = 2510 2588 const bestCaseBytes = swapSize 2589 const swapOutputSize = 34 2590 fees := uint64(totalBytes) * tDCR.MaxFeeRate 2591 p2pkhUnspent := walletjson.ListUnspentResult{ 2592 TxID: tTxID, 2593 Address: tPKHAddr.String(), 2594 Account: tAcctName, 2595 Amount: float64(swapVal+fees-1) / 1e8, // one atom less than needed 2596 Confirmations: 5, 2597 ScriptPubKey: hex.EncodeToString(tP2PKHScript), 2598 Spendable: true, 2599 } 2600 2601 node.unspent = []walletjson.ListUnspentResult{p2pkhUnspent} 2602 ord := &asset.Order{ 2603 Version: version, 2604 Value: swapVal, 2605 MaxSwapCount: lots, 2606 MaxFeeRate: tDCR.MaxFeeRate, 2607 FeeSuggestion: feeSuggestion, 2608 } 2609 2610 // MaxOrder will try a split tx, which will work with one lot less. 2611 var feeReduction uint64 = swapSize * tDCR.MaxFeeRate 2612 estFeeReduction := swapSize * feeSuggestion 2613 splitFees := splitTxBaggage * tDCR.MaxFeeRate 2614 checkMaxOrder(t, wallet, lots-1, swapVal-tLotSize, 2615 fees+splitFees-feeReduction, // max fees 2616 (totalBytes+splitTxBaggage)*feeSuggestion-estFeeReduction, // worst case 2617 (bestCaseBytes+splitTxBaggage)*feeSuggestion) // best case 2618 2619 _, _, _, err := wallet.FundOrder(ord) 2620 if err == nil { 2621 t.Fatalf("no error when not enough funds in single p2pkh utxo") 2622 } 2623 2624 // Now add the needed atoms and try again. The fees will reflect that the 2625 // split was skipped because insufficient available for splitFees. 2626 p2pkhUnspent.Amount = float64(swapVal+fees) / 1e8 2627 node.unspent = []walletjson.ListUnspentResult{p2pkhUnspent} 2628 2629 checkMaxOrder(t, wallet, lots, swapVal, fees, totalBytes*feeSuggestion, 2630 bestCaseBytes*feeSuggestion) 2631 2632 _, _, _, err = wallet.FundOrder(ord) 2633 if err != nil { 2634 t.Fatalf("should be enough to fund with a single p2pkh utxo: %v", err) 2635 } 2636 2637 // For a split transaction, we would need to cover the splitTxBaggage as 2638 // well. 2639 wallet.config().useSplitTx = true 2640 node.newAddr = tPKHAddr 2641 node.changeAddr = tPKHAddr 2642 node.signFunc = func(msgTx *wire.MsgTx) (*wire.MsgTx, bool, error) { 2643 return signFunc(msgTx, dexdcr.P2PKHSigScriptSize) 2644 } 2645 2646 fees = uint64(totalBytes+splitTxBaggage) * tDCR.MaxFeeRate 2647 v := swapVal + fees - 1 2648 node.unspent[0].Amount = float64(v) / 1e8 2649 coins, _, _, err := wallet.FundOrder(ord) 2650 if err != nil { 2651 t.Fatalf("error when skipping split tx because not enough to cover baggage: %v", err) 2652 } 2653 if coins[0].Value() != v { 2654 t.Fatalf("split performed when baggage wasn't covered") 2655 } 2656 // Now get the split. 2657 v = swapVal + fees 2658 node.unspent[0].Amount = float64(v) / 1e8 2659 2660 checkMaxOrder(t, wallet, lots, swapVal, fees, (totalBytes+splitTxBaggage)*feeSuggestion, 2661 (bestCaseBytes+splitTxBaggage)*feeSuggestion) // fees include split (did not fall back to no split) 2662 2663 coins, _, _, err = wallet.FundOrder(ord) 2664 if err != nil { 2665 t.Fatalf("error funding split tx: %v", err) 2666 } 2667 if coins[0].Value() == v { 2668 t.Fatalf("split performed when baggage wasn't covered") 2669 } 2670 2671 // Split transactions require a fee suggestion. 2672 // TODO: 2673 // 1.0: Error when no suggestion. 2674 // ord.FeeSuggestion = 0 2675 // _, _, err = wallet.FundOrder(ord) 2676 // if err == nil { 2677 // t.Fatalf("no error for no fee suggestions on split tx") 2678 // } 2679 ord.FeeSuggestion = tDCR.MaxFeeRate + 1 2680 _, _, _, err = wallet.FundOrder(ord) 2681 if err == nil { 2682 t.Fatalf("no error for high fee suggestions on split tx") 2683 } 2684 // Check success again. 2685 ord.FeeSuggestion = tDCR.MaxFeeRate 2686 _, _, _, err = wallet.FundOrder(ord) 2687 if err != nil { 2688 t.Fatalf("error fixing split tx: %v", err) 2689 } 2690 wallet.config().useSplitTx = false 2691 2692 // TODO: test version mismatch 2693 } 2694 2695 func TestSwap(t *testing.T) { 2696 wallet, node, shutdown := tNewWallet() 2697 defer shutdown() 2698 2699 swapVal := toAtoms(5) 2700 coins := asset.Coins{ 2701 newOutput(tTxHash, 0, toAtoms(3), wire.TxTreeRegular), 2702 newOutput(tTxHash, 0, toAtoms(3), wire.TxTreeRegular), 2703 } 2704 2705 privBytes, _ := hex.DecodeString("b07209eec1a8fb6cfe5cb6ace36567406971a75c330db7101fb21bc679bc5330") 2706 2707 node.changeAddr = tPKHAddr 2708 var err error 2709 node.privWIF, err = dcrutil.NewWIF(privBytes, tChainParams.PrivateKeyID, dcrec.STEcdsaSecp256k1) 2710 if err != nil { 2711 t.Fatalf("NewWIF error: %v", err) 2712 } 2713 2714 node.newAddr = tPKHAddr 2715 node.changeAddr = tPKHAddr 2716 2717 secretHash, _ := hex.DecodeString("5124208c80d33507befa517c08ed01aa8d33adbf37ecd70fb5f9352f7a51a88d") 2718 contract := &asset.Contract{ 2719 Address: tPKHAddr.String(), 2720 Value: swapVal, 2721 SecretHash: secretHash, 2722 LockTime: uint64(time.Now().Unix()), 2723 } 2724 2725 swaps := &asset.Swaps{ 2726 Inputs: coins, 2727 Contracts: []*asset.Contract{contract}, 2728 LockChange: true, 2729 FeeRate: tDCR.MaxFeeRate, 2730 } 2731 2732 // Aim for 3 signature cycles. 2733 sigSizer := 0 2734 signFunc := func(msgTx *wire.MsgTx) (*wire.MsgTx, bool, error) { 2735 // Set the sigScripts to random bytes of the correct length for spending a 2736 // p2pkh output. 2737 scriptSize := dexdcr.P2PKHSigScriptSize 2738 // Oscillate the signature size to work the fee optimization loop. 2739 if sigSizer%2 == 0 { 2740 scriptSize -= 2 2741 } 2742 sigSizer++ 2743 return signFunc(msgTx, scriptSize) 2744 } 2745 2746 node.signFunc = signFunc 2747 // reset the signFunc after this test so captured variables are free 2748 defer func() { node.signFunc = defaultSignFunc }() 2749 2750 // This time should succeed. 2751 _, changeCoin, feesPaid, err := wallet.Swap(swaps) 2752 if err != nil { 2753 t.Fatalf("swap error: %v", err) 2754 } 2755 2756 // Make sure the change coin is locked. 2757 if len(node.lockedCoins) != 1 { 2758 t.Fatalf("change coin not locked") 2759 } 2760 txHash, _, _ := decodeCoinID(changeCoin.ID()) 2761 if txHash.String() != node.lockedCoins[0].Hash.String() { 2762 t.Fatalf("wrong coin locked during swap") 2763 } 2764 2765 // Fees should be returned. 2766 minFees := tDCR.MaxFeeRate * uint64(node.sentRawTx.SerializeSize()) 2767 if feesPaid < minFees { 2768 t.Fatalf("sent fees, %d, less than required fees, %d", feesPaid, minFees) 2769 } 2770 2771 // Not enough funds 2772 swaps.Inputs = coins[:1] 2773 _, _, _, err = wallet.Swap(swaps) 2774 if err == nil { 2775 t.Fatalf("no error for listunspent not enough funds") 2776 } 2777 swaps.Inputs = coins 2778 2779 // AddressPKH error 2780 node.newAddrErr = tErr 2781 _, _, _, err = wallet.Swap(swaps) 2782 if err == nil { 2783 t.Fatalf("no error for getnewaddress rpc error") 2784 } 2785 node.newAddrErr = nil 2786 2787 // ChangeAddress error 2788 node.changeAddrErr = tErr 2789 _, _, _, err = wallet.Swap(swaps) 2790 if err == nil { 2791 t.Fatalf("no error for getrawchangeaddress rpc error") 2792 } 2793 node.changeAddrErr = nil 2794 2795 // SignTx error 2796 node.signFunc = func(msgTx *wire.MsgTx) (*wire.MsgTx, bool, error) { 2797 return nil, false, tErr 2798 } 2799 _, _, _, err = wallet.Swap(swaps) 2800 if err == nil { 2801 t.Fatalf("no error for signrawtransactionwithwallet rpc error") 2802 } 2803 2804 // incomplete signatures 2805 node.signFunc = func(msgTx *wire.MsgTx) (*wire.MsgTx, bool, error) { 2806 return msgTx, false, nil 2807 } 2808 _, _, _, err = wallet.Swap(swaps) 2809 if err == nil { 2810 t.Fatalf("no error for incomplete signature rpc error") 2811 } 2812 node.signFunc = signFunc 2813 2814 // Make sure we can succeed again. 2815 _, _, _, err = wallet.Swap(swaps) 2816 if err != nil { 2817 t.Fatalf("re-swap error: %v", err) 2818 } 2819 } 2820 2821 type TAuditInfo struct{} 2822 2823 func (ai *TAuditInfo) Recipient() string { return tPKHAddr.String() } 2824 func (ai *TAuditInfo) Expiration() time.Time { return time.Time{} } 2825 func (ai *TAuditInfo) Coin() asset.Coin { return &tCoin{} } 2826 func (ai *TAuditInfo) Contract() dex.Bytes { return nil } 2827 func (ai *TAuditInfo) SecretHash() dex.Bytes { return nil } 2828 2829 func TestRedeem(t *testing.T) { 2830 wallet, node, shutdown := tNewWallet() 2831 defer shutdown() 2832 2833 swapVal := toAtoms(5) 2834 secret := randBytes(32) 2835 secretHash := sha256.Sum256(secret) 2836 lockTime := time.Now().Add(time.Hour * 12) 2837 addr := tPKHAddr.String() 2838 2839 contract, err := dexdcr.MakeContract(addr, addr, secretHash[:], lockTime.Unix(), tChainParams) 2840 if err != nil { 2841 t.Fatalf("error making swap contract: %v", err) 2842 } 2843 2844 coin := newOutput(tTxHash, 0, swapVal, wire.TxTreeRegular) 2845 2846 ci := &asset.AuditInfo{ 2847 Coin: coin, 2848 Contract: contract, 2849 Recipient: tPKHAddr.String(), 2850 Expiration: lockTime, 2851 } 2852 2853 redemption := &asset.Redemption{ 2854 Spends: ci, 2855 Secret: secret, 2856 } 2857 2858 privBytes, _ := hex.DecodeString("b07209eec1a8fb6cfe5cb6ace36567406971a75c330db7101fb21bc679bc5330") 2859 2860 node.newAddr = tPKHAddr 2861 node.privWIF, err = dcrutil.NewWIF(privBytes, tChainParams.PrivateKeyID, dcrec.STEcdsaSecp256k1) 2862 if err != nil { 2863 t.Fatalf("NewWIF error: %v", err) 2864 } 2865 2866 redemptions := &asset.RedeemForm{ 2867 Redemptions: []*asset.Redemption{redemption}, 2868 } 2869 2870 _, _, feesPaid, err := wallet.Redeem(redemptions) 2871 if err != nil { 2872 t.Fatalf("redeem error: %v", err) 2873 } 2874 2875 // Check that fees are returned. 2876 minFees := optimalFeeRate * uint64(node.sentRawTx.SerializeSize()) 2877 if feesPaid < minFees { 2878 t.Fatalf("sent fees, %d, less than expected minimum fees, %d", feesPaid, minFees) 2879 } 2880 2881 // No audit info 2882 redemption.Spends = nil 2883 _, _, _, err = wallet.Redeem(redemptions) 2884 if err == nil { 2885 t.Fatalf("no error for nil AuditInfo") 2886 } 2887 redemption.Spends = ci 2888 2889 // Spoofing AuditInfo is not allowed. 2890 redemption.Spends = &asset.AuditInfo{} 2891 _, _, _, err = wallet.Redeem(redemptions) 2892 if err == nil { 2893 t.Fatalf("no error for spoofed AuditInfo") 2894 } 2895 redemption.Spends = ci 2896 2897 // Wrong secret hash 2898 redemption.Secret = randBytes(32) 2899 _, _, _, err = wallet.Redeem(redemptions) 2900 if err == nil { 2901 t.Fatalf("no error for wrong secret") 2902 } 2903 redemption.Secret = secret 2904 2905 // too low of value 2906 coin.value = 200 2907 _, _, _, err = wallet.Redeem(redemptions) 2908 if err == nil { 2909 t.Fatalf("no error for redemption not worth the fees") 2910 } 2911 coin.value = swapVal 2912 2913 // New address error 2914 node.newAddrErr = tErr 2915 _, _, _, err = wallet.Redeem(redemptions) 2916 if err == nil { 2917 t.Fatalf("no error for new address error") 2918 } 2919 2920 // Change address error 2921 node.changeAddrErr = tErr 2922 _, _, _, err = wallet.Redeem(redemptions) 2923 if err == nil { 2924 t.Fatalf("no error for change address error") 2925 } 2926 node.changeAddrErr = nil 2927 2928 // Missing priv key error 2929 node.privWIFErr = tErr 2930 _, _, _, err = wallet.Redeem(redemptions) 2931 if err == nil { 2932 t.Fatalf("no error for missing private key") 2933 } 2934 node.privWIFErr = nil 2935 2936 // Send error 2937 node.sendRawErr = tErr 2938 _, _, _, err = wallet.Redeem(redemptions) 2939 if err == nil { 2940 t.Fatalf("no error for send error") 2941 } 2942 node.sendRawErr = nil 2943 2944 // Wrong hash 2945 var h chainhash.Hash 2946 h[0] = 0x01 2947 node.sendRawHash = &h 2948 _, _, _, err = wallet.Redeem(redemptions) 2949 if err == nil { 2950 t.Fatalf("no error for wrong return hash") 2951 } 2952 node.sendRawHash = nil 2953 } 2954 2955 const ( 2956 txCatReceive = "recv" 2957 txCatSend = "send" 2958 //txCatGenerate = "generate" 2959 ) 2960 2961 func TestSignMessage(t *testing.T) { 2962 wallet, node, shutdown := tNewWallet() 2963 defer shutdown() 2964 2965 vout := uint32(5) 2966 privBytes, _ := hex.DecodeString("b07209eec1a8fb6cfe5cb6ace36567406971a75c330db7101fb21bc679bc5330") 2967 privKey := secp256k1.PrivKeyFromBytes(privBytes) 2968 pubKey := privKey.PubKey() 2969 2970 msg := randBytes(36) 2971 pk := pubKey.SerializeCompressed() 2972 msgHash := chainhash.HashB(msg) 2973 signature := ecdsa.Sign(privKey, msgHash) 2974 sig := signature.Serialize() 2975 2976 var err error 2977 node.privWIF, err = dcrutil.NewWIF(privBytes, tChainParams.PrivateKeyID, dcrec.STEcdsaSecp256k1) 2978 if err != nil { 2979 t.Fatalf("NewWIF error: %v", err) 2980 } 2981 2982 op := newOutput(tTxHash, vout, 5e7, wire.TxTreeRegular) 2983 2984 wallet.fundingCoins[op.pt] = &fundingCoin{ 2985 addr: tPKHAddr.String(), 2986 } 2987 2988 check := func() { 2989 pubkeys, sigs, err := wallet.SignMessage(op, msg) 2990 if err != nil { 2991 t.Fatalf("SignMessage error: %v", err) 2992 } 2993 if len(pubkeys) != 1 { 2994 t.Fatalf("expected 1 pubkey, received %d", len(pubkeys)) 2995 } 2996 if len(sigs) != 1 { 2997 t.Fatalf("expected 1 sig, received %d", len(sigs)) 2998 } 2999 if !bytes.Equal(pk, pubkeys[0]) { 3000 t.Fatalf("wrong pubkey. expected %x, got %x", pubkeys[0], pk) 3001 } 3002 if !bytes.Equal(sig, sigs[0]) { 3003 t.Fatalf("wrong signature. exptected %x, got %x", sigs[0], sig) 3004 } 3005 } 3006 3007 check() 3008 delete(wallet.fundingCoins, op.pt) 3009 txOut := makeGetTxOutRes(0, 5, nil) 3010 txOut.ScriptPubKey.Addresses = []string{tPKHAddr.String()} 3011 node.txOutRes[newOutPoint(tTxHash, vout)] = txOut 3012 check() 3013 3014 // gettxout error 3015 node.txOutErr = tErr 3016 _, _, err = wallet.SignMessage(op, msg) 3017 if err == nil { 3018 t.Fatalf("no error for gettxout rpc error") 3019 } 3020 node.txOutErr = nil 3021 3022 // dumpprivkey error 3023 node.privWIFErr = tErr 3024 _, _, err = wallet.SignMessage(op, msg) 3025 if err == nil { 3026 t.Fatalf("no error for dumpprivkey rpc error") 3027 } 3028 node.privWIFErr = nil 3029 3030 // bad coin 3031 badCoin := &tCoin{id: make([]byte, 15)} 3032 _, _, err = wallet.SignMessage(badCoin, msg) 3033 if err == nil { 3034 t.Fatalf("no error for bad coin") 3035 } 3036 } 3037 3038 func TestAuditContract(t *testing.T) { 3039 wallet, _, shutdown := tNewWallet() 3040 defer shutdown() 3041 3042 secretHash, _ := hex.DecodeString("5124208c80d33507befa517c08ed01aa8d33adbf37ecd70fb5f9352f7a51a88d") 3043 lockTime := time.Now().Add(time.Hour * 12) 3044 addrStr := tPKHAddr.String() 3045 contract, err := dexdcr.MakeContract(addrStr, addrStr, secretHash, lockTime.Unix(), tChainParams) 3046 if err != nil { 3047 t.Fatalf("error making swap contract: %v", err) 3048 } 3049 addr, _ := stdaddr.NewAddressScriptHashV0(contract, tChainParams) 3050 _, pkScript := addr.PaymentScript() 3051 3052 // Prepare the contract tx data. 3053 contractTx := wire.NewMsgTx() 3054 contractTx.AddTxIn(&wire.TxIn{}) 3055 contractTx.AddTxOut(&wire.TxOut{ 3056 Value: 5 * int64(tLotSize), 3057 PkScript: pkScript, 3058 }) 3059 contractTxData, err := contractTx.Bytes() 3060 if err != nil { 3061 t.Fatalf("error preparing contract txdata: %v", err) 3062 } 3063 3064 contractHash := contractTx.TxHash() 3065 contractVout := uint32(0) 3066 contractCoinID := toCoinID(&contractHash, contractVout) 3067 3068 audit, err := wallet.AuditContract(contractCoinID, contract, contractTxData, true) 3069 if err != nil { 3070 t.Fatalf("audit error: %v", err) 3071 } 3072 if audit.Recipient != addrStr { 3073 t.Fatalf("wrong recipient. wanted '%s', got '%s'", addrStr, audit.Recipient) 3074 } 3075 if !bytes.Equal(audit.Contract, contract) { 3076 t.Fatalf("contract not set to coin redeem script") 3077 } 3078 if audit.Expiration.Equal(lockTime) { 3079 t.Fatalf("wrong lock time. wanted %d, got %d", lockTime.Unix(), audit.Expiration.Unix()) 3080 } 3081 3082 // Invalid txid 3083 _, err = wallet.AuditContract(make([]byte, 15), contract, contractTxData, false) 3084 if err == nil { 3085 t.Fatalf("no error for bad txid") 3086 } 3087 3088 // Wrong contract 3089 pkh, _ := hex.DecodeString("c6a704f11af6cbee8738ff19fc28cdc70aba0b82") 3090 wrongAddr, _ := stdaddr.NewAddressPubKeyHashEcdsaSecp256k1V0(pkh, tChainParams) 3091 wrongAddrStr := wrongAddr.String() 3092 wrongContract, err := dexdcr.MakeContract(wrongAddrStr, wrongAddrStr, secretHash, lockTime.Unix(), tChainParams) 3093 if err != nil { 3094 t.Fatalf("error making wrong swap contract: %v", err) 3095 } 3096 _, err = wallet.AuditContract(contractCoinID, wrongContract, contractTxData, false) 3097 if err == nil { 3098 t.Fatalf("no error for wrong contract") 3099 } 3100 3101 // Invalid contract 3102 _, wrongPkScript := wrongAddr.PaymentScript() 3103 _, err = wallet.AuditContract(contractCoinID, wrongPkScript, contractTxData, false) // addrPkScript not a valid contract 3104 if err == nil { 3105 t.Fatalf("no error for invalid contract") 3106 } 3107 3108 // No txdata 3109 _, err = wallet.AuditContract(contractCoinID, contract, nil, false) 3110 if err == nil { 3111 t.Fatalf("no error for no txdata") 3112 } 3113 3114 // Invalid txdata, zero inputs 3115 contractTx.TxIn = nil 3116 invalidContractTxData, err := contractTx.Bytes() 3117 if err != nil { 3118 t.Fatalf("error preparing invalid contract txdata: %v", err) 3119 } 3120 _, err = wallet.AuditContract(contractCoinID, contract, invalidContractTxData, false) 3121 if err == nil { 3122 t.Fatalf("no error for unknown txout") 3123 } 3124 3125 // Wrong txdata, wrong output script 3126 wrongContractTx := wire.NewMsgTx() 3127 wrongContractTx.AddTxIn(&wire.TxIn{}) 3128 wrongContractTx.AddTxOut(&wire.TxOut{ 3129 Value: 5 * int64(tLotSize), 3130 PkScript: wrongPkScript, 3131 }) 3132 wrongContractTxData, err := wrongContractTx.Bytes() 3133 if err != nil { 3134 t.Fatalf("error preparing wrong contract txdata: %v", err) 3135 } 3136 _, err = wallet.AuditContract(contractCoinID, contract, wrongContractTxData, false) 3137 if err == nil { 3138 t.Fatalf("no error for unknown txout") 3139 } 3140 } 3141 3142 type tReceipt struct { 3143 coin *tCoin 3144 contract []byte 3145 expiration uint64 3146 } 3147 3148 func (r *tReceipt) Expiration() time.Time { return time.Unix(int64(r.expiration), 0).UTC() } 3149 func (r *tReceipt) Coin() asset.Coin { return r.coin } 3150 func (r *tReceipt) Contract() dex.Bytes { return r.contract } 3151 3152 func TestFindRedemption(t *testing.T) { 3153 wallet, node, shutdown := tNewWallet() 3154 defer shutdown() 3155 3156 _, bestBlockHeight, err := node.GetBestBlock(context.Background()) 3157 if err != nil { 3158 t.Fatalf("unexpected GetBestBlock error: %v", err) 3159 } 3160 3161 contractHeight := bestBlockHeight + 1 3162 contractVout := uint32(1) 3163 3164 secret := randBytes(32) 3165 secretHash := sha256.Sum256(secret) 3166 lockTime := time.Now().Add(time.Hour * 12) 3167 addrStr := tPKHAddr.String() 3168 contract, err := dexdcr.MakeContract(addrStr, addrStr, secretHash[:], lockTime.Unix(), tChainParams) 3169 if err != nil { 3170 t.Fatalf("error making swap contract: %v", err) 3171 } 3172 contractAddr, _ := stdaddr.NewAddressScriptHashV0(contract, tChainParams) 3173 _, contractP2SHScript := contractAddr.PaymentScript() 3174 3175 tPKHAddrV3, _ := stdaddr.DecodeAddress(tPKHAddr.String(), tChainParams) 3176 _, otherScript := tPKHAddrV3.PaymentScript() 3177 3178 redemptionScript, _ := dexdcr.RedeemP2SHContract(contract, randBytes(73), randBytes(33), secret) 3179 otherSpendScript, _ := txscript.NewScriptBuilder(). 3180 AddData(randBytes(73)). 3181 AddData(randBytes(33)). 3182 Script() 3183 3184 // Prepare and add the contract transaction to the blockchain. Put the pay-to-contract script at index 1. 3185 inputs := []*wire.TxIn{makeRPCVin(&chainhash.Hash{}, 0, otherSpendScript)} 3186 outputScripts := []dex.Bytes{otherScript, contractP2SHScript} 3187 contractTx := makeRawTx(inputs, outputScripts) 3188 contractTxHash := contractTx.TxHash() 3189 coinID := toCoinID(&contractTxHash, contractVout) 3190 blockHash, _ := node.blockchain.addRawTx(contractHeight, contractTx) 3191 txHex, err := makeTxHex(inputs, outputScripts) 3192 if err != nil { 3193 t.Fatalf("error generating hex for contract tx: %v", err) 3194 } 3195 walletTx := &walletjson.GetTransactionResult{ 3196 BlockHash: blockHash.String(), 3197 Confirmations: 1, 3198 Details: []walletjson.GetTransactionDetailsResult{ 3199 { 3200 Address: contractAddr.String(), 3201 Category: txCatSend, 3202 Vout: contractVout, 3203 }, 3204 }, 3205 Hex: txHex, 3206 } 3207 3208 // Add an intermediate block for good measure. 3209 node.blockchain.addRawTx(contractHeight+1, dummyTx()) 3210 3211 // Prepare the redemption tx inputs including an input that spends the contract output. 3212 inputs = append(inputs, makeRPCVin(&contractTxHash, contractVout, redemptionScript)) 3213 3214 // Add the redemption to mempool and check if wallet.FindRedemption finds it. 3215 redeemTx := makeRawTx(inputs, []dex.Bytes{otherScript}) 3216 node.blockchain.addRawTx(-1, redeemTx) 3217 _, checkSecret, err := wallet.FindRedemption(tCtx, coinID, nil) 3218 if err != nil { 3219 t.Fatalf("error finding redemption: %v", err) 3220 } 3221 if !bytes.Equal(checkSecret, secret) { 3222 t.Fatalf("wrong secret. expected %x, got %x", secret, checkSecret) 3223 } 3224 3225 node.walletTxFn = func() (*walletjson.GetTransactionResult, error) { 3226 return walletTx, nil 3227 } 3228 3229 // Move the redemption to a new block and check if wallet.FindRedemption finds it. 3230 _, redeemBlock := node.blockchain.addRawTx(contractHeight+2, makeRawTx(inputs, []dex.Bytes{otherScript})) 3231 _, checkSecret, err = wallet.FindRedemption(tCtx, coinID, nil) 3232 if err != nil { 3233 t.Fatalf("error finding redemption: %v", err) 3234 } 3235 if !bytes.Equal(checkSecret, secret) { 3236 t.Fatalf("wrong secret. expected %x, got %x", secret, checkSecret) 3237 } 3238 3239 // gettransaction error 3240 node.walletTxFn = func() (*walletjson.GetTransactionResult, error) { 3241 return walletTx, tErr 3242 } 3243 _, _, err = wallet.FindRedemption(tCtx, coinID, nil) 3244 if err == nil { 3245 t.Fatalf("no error for gettransaction rpc error") 3246 } 3247 node.walletTxFn = func() (*walletjson.GetTransactionResult, error) { 3248 return walletTx, nil 3249 } 3250 3251 // getcfilterv2 error 3252 node.rawErr[methodGetCFilterV2] = tErr 3253 _, _, err = wallet.FindRedemption(tCtx, coinID, nil) 3254 if err == nil { 3255 t.Fatalf("no error for getcfilterv2 rpc error") 3256 } 3257 delete(node.rawErr, methodGetCFilterV2) 3258 3259 // missing redemption 3260 redeemBlock.Transactions[0].TxIn[1].PreviousOutPoint.Hash = chainhash.Hash{} 3261 ctx, cancel := context.WithTimeout(tCtx, 2*time.Second) 3262 defer cancel() // ctx should auto-cancel after 2 seconds, but this is apparently good practice to prevent leak 3263 _, k, err := wallet.FindRedemption(ctx, coinID, nil) 3264 if ctx.Err() == nil || k != nil { 3265 // Expected ctx to cancel after timeout and no secret should be found. 3266 t.Fatalf("unexpected result for missing redemption: secret: %v, err: %v", k, err) 3267 } 3268 redeemBlock.Transactions[0].TxIn[1].PreviousOutPoint.Hash = contractTxHash 3269 3270 // Canceled context 3271 deadCtx, cancelCtx := context.WithCancel(tCtx) 3272 cancelCtx() 3273 _, _, err = wallet.FindRedemption(deadCtx, coinID, nil) 3274 if err == nil { 3275 t.Fatalf("no error for canceled context") 3276 } 3277 3278 // Expect FindRedemption to error because of bad input sig. 3279 redeemBlock.Transactions[0].TxIn[1].SignatureScript = randBytes(100) 3280 _, _, err = wallet.FindRedemption(tCtx, coinID, nil) 3281 if err == nil { 3282 t.Fatalf("no error for wrong redemption") 3283 } 3284 redeemBlock.Transactions[0].TxIn[1].SignatureScript = redemptionScript 3285 3286 // Wrong script type for output 3287 walletTx.Hex, _ = makeTxHex(inputs, []dex.Bytes{otherScript, otherScript}) 3288 _, _, err = wallet.FindRedemption(tCtx, coinID, nil) 3289 if err == nil { 3290 t.Fatalf("no error for wrong script type") 3291 } 3292 walletTx.Hex = txHex 3293 3294 // Sanity check to make sure it passes again. 3295 _, _, err = wallet.FindRedemption(tCtx, coinID, nil) 3296 if err != nil { 3297 t.Fatalf("error after clearing errors: %v", err) 3298 } 3299 } 3300 3301 func TestRefund(t *testing.T) { 3302 wallet, node, shutdown := tNewWallet() 3303 defer shutdown() 3304 3305 secret := randBytes(32) 3306 secretHash := sha256.Sum256(secret) 3307 lockTime := time.Now().Add(time.Hour * 12) 3308 addrStr := tPKHAddr.String() 3309 contract, err := dexdcr.MakeContract(addrStr, addrStr, secretHash[:], lockTime.Unix(), tChainParams) 3310 if err != nil { 3311 t.Fatalf("error making swap contract: %v", err) 3312 } 3313 const feeSuggestion = 100 3314 3315 tipHash, tipHeight := node.getBestBlock() 3316 var confs int64 = 1 3317 if tipHeight > 1 { 3318 confs = 2 3319 } 3320 3321 bigTxOut := makeGetTxOutRes(confs, 5, nil) 3322 bigOutID := newOutPoint(tTxHash, 0) 3323 node.txOutRes[bigOutID] = bigTxOut 3324 node.txOutRes[bigOutID].BestBlock = tipHash.String() // required to calculate the block for the output 3325 node.changeAddr = tPKHAddr 3326 node.newAddr = tPKHAddr 3327 3328 privBytes, _ := hex.DecodeString("b07209eec1a8fb6cfe5cb6ace36567406971a75c330db7101fb21bc679bc5330") 3329 node.privWIF, err = dcrutil.NewWIF(privBytes, tChainParams.PrivateKeyID, dcrec.STEcdsaSecp256k1) 3330 if err != nil { 3331 t.Fatalf("NewWIF error: %v", err) 3332 } 3333 3334 contractOutput := newOutput(tTxHash, 0, 1e8, wire.TxTreeRegular) 3335 _, err = wallet.Refund(contractOutput.ID(), contract, feeSuggestion) 3336 if err != nil { 3337 t.Fatalf("refund error: %v", err) 3338 } 3339 3340 // Invalid coin 3341 badReceipt := &tReceipt{ 3342 coin: &tCoin{id: make([]byte, 15)}, 3343 } 3344 _, err = wallet.Refund(badReceipt.coin.id, badReceipt.contract, feeSuggestion) 3345 if err == nil { 3346 t.Fatalf("no error for bad receipt") 3347 } 3348 3349 // gettxout error 3350 node.txOutErr = tErr 3351 _, err = wallet.Refund(contractOutput.ID(), contract, feeSuggestion) 3352 if err == nil { 3353 t.Fatalf("no error for missing utxo") 3354 } 3355 node.txOutErr = nil 3356 3357 // bad contract 3358 badContractOutput := newOutput(tTxHash, 0, 1e8, wire.TxTreeRegular) 3359 _, err = wallet.Refund(badContractOutput.ID(), randBytes(50), feeSuggestion) 3360 if err == nil { 3361 t.Fatalf("no error for bad contract") 3362 } 3363 3364 // Too small. 3365 node.txOutRes[bigOutID] = newTxOutResult(nil, 100, 2) 3366 _, err = wallet.Refund(contractOutput.ID(), contract, feeSuggestion) 3367 if err == nil { 3368 t.Fatalf("no error for value < fees") 3369 } 3370 node.txOutRes[bigOutID] = bigTxOut 3371 3372 // signature error 3373 node.privWIFErr = tErr 3374 _, err = wallet.Refund(contractOutput.ID(), contract, feeSuggestion) 3375 if err == nil { 3376 t.Fatalf("no error for dumpprivkey rpc error") 3377 } 3378 node.privWIFErr = nil 3379 3380 // send error 3381 node.sendRawErr = tErr 3382 _, err = wallet.Refund(contractOutput.ID(), contract, feeSuggestion) 3383 if err == nil { 3384 t.Fatalf("no error for sendrawtransaction rpc error") 3385 } 3386 node.sendRawErr = nil 3387 3388 // bad checkhash 3389 var badHash chainhash.Hash 3390 badHash[0] = 0x05 3391 node.sendRawHash = &badHash 3392 _, err = wallet.Refund(contractOutput.ID(), contract, feeSuggestion) 3393 if err == nil { 3394 t.Fatalf("no error for tx hash") 3395 } 3396 node.sendRawHash = nil 3397 3398 // Sanity check that we can succeed again. 3399 _, err = wallet.Refund(contractOutput.ID(), contract, feeSuggestion) 3400 if err != nil { 3401 t.Fatalf("re-refund error: %v", err) 3402 } 3403 } 3404 3405 type tSenderType byte 3406 3407 const ( 3408 tSendSender tSenderType = iota 3409 tWithdrawSender 3410 ) 3411 3412 func testSender(t *testing.T, senderType tSenderType) { 3413 wallet, node, shutdown := tNewWallet() 3414 defer shutdown() 3415 3416 var sendVal uint64 = 1e8 3417 var unspentVal uint64 = 100e8 3418 const feeSuggestion = 100 3419 funName := "Send" 3420 sender := func(addr string, val uint64) (asset.Coin, error) { 3421 return wallet.Send(addr, val, feeSuggestion) 3422 } 3423 if senderType == tWithdrawSender { 3424 funName = "Withdraw" 3425 // For withdraw, test with unspent total = withdraw value 3426 unspentVal = sendVal 3427 sender = func(addr string, val uint64) (asset.Coin, error) { 3428 return wallet.Withdraw(addr, val, feeSuggestion) 3429 } 3430 } 3431 3432 addr := tPKHAddr.String() 3433 node.changeAddr = tPKHAddr 3434 3435 node.unspent = []walletjson.ListUnspentResult{{ 3436 TxID: tTxID, 3437 Address: tPKHAddr.String(), 3438 Account: tAcctName, 3439 Amount: float64(unspentVal) / 1e8, 3440 Confirmations: 5, 3441 ScriptPubKey: hex.EncodeToString(tP2PKHScript), 3442 Spendable: true, 3443 }} 3444 //node.unspent = append(node.unspent, node.unspent[0]) 3445 3446 _, err := sender(addr, sendVal) 3447 if err != nil { 3448 t.Fatalf(funName+" error: %v", err) 3449 } 3450 3451 // invalid address 3452 _, err = sender("badaddr", sendVal) 3453 if err == nil { 3454 t.Fatalf("no error for bad address: %v", err) 3455 } 3456 3457 // GetRawChangeAddress error 3458 if senderType == tSendSender { // withdraw test does not get a change address 3459 node.changeAddrErr = tErr 3460 _, err = sender(addr, sendVal) 3461 if err == nil { 3462 t.Fatalf("no error for rawchangeaddress: %v", err) 3463 } 3464 node.changeAddrErr = nil 3465 } 3466 3467 // good again 3468 _, err = sender(addr, sendVal) 3469 if err != nil { 3470 t.Fatalf(funName+" error afterwards: %v", err) 3471 } 3472 } 3473 3474 func TestWithdraw(t *testing.T) { 3475 testSender(t, tWithdrawSender) 3476 } 3477 3478 func TestSend(t *testing.T) { 3479 testSender(t, tSendSender) 3480 } 3481 3482 func Test_withdraw(t *testing.T) { 3483 wallet, node, shutdown := tNewWallet() 3484 defer shutdown() 3485 3486 address := tPKHAddr.String() 3487 node.changeAddr = tPKHAddr 3488 3489 var unspentVal uint64 = 100e8 3490 node.unspent = []walletjson.ListUnspentResult{{ 3491 TxID: tTxID, 3492 Address: tPKHAddr.String(), 3493 Account: tAcctName, 3494 Amount: float64(unspentVal) / 1e8, 3495 Confirmations: 5, 3496 ScriptPubKey: hex.EncodeToString(tP2PKHScript), 3497 Spendable: true, 3498 }} 3499 3500 addr, err := stdaddr.DecodeAddress(address, tChainParams) 3501 if err != nil { 3502 t.Fatal(err) 3503 } 3504 3505 // This should make a msgTx with one input and one output. 3506 msgTx, val, err := wallet.withdraw(addr, unspentVal, optimalFeeRate) 3507 if err != nil { 3508 t.Fatal(err) 3509 } 3510 if len(msgTx.TxOut) != 1 { 3511 t.Fatalf("expected 1 output, got %d", len(msgTx.TxOut)) 3512 } 3513 if val != uint64(msgTx.TxOut[0].Value) { 3514 t.Errorf("expected non-change output to be %d, got %d", val, msgTx.TxOut[0].Value) 3515 } 3516 if val >= unspentVal { 3517 t.Errorf("expected output to be have fees deducted") 3518 } 3519 3520 // Then with unspentVal just slightly larger than send. This should still 3521 // make a msgTx with one output, but larger than before. The sent value is 3522 // SMALLER than requested because it was required for fees. 3523 avail := unspentVal + 77 3524 node.unspent[0].Amount = float64(avail) / 1e8 3525 msgTx, val, err = wallet.withdraw(addr, unspentVal, optimalFeeRate) 3526 if err != nil { 3527 t.Fatal(err) 3528 } 3529 if len(msgTx.TxOut) != 1 { 3530 t.Fatalf("expected 1 output, got %d", len(msgTx.TxOut)) 3531 } 3532 if val != uint64(msgTx.TxOut[0].Value) { 3533 t.Errorf("expected non-change output to be %d, got %d", val, msgTx.TxOut[0].Value) 3534 } 3535 if val >= unspentVal { 3536 t.Errorf("expected output to be have fees deducted") 3537 } 3538 3539 // Still no change, but this time the sent value is LARGER than requested 3540 // because change would be dust, and we don't over pay fees. 3541 avail = unspentVal + 3000 3542 node.unspent[0].Amount = float64(avail) / 1e8 3543 msgTx, val, err = wallet.withdraw(addr, unspentVal, optimalFeeRate) 3544 if err != nil { 3545 t.Fatal(err) 3546 } 3547 if len(msgTx.TxOut) != 1 { 3548 t.Fatalf("expected 1 output, got %d", len(msgTx.TxOut)) 3549 } 3550 if val <= unspentVal { 3551 t.Errorf("expected output to be more thrifty") 3552 } 3553 3554 // Then with unspentVal considerably larger than (double) send. This should 3555 // make a msgTx with two outputs as the change is no longer dust. The change 3556 // should be exactly unspentVal and the sent amount should be 3557 // unspentVal-fees. 3558 node.unspent[0].Amount = float64(unspentVal*2) / 1e8 3559 msgTx, val, err = wallet.withdraw(addr, unspentVal, optimalFeeRate) 3560 if err != nil { 3561 t.Fatal(err) 3562 } 3563 if len(msgTx.TxOut) != 2 { 3564 t.Fatalf("expected 2 outputs, got %d", len(msgTx.TxOut)) 3565 } 3566 if val != uint64(msgTx.TxOut[0].Value) { 3567 t.Errorf("expected non-change output to be %d, got %d", val, msgTx.TxOut[0].Value) 3568 } 3569 if unspentVal != uint64(msgTx.TxOut[1].Value) { 3570 t.Errorf("expected change output to be %d, got %d", unspentVal, msgTx.TxOut[1].Value) 3571 } 3572 } 3573 3574 func Test_sendToAddress(t *testing.T) { 3575 wallet, node, shutdown := tNewWallet() 3576 defer shutdown() 3577 3578 address := tPKHAddr.String() 3579 node.changeAddr = tPKHAddr 3580 3581 var unspentVal uint64 = 100e8 3582 node.unspent = []walletjson.ListUnspentResult{{ 3583 TxID: tTxID, 3584 Address: tPKHAddr.String(), 3585 Account: tAcctName, 3586 Amount: float64(unspentVal) / 1e8, 3587 Confirmations: 5, 3588 ScriptPubKey: hex.EncodeToString(tP2PKHScript), 3589 Spendable: true, 3590 }} 3591 3592 addr, err := stdaddr.DecodeAddress(address, tChainParams) 3593 if err != nil { 3594 t.Fatal(err) 3595 } 3596 3597 // This should return an error, not enough funds to send. 3598 _, _, _, err = wallet.sendToAddress(addr, unspentVal, optimalFeeRate) 3599 if err == nil { 3600 t.Fatal("Expected error, not enough funds to send.") 3601 } 3602 3603 // With a lower send value, send should be successful. 3604 var sendVal uint64 = 10e8 3605 node.unspent[0].Amount = float64(unspentVal) 3606 msgTx, val, _, err := wallet.sendToAddress(addr, sendVal, optimalFeeRate) 3607 if err != nil { 3608 t.Fatal(err) 3609 } 3610 if val != uint64(msgTx.TxOut[0].Value) { 3611 t.Errorf("expected non-change output to be %d, got %d", val, msgTx.TxOut[0].Value) 3612 } 3613 if val != sendVal { 3614 t.Errorf("expected non-change output to be %d, got %d", sendVal, val) 3615 } 3616 } 3617 3618 func TestLookupTxOutput(t *testing.T) { 3619 wallet, node, shutdown := tNewWallet() 3620 defer shutdown() 3621 3622 coinID := make([]byte, 36) 3623 copy(coinID[:32], tTxHash[:]) 3624 op := newOutPoint(tTxHash, 0) 3625 3626 // Bad output coin 3627 op.vout = 10 3628 _, _, spent, err := wallet.lookupTxOutput(context.Background(), &op.txHash, op.vout) 3629 if err == nil { 3630 t.Fatalf("no error for bad output coin") 3631 } 3632 if spent != 0 { 3633 t.Fatalf("spent is not 0 for bad output coin") 3634 } 3635 op.vout = 0 3636 3637 // Add the txOutRes with 2 confs and BestBlock correctly set. 3638 node.txOutRes[op] = makeGetTxOutRes(2, 1, tP2PKHScript) 3639 _, confs, spent, err := wallet.lookupTxOutput(context.Background(), &op.txHash, op.vout) 3640 if err != nil { 3641 t.Fatalf("unexpected error for gettxout path: %v", err) 3642 } 3643 if confs != 2 { 3644 t.Fatalf("confs not retrieved from gettxout path. expected 2, got %d", confs) 3645 } 3646 if spent != 0 { 3647 t.Fatalf("expected spent = 0 for gettxout path, got true") 3648 } 3649 3650 // gettransaction error 3651 delete(node.txOutRes, op) 3652 walletTx := &walletjson.GetTransactionResult{} 3653 node.walletTxFn = func() (*walletjson.GetTransactionResult, error) { 3654 return walletTx, tErr 3655 } 3656 _, _, spent, err = wallet.lookupTxOutput(context.Background(), &op.txHash, op.vout) 3657 if err == nil { 3658 t.Fatalf("no error for gettransaction error") 3659 } 3660 if spent != 0 { 3661 t.Fatalf("spent is not 0 with gettransaction error") 3662 } 3663 node.walletTxFn = func() (*walletjson.GetTransactionResult, error) { 3664 return walletTx, nil 3665 } 3666 3667 // wallet.lookupTxOutput will check if the tx is confirmed, its hex 3668 // is valid and contains an output at index 0, for the output to be 3669 // considered spent. 3670 tx := wire.NewMsgTx() 3671 tx.AddTxIn(&wire.TxIn{}) 3672 tx.AddTxOut(&wire.TxOut{ 3673 PkScript: tP2PKHScript, 3674 }) 3675 txHex, err := msgTxToHex(tx) 3676 if err != nil { 3677 t.Fatalf("error preparing tx hex with 1 output: %v", err) 3678 } 3679 walletTx.Hex = txHex // unconfirmed = unspent 3680 3681 _, _, spent, err = wallet.lookupTxOutput(context.Background(), &op.txHash, op.vout) 3682 if err != nil { 3683 t.Fatalf("unexpected error for gettransaction path (unconfirmed): %v", err) 3684 } 3685 if spent != 0 { 3686 t.Fatalf("expected spent = 0 for gettransaction path (unconfirmed), got true") 3687 } 3688 3689 // Confirmed wallet tx without gettxout response is spent. 3690 walletTx.Confirmations = 2 3691 _, _, spent, err = wallet.lookupTxOutput(context.Background(), &op.txHash, op.vout) 3692 if err != nil { 3693 t.Fatalf("unexpected error for gettransaction path (confirmed): %v", err) 3694 } 3695 if spent != 1 { 3696 t.Fatalf("expected spent = 1 for gettransaction path (confirmed), got false") 3697 } 3698 3699 // In spv mode, spent status is unknown without a block filters scan. 3700 wallet.wallet.(*rpcWallet).spvMode = true 3701 _, _, spent, err = wallet.lookupTxOutput(context.Background(), &op.txHash, op.vout) 3702 if err != nil { 3703 t.Fatalf("unexpected error for spv gettransaction path (non-wallet output): %v", err) 3704 } 3705 if spent != -1 { 3706 t.Fatalf("expected spent = -1 for spv gettransaction path (non-wallet output), got true") 3707 } 3708 3709 // In spv mode, output is spent if it pays to the wallet (but no txOutRes). 3710 /* what is the use case for this since a contract never pays to wallet? 3711 node.walletTx.Details = []walletjson.GetTransactionDetailsResult{{ 3712 Vout: 0, 3713 Category: "receive", // output at index 0 pays to the wallet 3714 }} 3715 _, _, spent, err = wallet.lookupTxOutput(context.Background(), &op.txHash, op.vout) 3716 if err != nil { 3717 t.Fatalf("unexpected error for spv gettransaction path (wallet output): %v", err) 3718 } 3719 if spent != 1 { 3720 t.Fatalf("expected spent = 1 for spv gettransaction path (wallet output), got false") 3721 } 3722 */ 3723 } 3724 3725 func TestSendEdges(t *testing.T) { 3726 wallet, node, shutdown := tNewWallet() 3727 defer shutdown() 3728 3729 const feeRate uint64 = 3 3730 3731 const swapVal = 2e8 // leaving untyped. NewTxOut wants int64 3732 3733 contractAddr, _ := stdaddr.NewAddressScriptHashV0(randBytes(20), tChainParams) 3734 // See dexdcr.IsDust for the source of this dustCoverage voodoo. 3735 dustCoverage := (dexdcr.P2PKHOutputSize + 165) * feeRate * 3 3736 dexReqFees := dexdcr.InitTxSize * feeRate 3737 3738 _, pkScript := contractAddr.PaymentScript() 3739 3740 newBaseTx := func(funding uint64) *wire.MsgTx { 3741 baseTx := wire.NewMsgTx() 3742 baseTx.AddTxIn(wire.NewTxIn(new(wire.OutPoint), int64(funding), nil)) 3743 baseTx.AddTxOut(wire.NewTxOut(swapVal, pkScript)) 3744 return baseTx 3745 } 3746 3747 node.signFunc = func(tx *wire.MsgTx) (*wire.MsgTx, bool, error) { 3748 return signFunc(tx, dexdcr.P2PKHSigScriptSize) 3749 } 3750 3751 tests := []struct { 3752 name string 3753 funding uint64 3754 expChange bool 3755 }{ 3756 { 3757 name: "not enough for change output", 3758 funding: swapVal + dexReqFees - 1, 3759 }, 3760 { 3761 // Still dust here, but a different path. 3762 name: "exactly enough for change output", 3763 funding: swapVal + dexReqFees, 3764 }, 3765 { 3766 name: "more than enough for change output but still dust", 3767 funding: swapVal + dexReqFees + 1, 3768 }, 3769 { 3770 name: "1 atom short to not be dust", 3771 funding: swapVal + dexReqFees + dustCoverage - 1, 3772 }, 3773 { 3774 name: "exactly enough to not be dust", 3775 funding: swapVal + dexReqFees + dustCoverage, 3776 expChange: true, 3777 }, 3778 } 3779 3780 // tPKHAddrV3, _ := stdaddr.DecodeAddress(tPKHAddr.String(), tChainParams) 3781 node.changeAddr = tPKHAddr 3782 3783 for _, tt := range tests { 3784 tx, err := wallet.sendWithReturn(newBaseTx(tt.funding), feeRate, -1) 3785 if err != nil { 3786 t.Fatalf("sendWithReturn error: %v", err) 3787 } 3788 3789 if len(tx.TxOut) == 1 && tt.expChange { 3790 t.Fatalf("%s: no change added", tt.name) 3791 } else if len(tx.TxOut) == 2 && !tt.expChange { 3792 t.Fatalf("%s: change output added for dust. Output value = %d", tt.name, tx.TxOut[1].Value) 3793 } 3794 } 3795 } 3796 3797 func TestSyncStatus(t *testing.T) { 3798 wallet, node, shutdown := tNewWallet() 3799 defer shutdown() 3800 3801 node.rawRes[methodSyncStatus], node.rawErr[methodSyncStatus] = json.Marshal(&walletjson.SyncStatusResult{ 3802 Synced: true, 3803 InitialBlockDownload: false, 3804 HeadersFetchProgress: 1, 3805 }) 3806 ss, err := wallet.SyncStatus() 3807 if err != nil { 3808 t.Fatalf("SyncStatus error (synced expected): %v", err) 3809 } 3810 if !ss.Synced { 3811 t.Fatalf("synced = false for progress=1") 3812 } 3813 if ss.BlockProgress() < 1 { 3814 t.Fatalf("progress not complete with sync true") 3815 } 3816 3817 node.rawErr[methodSyncStatus] = tErr 3818 _, err = wallet.SyncStatus() 3819 if err == nil { 3820 t.Fatalf("SyncStatus error not propagated") 3821 } 3822 node.rawErr[methodSyncStatus] = nil 3823 3824 nodeSyncStatusResult := &walletjson.SyncStatusResult{ 3825 Synced: false, 3826 InitialBlockDownload: false, 3827 HeadersFetchProgress: 0.5, // Headers: 200, WalletTip: 100 3828 } 3829 node.rawRes[methodSyncStatus], node.rawErr[methodSyncStatus] = json.Marshal(nodeSyncStatusResult) 3830 node.rawRes[methodGetPeerInfo], node.rawErr[methodGetPeerInfo] = json.Marshal([]*walletjson.GetPeerInfoResult{{StartingHeight: 1000}}) 3831 3832 ss, err = wallet.SyncStatus() 3833 if err != nil { 3834 t.Fatalf("SyncStatus error (half-synced): %v", err) 3835 } 3836 if ss.Synced { 3837 t.Fatalf("synced = true for progress=0.5") 3838 } 3839 if ss.BlockProgress() != nodeSyncStatusResult.HeadersFetchProgress { 3840 t.Fatalf("progress out of range. Expected %.2f, got %.2f", nodeSyncStatusResult.HeadersFetchProgress, ss.BlockProgress()) 3841 } 3842 if ss.Blocks != 500 { 3843 t.Fatalf("wrong header sync height. expected 500, got %d", ss.Blocks) 3844 } 3845 } 3846 3847 func TestPreSwap(t *testing.T) { 3848 wallet, node, shutdown := tNewWallet() 3849 defer shutdown() 3850 3851 // See math from TestFundEdges. 10 lots with max fee rate of 34 sats/vbyte. 3852 3853 swapVal := uint64(1e8) 3854 lots := swapVal / tLotSize // 10 lots 3855 3856 const totalBytes = 2510 3857 // base_best_case_bytes = swap_size_base + backing_bytes 3858 // = 85 + 166 = 251 3859 const bestCaseBytes = 251 // i.e. swapSize 3860 3861 backingFees := uint64(totalBytes) * tDCR.MaxFeeRate // total_bytes * fee_rate 3862 3863 minReq := swapVal + backingFees 3864 3865 fees := uint64(totalBytes) * tDCR.MaxFeeRate 3866 p2pkhUnspent := walletjson.ListUnspentResult{ 3867 TxID: tTxID, 3868 Address: tPKHAddr.String(), 3869 Account: tAcctName, 3870 Amount: float64(swapVal+fees-1) / 1e8, // one atom less than needed 3871 Confirmations: 5, 3872 ScriptPubKey: hex.EncodeToString(tP2PKHScript), 3873 Spendable: true, 3874 } 3875 3876 node.unspent = []walletjson.ListUnspentResult{p2pkhUnspent} 3877 3878 form := &asset.PreSwapForm{ 3879 Version: version, 3880 LotSize: tLotSize, 3881 Lots: lots, 3882 MaxFeeRate: tDCR.MaxFeeRate, 3883 Immediate: false, 3884 FeeSuggestion: feeSuggestion, 3885 // Redeem fields unneeded 3886 } 3887 3888 node.unspent[0].Amount = float64(minReq) / 1e8 3889 3890 // Initial success. 3891 preSwap, err := wallet.PreSwap(form) 3892 if err != nil { 3893 t.Fatalf("PreSwap error: %v", err) 3894 } 3895 3896 maxFees := totalBytes * tDCR.MaxFeeRate 3897 estHighFees := totalBytes * feeSuggestion 3898 estLowFees := bestCaseBytes * feeSuggestion 3899 checkSwapEstimate(t, preSwap.Estimate, lots, swapVal, maxFees, estHighFees, estLowFees) 3900 3901 // Too little funding is an error. 3902 node.unspent[0].Amount = float64(minReq-1) / 1e8 3903 _, err = wallet.PreSwap(form) 3904 if err == nil { 3905 t.Fatalf("no PreSwap error for not enough funds") 3906 } 3907 node.unspent[0].Amount = float64(minReq) / 1e8 3908 3909 // Success again. 3910 _, err = wallet.PreSwap(form) 3911 if err != nil { 3912 t.Fatalf("PreSwap error: %v", err) 3913 } 3914 } 3915 3916 func TestPreRedeem(t *testing.T) { 3917 wallet, _, shutdown := tNewWallet() 3918 defer shutdown() 3919 3920 preRedeem, err := wallet.PreRedeem(&asset.PreRedeemForm{ 3921 Version: version, 3922 Lots: 5, 3923 }) 3924 // Shouldn't actually be any path to error. 3925 if err != nil { 3926 t.Fatalf("PreRedeem non-segwit error: %v", err) 3927 } 3928 3929 // Just a sanity check. 3930 if preRedeem.Estimate.RealisticBestCase >= preRedeem.Estimate.RealisticWorstCase { 3931 t.Fatalf("best case > worst case") 3932 } 3933 } 3934 3935 func Test_dcrPerKBToAtomsPerByte(t *testing.T) { 3936 tests := []struct { 3937 name string 3938 estimatedFeeRate float64 3939 want uint64 3940 wantErr bool 3941 }{ 3942 { 3943 "catch negative", // but caller should check 3944 -0.0002, 3945 0, 3946 true, 3947 }, 3948 { 3949 "ok 0", // but caller should check 3950 0.0, 3951 0, 3952 false, 3953 }, 3954 { 3955 "ok 10", 3956 0.0001, 3957 10, 3958 false, 3959 }, 3960 { 3961 "ok 11", 3962 0.00011, 3963 11, 3964 false, 3965 }, 3966 { 3967 "ok 1", 3968 0.00001, 3969 1, 3970 false, 3971 }, 3972 { 3973 "ok 1 rounded up", 3974 0.000002, 3975 1, 3976 false, 3977 }, 3978 { 3979 "catch NaN err", 3980 math.NaN(), 3981 0, 3982 true, 3983 }, 3984 } 3985 for _, tt := range tests { 3986 t.Run(tt.name, func(t *testing.T) { 3987 got, err := dcrPerKBToAtomsPerByte(tt.estimatedFeeRate) 3988 if (err != nil) != tt.wantErr { 3989 t.Errorf("dcrPerKBToAtomsPerByte() error = %v, wantErr %v", err, tt.wantErr) 3990 return 3991 } 3992 if got != tt.want { 3993 t.Errorf("dcrPerKBToAtomsPerByte() = %v, want %v", got, tt.want) 3994 } 3995 }) 3996 } 3997 } 3998 3999 type tReconfigurer struct { 4000 *rpcWallet 4001 restart bool 4002 err error 4003 } 4004 4005 func (r *tReconfigurer) Reconfigure(ctx context.Context, cfg *asset.WalletConfig, net dex.Network, currentAddress string) (restartRequired bool, err error) { 4006 return r.restart, r.err 4007 } 4008 4009 func TestReconfigure(t *testing.T) { 4010 wallet, _, shutdown := tNewWalletMonitorBlocks(false) 4011 defer shutdown() 4012 4013 reconfigurer := tReconfigurer{ 4014 rpcWallet: wallet.wallet.(*rpcWallet), 4015 } 4016 4017 wallet.wallet = &reconfigurer 4018 4019 ctx, cancel := context.WithCancel(context.Background()) 4020 defer cancel() 4021 4022 cfg1 := &walletConfig{ 4023 UseSplitTx: true, 4024 FallbackFeeRate: 55, 4025 FeeRateLimit: 98, 4026 RedeemConfTarget: 7, 4027 ApiFeeFallback: true, 4028 } 4029 4030 cfg2 := &walletConfig{ 4031 UseSplitTx: false, 4032 FallbackFeeRate: 66, 4033 FeeRateLimit: 97, 4034 RedeemConfTarget: 5, 4035 ApiFeeFallback: false, 4036 } 4037 4038 // TODO: Test account names reconfiguration for rpcwallets. 4039 checkConfig := func(cfg *walletConfig) { 4040 if cfg.UseSplitTx != wallet.config().useSplitTx || 4041 toAtoms(cfg.FallbackFeeRate/1000) != wallet.config().fallbackFeeRate || 4042 toAtoms(cfg.FeeRateLimit/1000) != wallet.config().feeRateLimit || 4043 cfg.RedeemConfTarget != wallet.config().redeemConfTarget || 4044 cfg.ApiFeeFallback != wallet.config().apiFeeFallback { 4045 t.Fatalf("wallet not configured with the correct values") 4046 } 4047 } 4048 4049 settings1, err := config.Mapify(cfg1) 4050 if err != nil { 4051 t.Fatalf("failed to mapify: %v", err) 4052 } 4053 4054 settings2, err := config.Mapify(cfg2) 4055 if err != nil { 4056 t.Fatalf("failed to mapify: %v", err) 4057 } 4058 4059 walletCfg := &asset.WalletConfig{ 4060 Type: walletTypeDcrwRPC, 4061 Settings: settings1, 4062 DataDir: "abcd", 4063 } 4064 4065 // restart = false 4066 restart, err := wallet.Reconfigure(ctx, walletCfg, "123456") 4067 if err != nil { 4068 t.Fatalf("did not expect an error") 4069 } 4070 if restart { 4071 t.Fatalf("expected false restart but got true") 4072 } 4073 checkConfig(cfg1) 4074 4075 // restart = 2 4076 reconfigurer.restart = true 4077 restart, err = wallet.Reconfigure(ctx, walletCfg, "123456") 4078 if err != nil { 4079 t.Fatalf("did not expect an error") 4080 } 4081 if !restart { 4082 t.Fatalf("expected true restart but got false") 4083 } 4084 checkConfig(cfg1) 4085 4086 // try to set new configs, but get error. config should not change. 4087 reconfigurer.err = errors.New("reconfigure error") 4088 walletCfg.Settings = settings2 4089 _, err = wallet.Reconfigure(ctx, walletCfg, "123456") 4090 if err == nil { 4091 t.Fatalf("expected an error") 4092 } 4093 checkConfig(cfg1) 4094 } 4095 4096 func TestEstimateSendTxFee(t *testing.T) { 4097 wallet, node, shutdown := tNewWallet() 4098 defer shutdown() 4099 4100 addr := tPKHAddr.String() 4101 node.changeAddr = tPKHAddr 4102 var unspentVal uint64 = 100e8 4103 unspents := make([]walletjson.ListUnspentResult, 0) 4104 balanceResult := &walletjson.GetBalanceResult{ 4105 Balances: []walletjson.GetAccountBalanceResult{ 4106 { 4107 AccountName: tAcctName, 4108 }, 4109 }, 4110 } 4111 node.balanceResult = balanceResult 4112 4113 var vout uint32 4114 addUtxo := func(atomAmt uint64, confs int64, updateUnspent bool) { 4115 if updateUnspent { 4116 node.unspent[0].Amount += float64(atomAmt) / 1e8 4117 return 4118 } 4119 utxo := walletjson.ListUnspentResult{ 4120 TxID: tTxID, 4121 Vout: vout, 4122 Address: tPKHAddr.String(), 4123 Account: tAcctName, 4124 Amount: float64(atomAmt) / 1e8, 4125 Confirmations: confs, 4126 ScriptPubKey: hex.EncodeToString(tP2PKHScript), 4127 Spendable: true, 4128 } 4129 unspents = append(unspents, utxo) 4130 node.unspent = unspents 4131 // update balance 4132 balanceResult.Balances[0].Spendable += utxo.Amount 4133 vout++ 4134 } 4135 4136 tx := wire.NewMsgTx() 4137 payScriptVer, payScript := tPKHAddr.PaymentScript() 4138 tx.AddTxOut(newTxOut(int64(unspentVal), payScriptVer, payScript)) 4139 4140 // bSize is the base size for a single tx input. 4141 bSize := dexdcr.TxInOverhead + uint32(wire.VarIntSerializeSize(uint64(dexdcr.P2PKHSigScriptSize))) + dexdcr.P2PKHSigScriptSize 4142 4143 txSize := uint32(tx.SerializeSize()) + bSize 4144 estFee := uint64(txSize) * optimalFeeRate 4145 changeFee := dexdcr.P2PKHOutputSize * optimalFeeRate 4146 estFeeWithChange := changeFee + estFee 4147 4148 // This should return fee estimate for one output. 4149 addUtxo(unspentVal, 1, false) 4150 estimate, _, err := wallet.EstimateSendTxFee(addr, unspentVal, optimalFeeRate, true, false) 4151 if err != nil { 4152 t.Fatal(err) 4153 } 4154 if estimate != estFee { 4155 t.Fatalf("expected estimate to be %v, got %v)", estFee, estimate) 4156 } 4157 4158 // This should return fee estimate for two output. 4159 estimate, _, err = wallet.EstimateSendTxFee(addr, unspentVal/2, optimalFeeRate, true, false) 4160 if err != nil { 4161 t.Fatal(err) 4162 } 4163 if estimate != estFeeWithChange { 4164 t.Fatalf("expected estimate to be %v, got %v)", estFeeWithChange, estimate) 4165 } 4166 4167 // This should return an error, not enough funds to cover fees. 4168 _, _, err = wallet.EstimateSendTxFee(addr, unspentVal, optimalFeeRate, false, false) 4169 if err == nil { 4170 t.Fatal("Expected error not enough to cover funds required") 4171 } 4172 4173 dust := uint64(100) 4174 addUtxo(dust, 0, true) 4175 // This should return fee estimate for one output with dust added to fee. 4176 estFeeWithDust := estFee + 100 4177 estimate, _, err = wallet.EstimateSendTxFee(addr, unspentVal, optimalFeeRate, true, false) 4178 if err != nil { 4179 t.Fatal(err) 4180 } 4181 if estimate != estFeeWithDust { 4182 t.Fatalf("expected estimate to be %v, got %v)", estFeeWithDust, estimate) 4183 } 4184 4185 // Invalid address 4186 _, valid, _ := wallet.EstimateSendTxFee("invalidsendaddress", unspentVal, optimalFeeRate, true, false) 4187 if valid { 4188 t.Fatal("Expected false for an invalid address") 4189 } 4190 4191 // Successful estimate for empty address 4192 _, _, err = wallet.EstimateSendTxFee("", unspentVal, optimalFeeRate, true, false) 4193 if err != nil { 4194 t.Fatalf("Error for empty address: %v", err) 4195 } 4196 4197 // Zero send amount 4198 _, _, err = wallet.EstimateSendTxFee(addr, 0, optimalFeeRate, true, false) 4199 if err == nil { 4200 t.Fatal("Expected error, send amount is zero") 4201 } 4202 } 4203 4204 func TestConfirmRedemption(t *testing.T) { 4205 wallet, node, shutdown := tNewWallet() 4206 defer shutdown() 4207 4208 swapVal := toAtoms(5) 4209 secret := randBytes(32) 4210 secretHash := sha256.Sum256(secret) 4211 lockTime := time.Now().Add(time.Hour * 12) 4212 addr := tPKHAddr.String() 4213 4214 contract, err := dexdcr.MakeContract(addr, addr, secretHash[:], lockTime.Unix(), tChainParams) 4215 if err != nil { 4216 t.Fatalf("error making swap contract: %v", err) 4217 } 4218 4219 privBytes, _ := hex.DecodeString("b07209eec1a8fb6cfe5cb6ace36567406971a75c330db7101fb21bc679bc5330") 4220 4221 node.changeAddr = tPKHAddr 4222 node.privWIF, err = dcrutil.NewWIF(privBytes, tChainParams.PrivateKeyID, dcrec.STEcdsaSecp256k1) 4223 if err != nil { 4224 t.Fatalf("NewWIF error: %v", err) 4225 } 4226 4227 contractAddr, _ := stdaddr.NewAddressScriptHashV0(contract, tChainParams) 4228 _, contractP2SHScript := contractAddr.PaymentScript() 4229 4230 redemptionScript, _ := dexdcr.RedeemP2SHContract(contract, randBytes(73), randBytes(33), secret) 4231 4232 spentTx := makeRawTx(nil, []dex.Bytes{contractP2SHScript}) 4233 txHash := spentTx.TxHash() 4234 node.blockchain.addRawTx(1, spentTx) 4235 inputs := []*wire.TxIn{makeRPCVin(&txHash, 0, redemptionScript)} 4236 spenderTx := makeRawTx(inputs, nil) 4237 node.blockchain.addRawTx(2, spenderTx) 4238 4239 tip, _ := wallet.getBestBlock(wallet.ctx) 4240 wallet.currentTip.Store(tip) 4241 4242 txFn := func(doErr []bool) func() (*walletjson.GetTransactionResult, error) { 4243 var i int 4244 return func() (*walletjson.GetTransactionResult, error) { 4245 defer func() { i++ }() 4246 if doErr[i] { 4247 return nil, asset.CoinNotFoundError 4248 } 4249 b, err := spenderTx.Bytes() // spender is redeem, searched first 4250 if err != nil { 4251 t.Fatal(err) 4252 } 4253 if i > 0 { 4254 b, err = spentTx.Bytes() // spent is swap, searched if the fist call was a forced error 4255 if err != nil { 4256 t.Fatal(err) 4257 } 4258 } 4259 h := hex.EncodeToString(b) 4260 return &walletjson.GetTransactionResult{ 4261 BlockHash: hex.EncodeToString(randBytes(32)), 4262 Hex: h, 4263 Confirmations: int64(i), // 0 for redeem and 1 for swap 4264 }, nil 4265 } 4266 } 4267 4268 coin := newOutput(&txHash, 0, swapVal, wire.TxTreeRegular) 4269 4270 ci := &asset.AuditInfo{ 4271 Coin: coin, 4272 Contract: contract, 4273 Recipient: tPKHAddr.String(), 4274 Expiration: lockTime, 4275 SecretHash: secretHash[:], 4276 } 4277 4278 redemption := &asset.Redemption{ 4279 Spends: ci, 4280 Secret: secret, 4281 } 4282 4283 coinID := coin.ID() 4284 // Inverting the first byte. 4285 badCoinID := append(append(make([]byte, 0, len(coinID)), ^coinID[0]), coinID[1:]...) 4286 4287 tests := []struct { 4288 name string 4289 redemption *asset.Redemption 4290 coinID []byte 4291 wantErr bool 4292 bestBlockErr error 4293 txRes func() (*walletjson.GetTransactionResult, error) 4294 wantConfs uint64 4295 mempoolRedeems map[[32]byte]*mempoolRedeem 4296 txOutRes map[outPoint]*chainjson.GetTxOutResult 4297 unspentOutputErr error 4298 }{{ 4299 name: "ok tx never seen before now", 4300 coinID: coinID, 4301 redemption: redemption, 4302 txRes: txFn([]bool{false}), 4303 }, { 4304 name: "ok tx in map", 4305 coinID: coinID, 4306 redemption: redemption, 4307 txRes: txFn([]bool{false}), 4308 mempoolRedeems: map[[32]byte]*mempoolRedeem{secretHash: {txHash: txHash, firstSeen: time.Now()}}, 4309 }, { 4310 name: "tx in map has different hash than coin id", 4311 coinID: badCoinID, 4312 redemption: redemption, 4313 txRes: txFn([]bool{false}), 4314 mempoolRedeems: map[[32]byte]*mempoolRedeem{secretHash: {txHash: txHash, firstSeen: time.Now()}}, 4315 wantErr: true, 4316 }, { 4317 name: "ok tx not found spent new tx", 4318 coinID: coinID, 4319 redemption: redemption, 4320 txRes: txFn([]bool{false}), 4321 txOutRes: map[outPoint]*chainjson.GetTxOutResult{newOutPoint(&txHash, 0): makeGetTxOutRes(0, 5, nil)}, 4322 }, { 4323 name: "ok old tx should maybe be abandoned", 4324 coinID: coinID, 4325 redemption: redemption, 4326 txRes: txFn([]bool{false}), 4327 mempoolRedeems: map[[32]byte]*mempoolRedeem{secretHash: {txHash: txHash, firstSeen: time.Now().Add(-maxRedeemMempoolAge - time.Second)}}, 4328 }, { 4329 name: "ok and spent", 4330 coinID: coinID, 4331 txRes: txFn([]bool{true, false}), 4332 redemption: redemption, 4333 wantConfs: 1, // one confirm because this tx is in the best block 4334 }, { 4335 name: "ok and spent but we dont know who spent it", 4336 coinID: coinID, 4337 txRes: txFn([]bool{true, false}), 4338 redemption: func() *asset.Redemption { 4339 ci := &asset.AuditInfo{ 4340 Coin: coin, 4341 Contract: contract, 4342 Recipient: tPKHAddr.String(), 4343 Expiration: lockTime, 4344 SecretHash: make([]byte, 32), // fake secret hash 4345 } 4346 return &asset.Redemption{ 4347 Spends: ci, 4348 Secret: secret, 4349 } 4350 }(), 4351 wantConfs: requiredRedeemConfirms, 4352 }, { 4353 name: "get transaction error", 4354 coinID: coinID, 4355 redemption: redemption, 4356 txRes: txFn([]bool{true, true}), 4357 wantErr: true, 4358 }, { 4359 name: "decode coin error", 4360 coinID: nil, 4361 redemption: redemption, 4362 txRes: txFn([]bool{true, false}), 4363 wantErr: true, 4364 }, { 4365 name: "redeem error", 4366 coinID: coinID, 4367 txOutRes: map[outPoint]*chainjson.GetTxOutResult{newOutPoint(&txHash, 0): makeGetTxOutRes(0, 5, nil)}, 4368 txRes: txFn([]bool{true, false}), 4369 redemption: func() *asset.Redemption { 4370 ci := &asset.AuditInfo{ 4371 Coin: coin, 4372 // Contract: contract, 4373 Recipient: tPKHAddr.String(), 4374 Expiration: lockTime, 4375 SecretHash: secretHash[:], 4376 } 4377 return &asset.Redemption{ 4378 Spends: ci, 4379 Secret: secret, 4380 } 4381 }(), 4382 wantErr: true, 4383 }} 4384 for _, test := range tests { 4385 node.walletTxFn = test.txRes 4386 node.bestBlockErr = test.bestBlockErr 4387 wallet.mempoolRedeems = test.mempoolRedeems 4388 if wallet.mempoolRedeems == nil { 4389 wallet.mempoolRedeems = make(map[[32]byte]*mempoolRedeem) 4390 } 4391 node.txOutRes = test.txOutRes 4392 if node.txOutRes == nil { 4393 node.txOutRes = make(map[outPoint]*chainjson.GetTxOutResult) 4394 } 4395 status, err := wallet.ConfirmRedemption(test.coinID, test.redemption, 0) 4396 if test.wantErr { 4397 if err == nil { 4398 t.Fatalf("%q: expected error", test.name) 4399 } 4400 continue 4401 } 4402 if err != nil { 4403 t.Fatalf("%q: unexpected error: %v", test.name, err) 4404 } 4405 if status.Confs != test.wantConfs { 4406 t.Fatalf("%q: wanted %d confs but got %d", test.name, test.wantConfs, status.Confs) 4407 } 4408 } 4409 } 4410 4411 func TestPurchaseTickets(t *testing.T) { 4412 const feeSuggestion = 100 4413 const sdiff = 1 4414 4415 wallet, cl, shutdown := tNewWalletMonitorBlocks(false) 4416 defer shutdown() 4417 wallet.connected.Store(true) 4418 cl.stakeInfo.Difficulty = dcrutil.Amount(sdiff).ToCoin() 4419 cl.balanceResult = &walletjson.GetBalanceResult{Balances: []walletjson.GetAccountBalanceResult{{AccountName: tAcctName}}} 4420 setBalance := func(n, reserves uint64) { 4421 ticketCost := n * (sdiff + feeSuggestion*minVSPTicketPurchaseSize) 4422 cl.balanceResult.Balances[0].Spendable = dcrutil.Amount(ticketCost).ToCoin() 4423 wallet.bondReserves.Store(reserves) 4424 } 4425 4426 var blocksToConfirm atomic.Int64 4427 cl.walletTxFn = func() (*walletjson.GetTransactionResult, error) { 4428 txHex, _ := makeTxHex(nil, []dex.Bytes{randBytes(25)}) 4429 var confs int64 = 1 4430 if blocksToConfirm.Load() > 0 { 4431 confs = 0 4432 } 4433 return &walletjson.GetTransactionResult{Hex: txHex, Confirmations: confs}, nil 4434 } 4435 4436 var remains []uint32 4437 checkRemains := func(exp ...uint32) { 4438 t.Helper() 4439 if len(remains) != len(exp) { 4440 t.Fatalf("wrong number of remains, wanted %d, got %+v", len(exp), remains) 4441 } 4442 for i := 0; i < len(remains); i++ { 4443 if remains[i] != exp[i] { 4444 t.Fatalf("wrong remains updates: wanted %+v, got %+v", exp, remains) 4445 } 4446 } 4447 } 4448 4449 waitForTicketLoopToExit := func() { 4450 // Ensure the loop closes 4451 timeout := time.After(time.Second) 4452 for { 4453 if !wallet.ticketBuyer.running.Load() { 4454 break 4455 } 4456 select { 4457 case <-time.After(time.Millisecond): 4458 return 4459 case <-timeout: 4460 t.Fatalf("ticket loop didn't exit") 4461 } 4462 } 4463 } 4464 4465 buyTickets := func(n int, wantErr bool) { 4466 defer waitForTicketLoopToExit() 4467 remains = make([]uint32, 0) 4468 if err := wallet.PurchaseTickets(n, feeSuggestion); err != nil { 4469 t.Fatalf("initial PurchaseTickets error: %v", err) 4470 } 4471 4472 var emitted int 4473 timeout := time.After(time.Second) 4474 out: 4475 for { 4476 var ni asset.WalletNotification 4477 select { 4478 case ni = <-cl.emitC: 4479 case <-timeout: 4480 t.Fatalf("timed out looking for ticket updates") 4481 default: 4482 blocksToConfirm.Add(-1) 4483 wallet.runTicketBuyer() 4484 continue 4485 } 4486 switch nt := ni.(type) { 4487 case *asset.CustomWalletNote: 4488 switch n := nt.Payload.(type) { 4489 case *TicketPurchaseUpdate: 4490 remains = append(remains, n.Remaining) 4491 if n.Err != "" { 4492 if wantErr { 4493 return 4494 } 4495 t.Fatalf("Error received in TicketPurchaseUpdate: %s", n.Err) 4496 } 4497 if n.Remaining == 0 { 4498 break out 4499 } 4500 emitted++ 4501 } 4502 4503 } 4504 } 4505 } 4506 4507 tixHashes := func(n int) []*chainhash.Hash { 4508 hs := make([]*chainhash.Hash, n) 4509 for i := 0; i < n; i++ { 4510 var ticketHash chainhash.Hash 4511 copy(ticketHash[:], randBytes(32)) 4512 hs[i] = &ticketHash 4513 } 4514 return hs 4515 } 4516 4517 // Single ticket purchased right away. 4518 cl.purchasedTickets = [][]*chainhash.Hash{tixHashes(1)} 4519 setBalance(1, 0) 4520 buyTickets(1, false) 4521 checkRemains(1, 0) 4522 4523 // Multiple tickets purchased right away. 4524 cl.purchasedTickets = [][]*chainhash.Hash{tixHashes(2)} 4525 setBalance(2, 0) 4526 buyTickets(2, false) 4527 checkRemains(2, 0) 4528 4529 // Two tickets, purchased in two tries, skipping some tries for unconfirmed 4530 // tickets. 4531 blocksToConfirm.Store(3) 4532 cl.purchasedTickets = [][]*chainhash.Hash{tixHashes(1), tixHashes(1)} 4533 buyTickets(2, false) 4534 checkRemains(2, 1, 0) 4535 4536 // (Wallet).PurchaseTickets error 4537 cl.purchasedTickets = [][]*chainhash.Hash{tixHashes(4)} 4538 cl.purchaseTicketsErr = errors.New("test error") 4539 setBalance(4, 0) 4540 buyTickets(4, true) 4541 checkRemains(4, 0) 4542 4543 // Low-balance error 4544 cl.purchasedTickets = [][]*chainhash.Hash{tixHashes(1)} 4545 setBalance(1, 1) // reserves make our available balance 0 4546 buyTickets(1, true) 4547 checkRemains(1, 0) 4548 } 4549 4550 func TestFindBond(t *testing.T) { 4551 wallet, node, shutdown := tNewWallet() 4552 defer shutdown() 4553 4554 privBytes, _ := hex.DecodeString("b07209eec1a8fb6cfe5cb6ace36567406971a75c330db7101fb21bc679bc5330") 4555 bondKey := secp256k1.PrivKeyFromBytes(privBytes) 4556 4557 amt := uint64(50_000) 4558 acctID := [32]byte{} 4559 lockTime := time.Now().Add(time.Hour * 12) 4560 utxo := walletjson.ListUnspentResult{ 4561 TxID: tTxID, 4562 Address: tPKHAddr.String(), 4563 Account: tAcctName, 4564 Amount: 1.0, 4565 Confirmations: 1, 4566 ScriptPubKey: hex.EncodeToString(tP2PKHScript), 4567 Spendable: true, 4568 } 4569 node.unspent = []walletjson.ListUnspentResult{utxo} 4570 node.newAddr = tPKHAddr 4571 node.changeAddr = tPKHAddr 4572 4573 bond, _, err := wallet.MakeBondTx(0, amt, 200, lockTime, bondKey, acctID[:]) 4574 if err != nil { 4575 t.Fatal(err) 4576 } 4577 4578 txFn := func(err error, tx []byte) func() (*walletjson.GetTransactionResult, error) { 4579 return func() (*walletjson.GetTransactionResult, error) { 4580 if err != nil { 4581 return nil, err 4582 } 4583 h := hex.EncodeToString(tx) 4584 return &walletjson.GetTransactionResult{ 4585 BlockHash: hex.EncodeToString(randBytes(32)), 4586 Hex: h, 4587 }, nil 4588 } 4589 } 4590 4591 newBondTx := func() *wire.MsgTx { 4592 msgTx := wire.NewMsgTx() 4593 if err := msgTx.FromBytes(bond.SignedTx); err != nil { 4594 t.Fatal(err) 4595 } 4596 return msgTx 4597 } 4598 tooFewOutputs := newBondTx() 4599 tooFewOutputs.TxOut = tooFewOutputs.TxOut[2:] 4600 tooFewOutputsBytes, err := tooFewOutputs.Bytes() 4601 if err != nil { 4602 t.Fatal(err) 4603 } 4604 4605 badBondScript := newBondTx() 4606 badBondScript.TxOut[1].PkScript = badBondScript.TxOut[1].PkScript[1:] 4607 badBondScriptBytes, err := badBondScript.Bytes() 4608 if err != nil { 4609 t.Fatal(err) 4610 } 4611 4612 noBondMatch := newBondTx() 4613 noBondMatch.TxOut[0].PkScript = noBondMatch.TxOut[0].PkScript[1:] 4614 noBondMatchBytes, err := noBondMatch.Bytes() 4615 if err != nil { 4616 t.Fatal(err) 4617 } 4618 4619 node.blockchain.addRawTx(1, newBondTx()) 4620 verboseBlocks := node.blockchain.verboseBlocks 4621 4622 tests := []struct { 4623 name string 4624 coinID []byte 4625 txRes func() (*walletjson.GetTransactionResult, error) 4626 bestBlockErr error 4627 verboseBlocks map[chainhash.Hash]*wire.MsgBlock 4628 searchUntil time.Time 4629 wantErr bool 4630 }{{ 4631 name: "ok", 4632 coinID: bond.CoinID, 4633 txRes: txFn(nil, bond.SignedTx), 4634 }, { 4635 name: "ok with find blocks", 4636 coinID: bond.CoinID, 4637 txRes: txFn(asset.CoinNotFoundError, nil), 4638 }, { 4639 name: "bad coin id", 4640 coinID: make([]byte, 0), 4641 txRes: txFn(nil, bond.SignedTx), 4642 wantErr: true, 4643 }, { 4644 name: "missing an output", 4645 coinID: bond.CoinID, 4646 txRes: txFn(nil, tooFewOutputsBytes), 4647 wantErr: true, 4648 }, { 4649 name: "bad bond commitment script", 4650 coinID: bond.CoinID, 4651 txRes: txFn(nil, badBondScriptBytes), 4652 wantErr: true, 4653 }, { 4654 name: "bond script does not match commitment", 4655 coinID: bond.CoinID, 4656 txRes: txFn(nil, noBondMatchBytes), 4657 wantErr: true, 4658 }, { 4659 name: "bad msgtx", 4660 coinID: bond.CoinID, 4661 txRes: txFn(nil, bond.SignedTx[100:]), 4662 wantErr: true, 4663 }, { 4664 name: "get best block error", 4665 coinID: bond.CoinID, 4666 txRes: txFn(asset.CoinNotFoundError, nil), 4667 bestBlockErr: errors.New("some error"), 4668 wantErr: true, 4669 }, { 4670 name: "block not found", 4671 coinID: bond.CoinID, 4672 txRes: txFn(asset.CoinNotFoundError, nil), 4673 verboseBlocks: map[chainhash.Hash]*wire.MsgBlock{}, 4674 wantErr: true, 4675 }} 4676 4677 for _, test := range tests { 4678 t.Run(test.name, func(t *testing.T) { 4679 node.walletTxFn = test.txRes 4680 node.bestBlockErr = test.bestBlockErr 4681 node.blockchain.verboseBlocks = verboseBlocks 4682 if test.verboseBlocks != nil { 4683 node.blockchain.verboseBlocks = test.verboseBlocks 4684 } 4685 bd, err := wallet.FindBond(tCtx, test.coinID, test.searchUntil) 4686 if test.wantErr { 4687 if err == nil { 4688 t.Fatal("expected error") 4689 } 4690 return 4691 } 4692 if err != nil { 4693 t.Fatalf("unexpected error: %v", err) 4694 } 4695 if !bd.CheckPrivKey(bondKey) { 4696 t.Fatal("pkh not equal") 4697 } 4698 }) 4699 } 4700 } 4701 4702 func makeSwapContract(lockTimeOffset time.Duration) (pkScriptVer uint16, pkScript []byte) { 4703 secret := randBytes(32) 4704 secretHash := sha256.Sum256(secret) 4705 4706 lockTime := time.Now().Add(lockTimeOffset) 4707 var err error 4708 contract, err := dexdcr.MakeContract(tPKHAddr.String(), tPKHAddr.String(), secretHash[:], lockTime.Unix(), chaincfg.MainNetParams()) 4709 if err != nil { 4710 panic("error making swap contract:" + err.Error()) 4711 } 4712 4713 scriptAddr, err := stdaddr.NewAddressScriptHashV0(contract, chaincfg.MainNetParams()) 4714 if err != nil { 4715 panic("error making script address:" + err.Error()) 4716 } 4717 4718 return scriptAddr.PaymentScript() 4719 } 4720 4721 func TestIDUnknownTx(t *testing.T) { 4722 // Swap Tx - any tx with p2sh outputs that is not a bond. 4723 _, swapPKScript := makeSwapContract(time.Hour * 12) 4724 swapTx := &wire.MsgTx{ 4725 TxIn: []*wire.TxIn{wire.NewTxIn(&wire.OutPoint{}, 0, nil)}, 4726 TxOut: []*wire.TxOut{wire.NewTxOut(int64(toAtoms(1)), swapPKScript)}, 4727 } 4728 4729 // Redeem Tx 4730 swapContract, _ := dexdcr.MakeContract(tPKHAddr.String(), tPKHAddr.String(), randBytes(32), time.Now().Unix(), chaincfg.MainNetParams()) 4731 txIn := wire.NewTxIn(&wire.OutPoint{}, 0, nil) 4732 txIn.SignatureScript, _ = dexdcr.RedeemP2SHContract(swapContract, randBytes(73), randBytes(33), randBytes(32)) 4733 redeemFee := 0.0000143 4734 _, tP2PKH := tPKHAddr.PaymentScript() 4735 redemptionTx := &wire.MsgTx{ 4736 TxIn: []*wire.TxIn{txIn}, 4737 TxOut: []*wire.TxOut{wire.NewTxOut(int64(toAtoms(5-redeemFee)), tP2PKH)}, 4738 } 4739 4740 h2b := func(h string) []byte { 4741 b, _ := hex.DecodeString(h) 4742 return b 4743 } 4744 4745 // Create Bond Tx 4746 bondLockTime := 1711637410 4747 bondID := h2b("0e39bbb09592fd00b7d770cc832ddf4d625ae3a0") 4748 accountID := h2b("a0836b39b5ceb84f422b8a8cd5940117087a8522457c6d81d200557652fbe6ea") 4749 bondContract, _ := dexdcr.MakeBondScript(0, uint32(bondLockTime), bondID) 4750 contractAddr, err := stdaddr.NewAddressScriptHashV0(bondContract, chaincfg.MainNetParams()) 4751 if err != nil { 4752 t.Fatal("error making script address:" + err.Error()) 4753 } 4754 _, bondPkScript := contractAddr.PaymentScript() 4755 4756 bondOutput := wire.NewTxOut(int64(toAtoms(2)), bondPkScript) 4757 bondCommitPkScript, _ := bondPushDataScript(0, accountID, int64(bondLockTime), bondID) 4758 bondCommitmentOutput := wire.NewTxOut(0, bondCommitPkScript) 4759 createBondTx := &wire.MsgTx{ 4760 TxIn: []*wire.TxIn{wire.NewTxIn(&wire.OutPoint{}, 0, nil)}, 4761 TxOut: []*wire.TxOut{bondOutput, bondCommitmentOutput}, 4762 } 4763 4764 // Redeem Bond Tx 4765 txIn = wire.NewTxIn(&wire.OutPoint{}, 0, nil) 4766 txIn.SignatureScript, _ = dexdcr.RefundBondScript(bondContract, randBytes(73), randBytes(33)) 4767 redeemBondTx := &wire.MsgTx{ 4768 TxIn: []*wire.TxIn{txIn}, 4769 TxOut: []*wire.TxOut{wire.NewTxOut(int64(toAtoms(5)), tP2PKH)}, 4770 } 4771 4772 // Split Tx 4773 splitTx := &wire.MsgTx{ 4774 TxIn: []*wire.TxIn{wire.NewTxIn(&wire.OutPoint{}, 0, nil)}, 4775 TxOut: []*wire.TxOut{wire.NewTxOut(0, tP2PKH), wire.NewTxOut(0, tP2PKH)}, 4776 } 4777 4778 // Send Tx 4779 cpAddr, _ := stdaddr.DecodeAddress("Dsedb5o6Tw225Loq5J56BZ9jS4ehnEnmQ16", tChainParams) 4780 _, cpPkScript := cpAddr.PaymentScript() 4781 sendTx := &wire.MsgTx{ 4782 TxIn: []*wire.TxIn{wire.NewTxIn(&wire.OutPoint{}, 0, nil)}, 4783 TxOut: []*wire.TxOut{wire.NewTxOut(int64(toAtoms(0.001)), tP2PKH), wire.NewTxOut(int64(toAtoms(4)), cpPkScript)}, 4784 } 4785 4786 // Receive Tx 4787 receiveTx := &wire.MsgTx{ 4788 TxIn: []*wire.TxIn{wire.NewTxIn(&wire.OutPoint{}, 0, nil)}, 4789 TxOut: []*wire.TxOut{wire.NewTxOut(int64(toAtoms(0.001)), cpPkScript), wire.NewTxOut(int64(toAtoms(4)), tP2PKH)}, 4790 } 4791 type test struct { 4792 name string 4793 ltr *ListTransactionsResult 4794 tx *wire.MsgTx 4795 validateAddress map[string]*walletjson.ValidateAddressResult 4796 exp *asset.WalletTransaction 4797 } 4798 4799 // Ticket Tx 4800 ticketTx := &wire.MsgTx{ 4801 TxIn: []*wire.TxIn{wire.NewTxIn(&wire.OutPoint{}, 0, nil)}, 4802 TxOut: []*wire.TxOut{wire.NewTxOut(int64(toAtoms(1)), cpPkScript)}, 4803 } 4804 4805 float64Ptr := func(f float64) *float64 { 4806 return &f 4807 } 4808 4809 stringPtr := func(s string) *string { 4810 return &s 4811 } 4812 4813 regularTx := walletjson.LTTTRegular 4814 ticketPurchaseTx := walletjson.LTTTTicket 4815 ticketRevocationTx := walletjson.LTTTRevocation 4816 ticketVote := walletjson.LTTTVote 4817 4818 tests := []*test{ 4819 { 4820 name: "swap", 4821 ltr: &ListTransactionsResult{ 4822 TxType: ®ularTx, 4823 Fee: float64Ptr(0.0000321), 4824 TxID: swapTx.TxHash().String(), 4825 }, 4826 tx: swapTx, 4827 exp: &asset.WalletTransaction{ 4828 Type: asset.SwapOrSend, 4829 ID: swapTx.TxHash().String(), 4830 Amount: toAtoms(1), 4831 Fees: toAtoms(0.0000321), 4832 }, 4833 }, 4834 { 4835 name: "redeem", 4836 ltr: &ListTransactionsResult{ 4837 TxType: ®ularTx, 4838 TxID: redemptionTx.TxHash().String(), 4839 }, 4840 tx: redemptionTx, 4841 exp: &asset.WalletTransaction{ 4842 Type: asset.Redeem, 4843 ID: redemptionTx.TxHash().String(), 4844 Amount: toAtoms(5 - redeemFee), 4845 Fees: 0, 4846 }, 4847 }, 4848 { 4849 name: "create bond", 4850 ltr: &ListTransactionsResult{ 4851 TxType: ®ularTx, 4852 Fee: float64Ptr(0.0000222), 4853 TxID: createBondTx.TxHash().String(), 4854 }, 4855 tx: createBondTx, 4856 exp: &asset.WalletTransaction{ 4857 Type: asset.CreateBond, 4858 ID: createBondTx.TxHash().String(), 4859 Amount: toAtoms(2), 4860 Fees: toAtoms(0.0000222), 4861 BondInfo: &asset.BondTxInfo{ 4862 AccountID: accountID, 4863 BondID: bondID, 4864 LockTime: uint64(bondLockTime), 4865 }, 4866 }, 4867 }, 4868 { 4869 name: "redeem bond", 4870 ltr: &ListTransactionsResult{ 4871 TxType: ®ularTx, 4872 TxID: redeemBondTx.TxHash().String(), 4873 }, 4874 tx: redeemBondTx, 4875 exp: &asset.WalletTransaction{ 4876 Type: asset.RedeemBond, 4877 ID: redeemBondTx.TxHash().String(), 4878 Amount: toAtoms(5), 4879 BondInfo: &asset.BondTxInfo{ 4880 AccountID: []byte{}, 4881 BondID: bondID, 4882 LockTime: uint64(bondLockTime), 4883 }, 4884 }, 4885 }, 4886 { 4887 name: "split", 4888 ltr: &ListTransactionsResult{ 4889 TxType: ®ularTx, 4890 Fee: float64Ptr(-0.0000251), 4891 Send: true, 4892 TxID: splitTx.TxHash().String(), 4893 }, 4894 tx: splitTx, 4895 exp: &asset.WalletTransaction{ 4896 Type: asset.Split, 4897 ID: splitTx.TxHash().String(), 4898 Fees: toAtoms(0.0000251), 4899 }, 4900 }, 4901 { 4902 name: "send", 4903 ltr: &ListTransactionsResult{ 4904 TxType: ®ularTx, 4905 Send: true, 4906 Fee: float64Ptr(0.0000504), 4907 TxID: sendTx.TxHash().String(), 4908 }, 4909 tx: sendTx, 4910 exp: &asset.WalletTransaction{ 4911 Type: asset.Send, 4912 ID: sendTx.TxHash().String(), 4913 Amount: toAtoms(4), 4914 Recipient: stringPtr(cpAddr.String()), 4915 Fees: toAtoms(0.0000504), 4916 }, 4917 validateAddress: map[string]*walletjson.ValidateAddressResult{ 4918 tPKHAddr.String(): { 4919 IsMine: true, 4920 Account: tAcctName, 4921 }, 4922 }, 4923 }, 4924 { 4925 name: "receive", 4926 ltr: &ListTransactionsResult{ 4927 TxType: ®ularTx, 4928 TxID: receiveTx.TxHash().String(), 4929 }, 4930 tx: receiveTx, 4931 exp: &asset.WalletTransaction{ 4932 Type: asset.Receive, 4933 ID: receiveTx.TxHash().String(), 4934 Amount: toAtoms(4), 4935 Recipient: stringPtr(tPKHAddr.String()), 4936 }, 4937 validateAddress: map[string]*walletjson.ValidateAddressResult{ 4938 tPKHAddr.String(): { 4939 IsMine: true, 4940 Account: tAcctName, 4941 }, 4942 }, 4943 }, 4944 { 4945 name: "ticket purchase", 4946 ltr: &ListTransactionsResult{ 4947 TxType: &ticketPurchaseTx, 4948 TxID: ticketTx.TxHash().String(), 4949 }, 4950 tx: ticketTx, 4951 exp: &asset.WalletTransaction{ 4952 Type: asset.TicketPurchase, 4953 ID: ticketTx.TxHash().String(), 4954 Amount: toAtoms(1), 4955 }, 4956 }, 4957 { 4958 name: "ticket vote", 4959 ltr: &ListTransactionsResult{ 4960 TxType: &ticketVote, 4961 TxID: ticketTx.TxHash().String(), 4962 }, 4963 tx: ticketTx, 4964 exp: &asset.WalletTransaction{ 4965 Type: asset.TicketVote, 4966 ID: ticketTx.TxHash().String(), 4967 Amount: toAtoms(1), 4968 }, 4969 }, 4970 { 4971 name: "ticket revocation", 4972 ltr: &ListTransactionsResult{ 4973 TxType: &ticketRevocationTx, 4974 TxID: ticketTx.TxHash().String(), 4975 }, 4976 tx: ticketTx, 4977 exp: &asset.WalletTransaction{ 4978 Type: asset.TicketRevocation, 4979 ID: ticketTx.TxHash().String(), 4980 Amount: toAtoms(1), 4981 }, 4982 }, 4983 } 4984 4985 runTest := func(tt *test) { 4986 t.Run(tt.name, func(t *testing.T) { 4987 wallet, node, shutdown := tNewWallet() 4988 defer shutdown() 4989 node.validateAddress = tt.validateAddress 4990 node.blockchain.rawTxs[tt.tx.TxHash()] = &wireTxWithHeight{ 4991 tx: tt.tx, 4992 } 4993 wt, err := wallet.idUnknownTx(context.Background(), tt.ltr) 4994 if err != nil { 4995 t.Fatalf("%s: unexpected error: %v", tt.name, err) 4996 } 4997 if !reflect.DeepEqual(wt, tt.exp) { 4998 t.Fatalf("%s: expected %+v, got %+v", tt.name, tt.exp, wt) 4999 } 5000 }) 5001 } 5002 5003 for _, tt := range tests { 5004 runTest(tt) 5005 } 5006 } 5007 5008 func TestRescanSync(t *testing.T) { 5009 wallet, node, shutdown := tNewWalletMonitorBlocks(false) 5010 defer shutdown() 5011 5012 const tip = 1000 5013 wallet.currentTip.Store(&block{height: tip}) 5014 5015 node.rawRes[methodSyncStatus], node.rawErr[methodSyncStatus] = json.Marshal(&walletjson.SyncStatusResult{ 5016 Synced: true, 5017 InitialBlockDownload: false, 5018 HeadersFetchProgress: 1, 5019 }) 5020 5021 node.blockchain.mainchain[tip] = &chainhash.Hash{} 5022 5023 checkProgress := func(expSynced bool, expProgress float32) { 5024 t.Helper() 5025 ss, err := wallet.SyncStatus() 5026 if err != nil { 5027 t.Fatalf("Unexpected error: %v", err) 5028 } 5029 if ss.Synced != expSynced { 5030 t.Fatalf("expected synced = %t, bot %t", expSynced, ss.Synced) 5031 } 5032 if !ss.Synced { 5033 txProgress := float32(*ss.Transactions) / float32(ss.TargetHeight) 5034 if math.Abs(float64(expProgress/txProgress)-1) > 0.001 { 5035 t.Fatalf("expected progress %f, got %f", expProgress, txProgress) 5036 } 5037 } 5038 } 5039 5040 // No rescan in progress. 5041 checkProgress(true, 1) 5042 5043 // Rescan running. No progress. 5044 wallet.rescan.progress = &rescanProgress{} 5045 checkProgress(false, 0) 5046 5047 // Halfway done. 5048 wallet.rescan.progress = &rescanProgress{scannedThrough: tip / 2} 5049 checkProgress(false, 0.5) 5050 5051 // Not synced until progress is nil. 5052 wallet.rescan.progress = &rescanProgress{scannedThrough: tip} 5053 checkProgress(false, 1) 5054 5055 // Scanned > tip OK 5056 wallet.rescan.progress = &rescanProgress{scannedThrough: tip * 2} 5057 checkProgress(false, 1) 5058 5059 // Rescan complete. 5060 wallet.rescan.progress = nil 5061 checkProgress(true, 1) 5062 5063 }