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