decred.org/dcrdex@v1.0.5/client/asset/btc/btc_test.go (about) 1 //go:build !spvlive && !harness 2 3 package btc 4 5 import ( 6 "bytes" 7 "context" 8 "crypto/sha256" 9 "encoding/base64" 10 "encoding/hex" 11 "encoding/json" 12 "errors" 13 "fmt" 14 "math/rand" 15 "os" 16 "reflect" 17 "sort" 18 "strings" 19 "sync" 20 "testing" 21 "time" 22 23 "decred.org/dcrdex/client/asset" 24 "decred.org/dcrdex/dex" 25 "decred.org/dcrdex/dex/calc" 26 "decred.org/dcrdex/dex/encode" 27 dexbtc "decred.org/dcrdex/dex/networks/btc" 28 "github.com/btcsuite/btcd/btcec/v2" 29 "github.com/btcsuite/btcd/btcec/v2/ecdsa" 30 "github.com/btcsuite/btcd/btcjson" 31 "github.com/btcsuite/btcd/btcutil" 32 "github.com/btcsuite/btcd/chaincfg" 33 "github.com/btcsuite/btcd/chaincfg/chainhash" 34 "github.com/btcsuite/btcd/txscript" 35 "github.com/btcsuite/btcd/wire" 36 "github.com/btcsuite/btcwallet/wallet" 37 ) 38 39 var ( 40 tLogger dex.Logger 41 tCtx context.Context 42 tLotSize uint64 = 1e6 // 0.01 BTC 43 tBTC = &dex.Asset{ 44 ID: 0, 45 Symbol: "btc", 46 Version: version, 47 MaxFeeRate: 34, 48 SwapConf: 1, 49 } 50 tSwapSizeBase uint64 = dexbtc.InitTxSizeBaseSegwit 51 tSwapSize uint64 = dexbtc.InitTxSizeSegwit 52 optimalFeeRate uint64 = 24 53 tErr = fmt.Errorf("test error") 54 tTxID = "308e9a3675fc3ea3862b7863eeead08c621dcc37ff59de597dd3cdab41450ad9" 55 tTxHash *chainhash.Hash 56 tP2PKHAddr = "1Bggq7Vu5oaoLFV1NNp5KhAzcku83qQhgi" 57 tP2PKH []byte 58 tP2WPKH []byte 59 tP2WPKHAddr = "bc1qq49ypf420s0kh52l9pk7ha8n8nhsugdpculjas" 60 feeSuggestion uint64 = 10 61 ) 62 63 func boolPtr(v bool) *bool { 64 return &v 65 } 66 67 func btcAddr(segwit bool) btcutil.Address { 68 var addr btcutil.Address 69 if segwit { 70 addr, _ = btcutil.DecodeAddress(tP2WPKHAddr, &chaincfg.MainNetParams) 71 } else { 72 addr, _ = btcutil.DecodeAddress(tP2PKHAddr, &chaincfg.MainNetParams) 73 } 74 return addr 75 } 76 77 func randBytes(l int) []byte { 78 b := make([]byte, l) 79 rand.Read(b) 80 return b 81 } 82 83 func signFunc(tx *wire.MsgTx, sizeTweak int, segwit bool) { 84 // Set the sigScripts to random bytes of the correct length for spending a 85 // p2pkh output. 86 if segwit { 87 sigSize := 73 + sizeTweak 88 for i := range tx.TxIn { 89 tx.TxIn[i].Witness = wire.TxWitness{ 90 randBytes(sigSize), 91 randBytes(33), 92 } 93 } 94 } else { 95 scriptSize := dexbtc.RedeemP2PKHSigScriptSize + sizeTweak 96 for i := range tx.TxIn { 97 tx.TxIn[i].SignatureScript = randBytes(scriptSize) 98 } 99 } 100 } 101 102 type msgBlockWithHeight struct { 103 msgBlock *wire.MsgBlock 104 height int64 105 } 106 107 type testData struct { 108 walletCfg *baseWalletConfig 109 badSendHash *chainhash.Hash 110 sendErr error 111 sentRawTx *wire.MsgTx 112 txOutRes *btcjson.GetTxOutResult 113 txOutErr error 114 sigIncomplete bool 115 signFunc func(*wire.MsgTx) 116 signMsgFunc func([]json.RawMessage) (json.RawMessage, error) 117 118 blockchainMtx sync.RWMutex 119 verboseBlocks map[chainhash.Hash]*msgBlockWithHeight 120 dbBlockForTx map[chainhash.Hash]*hashEntry 121 mainchain map[int64]*chainhash.Hash 122 getBlockchainInfo *GetBlockchainInfoResult 123 getBestBlockHashErr error 124 125 mempoolTxs map[chainhash.Hash]*wire.MsgTx 126 rawVerboseErr error 127 lockedCoins []*RPCOutpoint 128 estFeeErr error 129 listLockUnspent []*RPCOutpoint 130 getBalances *GetBalancesResult 131 getBalancesErr error 132 lockUnspentErr error 133 changeAddr string 134 changeAddrErr error 135 newAddress string 136 newAddressErr error 137 privKeyForAddr *btcutil.WIF 138 privKeyForAddrErr error 139 birthdayTime time.Time 140 141 // If there is an "any" key in the getTransactionMap, that value will be 142 // returned for all requests. Otherwise the tx id is looked up. 143 getTransactionMap map[string]*GetTransactionResult 144 getTransactionErr error 145 146 getBlockchainInfoErr error 147 unlockErr error 148 lockErr error 149 sendToAddress string 150 sendToAddressErr error 151 setTxFee bool 152 signTxErr error 153 listUnspent []*ListUnspentResult 154 listUnspentErr error 155 tipChanged chan asset.WalletNotification 156 157 // spv 158 fetchInputInfoTx *wire.MsgTx 159 getCFilterScripts map[chainhash.Hash][][]byte 160 checkpoints map[OutPoint]*ScanCheckpoint 161 confs uint32 162 confsSpent bool 163 confsErr error 164 walletTxSpent bool 165 txFee uint64 166 ownedAddresses map[string]bool 167 ownsAddress bool 168 locked bool 169 } 170 171 func newTestData() *testData { 172 // setup genesis block, required by bestblock polling goroutine 173 genesisHash := chaincfg.MainNetParams.GenesisHash 174 return &testData{ 175 txOutRes: newTxOutResult([]byte{}, 1, 0), 176 verboseBlocks: map[chainhash.Hash]*msgBlockWithHeight{ 177 *genesisHash: {msgBlock: &wire.MsgBlock{}}, 178 }, 179 dbBlockForTx: make(map[chainhash.Hash]*hashEntry), 180 mainchain: map[int64]*chainhash.Hash{ 181 0: genesisHash, 182 }, 183 mempoolTxs: make(map[chainhash.Hash]*wire.MsgTx), 184 fetchInputInfoTx: dummyTx(), 185 getCFilterScripts: make(map[chainhash.Hash][][]byte), 186 confsErr: WalletTransactionNotFound, 187 checkpoints: make(map[OutPoint]*ScanCheckpoint), 188 tipChanged: make(chan asset.WalletNotification, 1), 189 getTransactionMap: make(map[string]*GetTransactionResult), 190 } 191 } 192 193 func (c *testData) GetTransactions(startHeight, endHeight int32, accountName string, cancel <-chan struct{}) (*wallet.GetTransactionsResult, error) { 194 return nil, fmt.Errorf("not implemented") 195 } 196 197 func (c *testData) getBlock(blockHash chainhash.Hash) *msgBlockWithHeight { 198 c.blockchainMtx.Lock() 199 defer c.blockchainMtx.Unlock() 200 return c.verboseBlocks[blockHash] 201 } 202 203 func (c *testData) GetBestBlockHeight() int64 { 204 c.blockchainMtx.RLock() 205 defer c.blockchainMtx.RUnlock() 206 var bestBlkHeight int64 207 for height := range c.mainchain { 208 if height >= bestBlkHeight { 209 bestBlkHeight = height 210 } 211 } 212 return bestBlkHeight 213 } 214 215 func (c *testData) bestBlock() (*chainhash.Hash, int64) { 216 c.blockchainMtx.RLock() 217 defer c.blockchainMtx.RUnlock() 218 var bestHash *chainhash.Hash 219 var bestBlkHeight int64 220 for height, hash := range c.mainchain { 221 if height >= bestBlkHeight { 222 bestBlkHeight = height 223 bestHash = hash 224 } 225 } 226 return bestHash, bestBlkHeight 227 } 228 229 func encodeOrError(thing any, err error) (json.RawMessage, error) { 230 if err != nil { 231 return nil, err 232 } 233 return json.Marshal(thing) 234 } 235 236 type tRawRequester struct { 237 *testData 238 } 239 240 func (c *tRawRequester) RawRequest(_ context.Context, method string, params []json.RawMessage) (json.RawMessage, error) { 241 switch method { 242 // TODO: handle methodGetBlockHash and add actual tests to cover it. 243 case methodGetNetworkInfo: 244 return json.Marshal(&btcjson.GetNetworkInfoResult{ 245 Connections: 1, 246 }) 247 case methodEstimateSmartFee: 248 if c.testData.estFeeErr != nil { 249 return nil, c.testData.estFeeErr 250 } 251 optimalRate := float64(optimalFeeRate) * 1e-5 // ~0.00024 252 return json.Marshal(&btcjson.EstimateSmartFeeResult{ 253 Blocks: 2, 254 FeeRate: &optimalRate, 255 }) 256 case methodSendRawTransaction: 257 var txHex string 258 err := json.Unmarshal(params[0], &txHex) 259 if err != nil { 260 return nil, err 261 } 262 tx, err := msgTxFromHex(txHex) 263 if err != nil { 264 return nil, err 265 } 266 c.sentRawTx = tx 267 if c.sendErr == nil && c.badSendHash == nil { 268 h := tx.TxHash().String() 269 return json.Marshal(&h) 270 } 271 if c.sendErr != nil { 272 return nil, c.sendErr 273 } 274 return json.Marshal(c.badSendHash.String()) 275 case methodGetTxOut: 276 return encodeOrError(c.txOutRes, c.txOutErr) 277 case methodGetBestBlockHash: 278 c.blockchainMtx.RLock() 279 if c.getBestBlockHashErr != nil { 280 c.blockchainMtx.RUnlock() 281 return nil, c.getBestBlockHashErr 282 } 283 c.blockchainMtx.RUnlock() 284 bestHash, _ := c.bestBlock() 285 return json.Marshal(bestHash.String()) 286 case methodGetBlockHash: 287 var blockHeight int64 288 if err := json.Unmarshal(params[0], &blockHeight); err != nil { 289 return nil, err 290 } 291 c.blockchainMtx.RLock() 292 defer c.blockchainMtx.RUnlock() 293 for height, blockHash := range c.mainchain { 294 if height == blockHeight { 295 return json.Marshal(blockHash.String()) 296 } 297 } 298 return nil, fmt.Errorf("block not found") 299 300 case methodGetRawMempool: 301 hashes := make([]string, 0, len(c.mempoolTxs)) 302 for txHash := range c.mempoolTxs { 303 hashes = append(hashes, txHash.String()) 304 } 305 return json.Marshal(hashes) 306 case methodGetRawTransaction: 307 if c.testData.rawVerboseErr != nil { 308 return nil, c.testData.rawVerboseErr 309 } 310 var hashStr string 311 err := json.Unmarshal(params[0], &hashStr) 312 if err != nil { 313 return nil, err 314 } 315 txHash, err := chainhash.NewHashFromStr(hashStr) 316 if err != nil { 317 return nil, err 318 } 319 msgTx := c.mempoolTxs[*txHash] 320 if msgTx == nil { 321 return nil, fmt.Errorf("transaction not found") 322 } 323 txB, _ := serializeMsgTx(msgTx) 324 return json.Marshal(hex.EncodeToString(txB)) 325 case methodSignTx: 326 if c.signTxErr != nil { 327 return nil, c.signTxErr 328 } 329 signTxRes := SignTxResult{ 330 Complete: !c.sigIncomplete, 331 } 332 var msgHex string 333 err := json.Unmarshal(params[0], &msgHex) 334 if err != nil { 335 return nil, fmt.Errorf("json.Unmarshal error for tRawRequester -> RawRequest -> methodSignTx: %v", err) 336 } 337 msgBytes, _ := hex.DecodeString(msgHex) 338 txReader := bytes.NewReader(msgBytes) 339 msgTx := wire.NewMsgTx(wire.TxVersion) 340 err = msgTx.Deserialize(txReader) 341 if err != nil { 342 return nil, fmt.Errorf("MsgTx.Deserialize error for tRawRequester -> RawRequest -> methodSignTx: %v", err) 343 } 344 345 c.signFunc(msgTx) 346 347 buf := new(bytes.Buffer) 348 err = msgTx.Serialize(buf) 349 if err != nil { 350 return nil, fmt.Errorf("MsgTx.Serialize error for tRawRequester -> RawRequest -> methodSignTx: %v", err) 351 } 352 signTxRes.Hex = buf.Bytes() 353 return mustMarshal(signTxRes), nil 354 case methodGetBlock: 355 c.blockchainMtx.Lock() 356 defer c.blockchainMtx.Unlock() 357 var blockHashStr string 358 err := json.Unmarshal(params[0], &blockHashStr) 359 if err != nil { 360 return nil, err 361 } 362 blkHash, err := chainhash.NewHashFromStr(blockHashStr) 363 if err != nil { 364 return nil, err 365 } 366 blk, found := c.verboseBlocks[*blkHash] 367 if !found { 368 return nil, fmt.Errorf("block not found") 369 } 370 var buf bytes.Buffer 371 err = blk.msgBlock.Serialize(&buf) 372 if err != nil { 373 return nil, err 374 } 375 return json.Marshal(hex.EncodeToString(buf.Bytes())) 376 377 case methodGetBlockHeader: 378 var blkHashStr string 379 _ = json.Unmarshal(params[0], &blkHashStr) 380 blkHash, err := chainhash.NewHashFromStr(blkHashStr) 381 if err != nil { 382 return nil, err 383 } 384 block := c.getBlock(*blkHash) 385 if block == nil { 386 return nil, fmt.Errorf("no block verbose found") 387 } 388 // block may get modified concurrently, lock mtx before reading fields. 389 c.blockchainMtx.RLock() 390 defer c.blockchainMtx.RUnlock() 391 return json.Marshal(&BlockHeader{ 392 Hash: blkHash.String(), 393 Height: block.height, 394 // Confirmations: block.Confirmations, 395 // Time: block.Time, 396 }) 397 case methodLockUnspent: 398 if c.lockUnspentErr != nil { 399 return json.Marshal(false) 400 } 401 coins := make([]*RPCOutpoint, 0) 402 _ = json.Unmarshal(params[1], &coins) 403 if string(params[0]) == "false" { 404 if c.lockedCoins != nil { 405 c.lockedCoins = append(c.lockedCoins, coins...) 406 } else { 407 c.lockedCoins = coins 408 } 409 } 410 return json.Marshal(true) 411 case methodListLockUnspent: 412 return mustMarshal(c.listLockUnspent), nil 413 case methodGetBalances: 414 return encodeOrError(c.getBalances, c.getBalancesErr) 415 case methodChangeAddress: 416 return encodeOrError(c.changeAddr, c.changeAddrErr) 417 case methodNewAddress: 418 return encodeOrError(c.newAddress, c.newAddressErr) 419 case methodPrivKeyForAddress: 420 if c.privKeyForAddrErr != nil { 421 return nil, c.privKeyForAddrErr 422 } 423 return json.Marshal(c.privKeyForAddr.String()) 424 case methodGetTransaction: 425 if c.getTransactionErr != nil { 426 return nil, c.getTransactionErr 427 } 428 429 c.blockchainMtx.Lock() 430 defer c.blockchainMtx.Unlock() 431 var txID string 432 err := json.Unmarshal(params[0], &txID) 433 if err != nil { 434 return nil, err 435 } 436 437 var txData *GetTransactionResult 438 if c.getTransactionMap != nil { 439 if txData = c.getTransactionMap["any"]; txData == nil { 440 txData = c.getTransactionMap[txID] 441 } 442 } 443 if txData == nil { 444 return nil, WalletTransactionNotFound 445 } 446 return json.Marshal(txData) 447 case methodGetBlockchainInfo: 448 c.blockchainMtx.RLock() 449 defer c.blockchainMtx.RUnlock() 450 return encodeOrError(c.getBlockchainInfo, c.getBlockchainInfoErr) 451 case methodLock: 452 return nil, c.lockErr 453 case methodUnlock: 454 return nil, c.unlockErr 455 case methodSendToAddress: 456 return encodeOrError(c.sendToAddress, c.sendToAddressErr) 457 case methodSetTxFee: 458 return json.Marshal(c.setTxFee) 459 case methodListUnspent: 460 return encodeOrError(c.listUnspent, c.listUnspentErr) 461 case methodFundRawTransaction: 462 b, _ := serializeMsgTx(wire.NewMsgTx(wire.TxVersion)) 463 resp := &struct { 464 Transaction string `json:"hex"` 465 Fee float64 `json:"fee"` 466 }{ 467 Transaction: hex.EncodeToString(b), 468 Fee: toBTC(c.txFee), 469 } 470 return json.Marshal(resp) 471 case methodGetWalletInfo: 472 return json.Marshal(&GetWalletInfoResult{UnlockedUntil: nil /* unencrypted -> unlocked */}) 473 case methodGetAddressInfo: 474 var addr string 475 err := json.Unmarshal(params[0], &addr) 476 if err != nil { 477 panic(err) 478 } 479 owns := c.ownedAddresses != nil && c.ownedAddresses[addr] 480 if !owns { 481 owns = c.ownsAddress 482 } 483 return json.Marshal(&btcjson.GetAddressInfoResult{ 484 IsMine: owns, 485 }) 486 } 487 panic("method not registered: " + method) 488 } 489 490 const testBlocksPerBlockTimeOffset = 4 491 492 func generateTestBlockTime(blockHeight int64) time.Time { 493 return time.Unix(1e6, 0).Add(time.Duration(blockHeight) * maxFutureBlockTime / testBlocksPerBlockTimeOffset) 494 } 495 496 func (c *testData) addRawTx(blockHeight int64, tx *wire.MsgTx) (*chainhash.Hash, *wire.MsgBlock) { 497 c.blockchainMtx.Lock() 498 defer c.blockchainMtx.Unlock() 499 blockHash, found := c.mainchain[blockHeight] 500 if !found { 501 prevBlock := &chainhash.Hash{} 502 if blockHeight > 0 { 503 var exists bool 504 prevBlock, exists = c.mainchain[blockHeight-1] 505 if !exists { 506 prevBlock = &chainhash.Hash{} 507 } 508 } 509 nonce, bits := rand.Uint32(), rand.Uint32() 510 header := wire.NewBlockHeader(0, prevBlock, &chainhash.Hash{} /* lie, maybe fix this */, bits, nonce) 511 header.Timestamp = generateTestBlockTime(blockHeight) 512 msgBlock := wire.NewMsgBlock(header) // only now do we know the block hash 513 hash := msgBlock.BlockHash() 514 blockHash = &hash 515 c.verboseBlocks[*blockHash] = &msgBlockWithHeight{ 516 msgBlock: msgBlock, 517 height: blockHeight, 518 } 519 c.mainchain[blockHeight] = blockHash 520 } 521 block := c.verboseBlocks[*blockHash] 522 // NOTE: Adding a transaction changes the msgBlock.BlockHash() so the 523 // map key is technically always wrong. 524 block.msgBlock.AddTransaction(tx) 525 return blockHash, block.msgBlock 526 } 527 528 func (c *testData) getBlockAtHeight(blockHeight int64) (*chainhash.Hash, *msgBlockWithHeight) { 529 c.blockchainMtx.RLock() 530 defer c.blockchainMtx.RUnlock() 531 blockHash, found := c.mainchain[blockHeight] 532 if !found { 533 return nil, nil 534 } 535 blk := c.verboseBlocks[*blockHash] 536 return blockHash, blk 537 } 538 539 func (c *testData) truncateChains() { 540 c.blockchainMtx.RLock() 541 defer c.blockchainMtx.RUnlock() 542 c.mainchain = make(map[int64]*chainhash.Hash) 543 c.verboseBlocks = make(map[chainhash.Hash]*msgBlockWithHeight) 544 c.mempoolTxs = make(map[chainhash.Hash]*wire.MsgTx) 545 } 546 547 func makeRawTx(pkScripts []dex.Bytes, inputs []*wire.TxIn) *wire.MsgTx { 548 tx := &wire.MsgTx{ 549 TxIn: inputs, 550 } 551 for _, pkScript := range pkScripts { 552 tx.TxOut = append(tx.TxOut, wire.NewTxOut(1, pkScript)) 553 } 554 return tx 555 } 556 557 func makeTxHex(pkScripts []dex.Bytes, inputs []*wire.TxIn) ([]byte, error) { 558 msgTx := wire.NewMsgTx(wire.TxVersion) 559 for _, txIn := range inputs { 560 msgTx.AddTxIn(txIn) 561 } 562 for _, pkScript := range pkScripts { 563 txOut := wire.NewTxOut(100000000, pkScript) 564 msgTx.AddTxOut(txOut) 565 } 566 txBuf := bytes.NewBuffer(make([]byte, 0, dexbtc.MsgTxVBytes(msgTx))) 567 err := msgTx.Serialize(txBuf) 568 if err != nil { 569 return nil, err 570 } 571 return txBuf.Bytes(), nil 572 } 573 574 // msgTxFromHex creates a wire.MsgTx by deserializing the hex-encoded 575 // transaction. 576 func msgTxFromHex(txHex string) (*wire.MsgTx, error) { 577 return deserializeMsgTx(hex.NewDecoder(strings.NewReader(txHex))) 578 } 579 580 func makeRPCVin(txHash *chainhash.Hash, vout uint32, sigScript []byte, witness [][]byte) *wire.TxIn { 581 return wire.NewTxIn(wire.NewOutPoint(txHash, vout), sigScript, witness) 582 } 583 584 func dummyInput() *wire.TxIn { 585 return wire.NewTxIn(wire.NewOutPoint(&chainhash.Hash{0x01}, 0), nil, nil) 586 } 587 588 func dummyTx() *wire.MsgTx { 589 return makeRawTx([]dex.Bytes{randBytes(32)}, []*wire.TxIn{dummyInput()}) 590 } 591 592 func newTxOutResult(script []byte, value uint64, confs int64) *btcjson.GetTxOutResult { 593 return &btcjson.GetTxOutResult{ 594 Confirmations: confs, 595 Value: float64(value) / 1e8, 596 ScriptPubKey: btcjson.ScriptPubKeyResult{ 597 Hex: hex.EncodeToString(script), 598 }, 599 } 600 } 601 602 func makeSwapContract(segwit bool, lockTimeOffset time.Duration) (secret []byte, secretHash [32]byte, pkScript, contract []byte, addr, contractAddr btcutil.Address, lockTime time.Time) { 603 secret = randBytes(32) 604 secretHash = sha256.Sum256(secret) 605 606 addr = btcAddr(segwit) 607 608 lockTime = time.Now().Add(lockTimeOffset) 609 contract, err := dexbtc.MakeContract(addr, addr, secretHash[:], lockTime.Unix(), segwit, &chaincfg.MainNetParams) 610 if err != nil { 611 panic("error making swap contract:" + err.Error()) 612 } 613 contractAddr, _ = scriptHashAddress(segwit, contract, &chaincfg.MainNetParams) 614 pkScript, _ = txscript.PayToAddrScript(contractAddr) 615 return 616 } 617 618 func tNewWallet(segwit bool, walletType string) (*intermediaryWallet, *testData, func()) { 619 if segwit { 620 tSwapSize = dexbtc.InitTxSizeSegwit 621 tSwapSizeBase = dexbtc.InitTxSizeBaseSegwit 622 } else { 623 tSwapSize = dexbtc.InitTxSize 624 tSwapSizeBase = dexbtc.InitTxSizeBase 625 } 626 627 dataDir, err := os.MkdirTemp("", "") 628 if err != nil { 629 panic("couldn't create data dir:" + err.Error()) 630 } 631 632 data := newTestData() 633 walletCfg := &asset.WalletConfig{ 634 Emit: asset.NewWalletEmitter(data.tipChanged, BipID, tLogger), 635 PeersChange: func(num uint32, err error) { 636 fmt.Printf("peer count = %d, err = %v", num, err) 637 }, 638 DataDir: dataDir, 639 } 640 walletCtx, shutdown := context.WithCancel(tCtx) 641 cfg := &BTCCloneCFG{ 642 WalletCFG: walletCfg, 643 Symbol: "btc", 644 Logger: tLogger, 645 ChainParams: &chaincfg.MainNetParams, 646 WalletInfo: WalletInfo, 647 DefaultFallbackFee: defaultFee, 648 DefaultFeeRateLimit: defaultFeeRateLimit, 649 Segwit: segwit, 650 FeeEstimator: rpcFeeRate, 651 AddressDecoder: btcutil.DecodeAddress, 652 } 653 654 var wallet *intermediaryWallet 655 switch walletType { 656 case walletTypeRPC: 657 wallet, err = newRPCWallet(&tRawRequester{data}, cfg, &RPCWalletConfig{}) 658 case walletTypeSPV: 659 w, err := newUnconnectedWallet(cfg, &WalletConfig{}) 660 if err == nil { 661 neutrinoClient := &tNeutrinoClient{data} 662 spvw := &spvWallet{ 663 chainParams: &chaincfg.MainNetParams, 664 cfg: &WalletConfig{}, 665 wallet: &tBtcWallet{data}, 666 cl: neutrinoClient, 667 tipChan: make(chan *BlockVector, 1), 668 acctNum: 0, 669 log: cfg.Logger.SubLogger("SPV"), 670 decodeAddr: btcutil.DecodeAddress, 671 } 672 spvw.BlockFiltersScanner = NewBlockFiltersScanner(spvw, spvw.log) 673 spvw.txBlocks = data.dbBlockForTx 674 spvw.checkpoints = data.checkpoints 675 w.setNode(spvw) 676 wallet = &intermediaryWallet{ 677 baseWallet: w, 678 txFeeEstimator: spvw, 679 tipRedeemer: spvw, 680 } 681 wallet.prepareRedemptionFinder() 682 } 683 } 684 685 data.walletCfg = wallet.cfgV.Load().(*baseWalletConfig) 686 687 if err != nil { 688 shutdown() 689 os.RemoveAll(dataDir) 690 panic(err.Error()) 691 } 692 // Initialize the best block. 693 bestHash, err := wallet.node.GetBestBlockHash() 694 if err != nil { 695 shutdown() 696 os.RemoveAll(dataDir) 697 panic(err.Error()) 698 } 699 wallet.tipMtx.Lock() 700 wallet.currentTip = &BlockVector{ 701 Height: data.GetBestBlockHeight(), 702 Hash: *bestHash, 703 } 704 wallet.tipMtx.Unlock() 705 var wg sync.WaitGroup 706 wg.Add(1) 707 go func() { 708 defer wg.Done() 709 wallet.watchBlocks(walletCtx) 710 }() 711 shutdownAndWait := func() { 712 shutdown() 713 os.RemoveAll(dataDir) 714 wg.Wait() 715 } 716 return wallet, data, shutdownAndWait 717 } 718 719 func mustMarshal(thing any) []byte { 720 b, err := json.Marshal(thing) 721 if err != nil { 722 panic("mustMarshal error: " + err.Error()) 723 } 724 return b 725 } 726 727 func TestMain(m *testing.M) { 728 tLogger = dex.StdOutLogger("TEST", dex.LevelCritical) 729 var shutdown func() 730 tCtx, shutdown = context.WithCancel(context.Background()) 731 tTxHash, _ = chainhash.NewHashFromStr(tTxID) 732 tP2PKH, _ = hex.DecodeString("76a9148fc02268f208a61767504fe0b48d228641ba81e388ac") 733 tP2WPKH, _ = hex.DecodeString("0014148fc02268f208a61767504fe0b48d228641ba81") 734 // tP2SH, _ = hex.DecodeString("76a91412a9abf5c32392f38bd8a1f57d81b1aeecc5699588ac") 735 doIt := func() int { 736 // Not counted as coverage, must test Archiver constructor explicitly. 737 defer shutdown() 738 return m.Run() 739 } 740 os.Exit(doIt()) 741 } 742 743 type testFunc func(t *testing.T, segwit bool, walletType string) 744 745 func runRubric(t *testing.T, f testFunc) { 746 t.Run("rpc|segwit", func(t *testing.T) { 747 f(t, true, walletTypeRPC) 748 }) 749 t.Run("rpc|non-segwit", func(t *testing.T) { 750 f(t, false, walletTypeRPC) 751 }) 752 t.Run("spv|segwit", func(t *testing.T) { 753 f(t, true, walletTypeSPV) 754 }) 755 } 756 757 func TestFundMultiOrder(t *testing.T) { 758 runRubric(t, testFundMultiOrder) 759 } 760 761 func decodeString(s string) []byte { 762 b, _ := hex.DecodeString(s) 763 return b 764 } 765 766 func testFundMultiOrder(t *testing.T, segwit bool, walletType string) { 767 wallet, node, shutdown := tNewWallet(segwit, walletType) 768 defer shutdown() 769 770 maxFeeRate := uint64(200) 771 feeSuggestion := uint64(100) 772 773 txIDs := make([]string, 0, 5) 774 txHashes := make([]*chainhash.Hash, 0, 5) 775 776 addresses_legacy := []string{ 777 "n235HrCqx9EcS7teHcJEAthoBF5gvtrAoy", 778 "mfjtHyu163DW5ZJXRHkY6kMLtHyWHTH6Qx", 779 "mi5QmGr9KwVLM2WVNB9hwSS7KhUx8uNQqU", 780 "mhMda8yTy52Avowe34ufbq3D59VoG7jUsy", 781 "mhgZ41MUC6EBevkwnhvetkLbBmb61F7Lyr", 782 } 783 scriptPubKeys_legacy := []dex.Bytes{ 784 decodeString("76a914e114d5bb20cdbd75f3726f27c10423eb1332576288ac"), 785 decodeString("76a91402721143370117f4b37146f6688862892f272a7b88ac"), 786 decodeString("76a9141c13a8666a373c29a8a8a270b56816bb43a395e888ac"), 787 decodeString("76a914142ce1063712235182613794d6759f62dea8205a88ac"), 788 decodeString("76a91417c10273e4236592fd91f3aec1571810bbb0db6888ac"), 789 } 790 791 addresses_segwit := []string{ 792 "bcrt1qy7agjj62epx0ydnqskgwlcfwu52xjtpj36hr0d", 793 "bcrt1qhyflz52jwha67dvg92q92k887emxu6jj0ytrrd", 794 "bcrt1qus3827tpu7uhreq3f3x5wmfmpgmkd0y7zhjdrs", 795 "bcrt1q4fjzhnum2krkurhg55xtadzyf76f8waqj26d0e", 796 "bcrt1qqnl9fhnms6gpmlmrwnsq36h4rqxwd3m4plkcw4", 797 } 798 scriptPubKeys_segwit := []dex.Bytes{ 799 decodeString("001427ba894b4ac84cf236608590efe12ee514692c32"), 800 decodeString("0014b913f1515275fbaf35882a805558e7f6766e6a52"), 801 decodeString("0014e422757961e7b971e4114c4d476d3b0a3766bc9e"), 802 decodeString("0014aa642bcf9b55876e0ee8a50cbeb4444fb493bba0"), 803 decodeString("001404fe54de7b86901dff6374e008eaf5180ce6c775"), 804 } 805 for i := 0; i < 5; i++ { 806 txIDs = append(txIDs, hex.EncodeToString(encode.RandomBytes(32))) 807 h, _ := chainhash.NewHashFromStr(txIDs[i]) 808 txHashes = append(txHashes, h) 809 } 810 811 addresses := func(i int) string { 812 if segwit { 813 return addresses_segwit[i] 814 } 815 return addresses_legacy[i] 816 } 817 818 scriptPubKeys := func(i int) dex.Bytes { 819 if segwit { 820 return scriptPubKeys_segwit[i] 821 } 822 return scriptPubKeys_legacy[i] 823 } 824 825 addrStr := tP2PKHAddr 826 if segwit { 827 addrStr = tP2WPKHAddr 828 } 829 node.newAddress = addrStr 830 node.changeAddr = addrStr 831 node.signFunc = func(tx *wire.MsgTx) { 832 signFunc(tx, 0, wallet.segwit) 833 } 834 835 expectedSplitFee := func(numInputs, numOutputs uint64) uint64 { 836 var inputSize, outputSize uint64 837 if segwit { 838 inputSize = dexbtc.RedeemP2WPKHInputTotalSize 839 outputSize = dexbtc.P2WPKHOutputSize 840 } else { 841 inputSize = dexbtc.RedeemP2PKHInputSize 842 outputSize = dexbtc.P2PKHOutputSize 843 } 844 845 return (dexbtc.MinimumTxOverhead + numInputs*inputSize + numOutputs*outputSize) * feeSuggestion 846 } 847 848 requiredForOrder := func(value, maxSwapCount uint64) int64 { 849 var inputSize uint64 850 if segwit { 851 inputSize = dexbtc.RedeemP2WPKHInputTotalSize 852 } else { 853 inputSize = dexbtc.RedeemP2PKHInputSize 854 } 855 return int64(calc.RequiredOrderFunds(value, inputSize, maxSwapCount, 856 wallet.initTxSizeBase, wallet.initTxSize, maxFeeRate)) 857 } 858 859 type test struct { 860 name string 861 multiOrder *asset.MultiOrder 862 allOrNothing bool 863 maxLock uint64 864 utxos []*ListUnspentResult 865 bondReserves uint64 866 balance uint64 867 868 // if expectedCoins is nil, all the coins are from 869 // the split output. If any of the coins are nil, 870 // than that output is from the split output. 871 expectedCoins []asset.Coins 872 expectedRedeemScripts [][]dex.Bytes 873 expectSendRawTx bool 874 expectedSplitFee uint64 875 expectedInputs []*wire.TxIn 876 expectedOutputs []*wire.TxOut 877 expectedChange uint64 878 expectedLockedCoins []*RPCOutpoint 879 expectErr bool 880 } 881 882 tests := []*test{ 883 { // "split not allowed, utxos like split previously done" 884 name: "split not allowed, utxos like split previously done", 885 multiOrder: &asset.MultiOrder{ 886 Values: []*asset.MultiOrderValue{ 887 { 888 Value: 1e6, 889 MaxSwapCount: 1, 890 }, 891 { 892 Value: 2e6, 893 MaxSwapCount: 2, 894 }, 895 }, 896 MaxFeeRate: maxFeeRate, 897 FeeSuggestion: feeSuggestion, 898 Options: map[string]string{ 899 "swapsplit": "false", 900 }, 901 }, 902 utxos: []*ListUnspentResult{ 903 { 904 Confirmations: 1, 905 Spendable: true, 906 TxID: txIDs[0], 907 RedeemScript: nil, 908 ScriptPubKey: scriptPubKeys(0), 909 Address: addresses(0), 910 Amount: 19e5 / 1e8, 911 Vout: 0, 912 }, 913 { 914 Confirmations: 1, 915 Spendable: true, 916 TxID: txIDs[1], 917 RedeemScript: nil, 918 ScriptPubKey: scriptPubKeys(1), 919 Address: addresses(1), 920 Amount: 35e5 / 1e8, 921 Vout: 0, 922 }, 923 }, 924 balance: 35e5, 925 expectedCoins: []asset.Coins{ 926 {NewOutput(txHashes[0], 0, 19e5)}, 927 {NewOutput(txHashes[1], 0, 35e5)}, 928 }, 929 expectedRedeemScripts: [][]dex.Bytes{ 930 {nil}, 931 {nil}, 932 }, 933 }, 934 { // "split not allowed, require multiple utxos per order" 935 name: "split not allowed, require multiple utxos per order", 936 multiOrder: &asset.MultiOrder{ 937 Values: []*asset.MultiOrderValue{ 938 { 939 Value: 1e6, 940 MaxSwapCount: 1, 941 }, 942 { 943 Value: 2e6, 944 MaxSwapCount: 2, 945 }, 946 }, 947 MaxFeeRate: maxFeeRate, 948 FeeSuggestion: feeSuggestion, 949 Options: map[string]string{ 950 "swapsplit": "false", 951 }, 952 }, 953 allOrNothing: true, 954 utxos: []*ListUnspentResult{ 955 { 956 Confirmations: 1, 957 Spendable: true, 958 TxID: txIDs[0], 959 RedeemScript: nil, 960 ScriptPubKey: scriptPubKeys(0), 961 Address: addresses(0), 962 Amount: 6e5 / 1e8, 963 Vout: 0, 964 }, 965 { 966 Confirmations: 1, 967 Spendable: true, 968 TxID: txIDs[1], 969 RedeemScript: nil, 970 ScriptPubKey: scriptPubKeys(1), 971 Address: addresses(1), 972 Amount: 5e5 / 1e8, 973 Vout: 0, 974 }, 975 { 976 Confirmations: 1, 977 Spendable: true, 978 TxID: txIDs[2], 979 RedeemScript: nil, 980 ScriptPubKey: scriptPubKeys(2), 981 Address: addresses(2), 982 Amount: 22e5 / 1e8, 983 Vout: 0, 984 }, 985 }, 986 balance: 33e5, 987 expectedCoins: []asset.Coins{ 988 {NewOutput(txHashes[0], 0, 6e5), NewOutput(txHashes[1], 0, 5e5)}, 989 {NewOutput(txHashes[2], 0, 22e5)}, 990 }, 991 expectedRedeemScripts: [][]dex.Bytes{ 992 {nil, nil}, 993 {nil}, 994 }, 995 expectedLockedCoins: []*RPCOutpoint{ 996 {txHashes[0].String(), 0}, 997 {txHashes[1].String(), 0}, 998 {txHashes[2].String(), 0}, 999 }, 1000 }, 1001 { // "split not allowed, can only fund first order and respect maxLock" 1002 name: "split not allowed, can only fund first order and respect maxLock", 1003 multiOrder: &asset.MultiOrder{ 1004 Values: []*asset.MultiOrderValue{ 1005 { 1006 Value: 1e6, 1007 MaxSwapCount: 1, 1008 }, 1009 { 1010 Value: 2e6, 1011 MaxSwapCount: 2, 1012 }, 1013 }, 1014 MaxFeeRate: maxFeeRate, 1015 FeeSuggestion: feeSuggestion, 1016 Options: map[string]string{ 1017 "swapsplit": "false", 1018 }, 1019 }, 1020 maxLock: 32e5, 1021 utxos: []*ListUnspentResult{ 1022 { 1023 Confirmations: 1, 1024 Spendable: true, 1025 TxID: txIDs[2], 1026 RedeemScript: nil, 1027 ScriptPubKey: scriptPubKeys(2), 1028 Address: addresses(2), 1029 Amount: 1e6 / 1e8, 1030 Vout: 0, 1031 }, 1032 { 1033 Confirmations: 1, 1034 Spendable: true, 1035 TxID: txIDs[0], 1036 RedeemScript: nil, 1037 ScriptPubKey: scriptPubKeys(0), 1038 Address: addresses(0), 1039 Amount: 11e5 / 1e8, 1040 Vout: 0, 1041 }, 1042 { 1043 Confirmations: 1, 1044 Spendable: true, 1045 TxID: txIDs[1], 1046 RedeemScript: nil, 1047 ScriptPubKey: scriptPubKeys(1), 1048 Address: addresses(1), 1049 Amount: 25e5 / 1e8, 1050 Vout: 0, 1051 }, 1052 }, 1053 balance: 46e5, 1054 expectedCoins: []asset.Coins{ 1055 {NewOutput(txHashes[0], 0, 11e5)}, 1056 }, 1057 expectedRedeemScripts: [][]dex.Bytes{ 1058 {nil}, 1059 }, 1060 expectedLockedCoins: []*RPCOutpoint{ 1061 {txHashes[0].String(), 0}, 1062 }, 1063 }, 1064 { // "split not allowed, can only fund first order and respect bond reserves" 1065 name: "no split allowed, can only fund first order and respect bond reserves", 1066 multiOrder: &asset.MultiOrder{ 1067 Values: []*asset.MultiOrderValue{ 1068 { 1069 Value: 1e6, 1070 MaxSwapCount: 1, 1071 }, 1072 { 1073 Value: 2e6, 1074 MaxSwapCount: 2, 1075 }, 1076 }, 1077 MaxFeeRate: maxFeeRate, 1078 FeeSuggestion: feeSuggestion, 1079 Options: map[string]string{ 1080 multiSplitKey: "false", 1081 }, 1082 }, 1083 maxLock: 46e5, 1084 bondReserves: 12e5, 1085 utxos: []*ListUnspentResult{ 1086 { 1087 Confirmations: 1, 1088 Spendable: true, 1089 TxID: txIDs[2], 1090 RedeemScript: nil, 1091 ScriptPubKey: scriptPubKeys(2), 1092 Address: addresses(2), 1093 Amount: 1e6 / 1e8, 1094 Vout: 0, 1095 }, 1096 { 1097 Confirmations: 1, 1098 Spendable: true, 1099 TxID: txIDs[0], 1100 RedeemScript: nil, 1101 ScriptPubKey: scriptPubKeys(0), 1102 Address: addresses(0), 1103 Amount: 11e5 / 1e8, 1104 Vout: 0, 1105 }, 1106 { 1107 Confirmations: 1, 1108 Spendable: true, 1109 TxID: txIDs[1], 1110 RedeemScript: nil, 1111 ScriptPubKey: scriptPubKeys(1), 1112 Address: addresses(1), 1113 Amount: 25e5 / 1e8, 1114 Vout: 0, 1115 }, 1116 }, 1117 balance: 46e5, 1118 expectedCoins: []asset.Coins{ 1119 {NewOutput(txHashes[0], 0, 11e5)}, 1120 }, 1121 expectedRedeemScripts: [][]dex.Bytes{ 1122 {nil}, 1123 }, 1124 expectedLockedCoins: []*RPCOutpoint{ 1125 {txHashes[0].String(), 0}, 1126 }, 1127 }, 1128 { // "split not allowed, need to fund in increasing order" 1129 name: "no split, need to fund in increasing order", 1130 multiOrder: &asset.MultiOrder{ 1131 Values: []*asset.MultiOrderValue{ 1132 { 1133 Value: 2e6, 1134 MaxSwapCount: 2, 1135 }, 1136 { 1137 Value: 11e5, 1138 MaxSwapCount: 1, 1139 }, 1140 { 1141 Value: 9e5, 1142 MaxSwapCount: 1, 1143 }, 1144 }, 1145 MaxFeeRate: maxFeeRate, 1146 FeeSuggestion: feeSuggestion, 1147 Options: map[string]string{ 1148 multiSplitKey: "false", 1149 }, 1150 }, 1151 maxLock: 50e5, 1152 utxos: []*ListUnspentResult{ 1153 { 1154 Confirmations: 1, 1155 Spendable: true, 1156 TxID: txIDs[0], 1157 RedeemScript: nil, 1158 ScriptPubKey: scriptPubKeys(0), 1159 Address: addresses(0), 1160 Amount: 11e5 / 1e8, 1161 Vout: 0, 1162 }, 1163 { 1164 Confirmations: 1, 1165 Spendable: true, 1166 TxID: txIDs[1], 1167 RedeemScript: nil, 1168 ScriptPubKey: scriptPubKeys(1), 1169 Address: addresses(1), 1170 Amount: 13e5 / 1e8, 1171 Vout: 0, 1172 }, 1173 { 1174 Confirmations: 1, 1175 Spendable: true, 1176 TxID: txIDs[2], 1177 RedeemScript: nil, 1178 ScriptPubKey: scriptPubKeys(2), 1179 Address: addresses(2), 1180 Amount: 26e5 / 1e8, 1181 Vout: 0, 1182 }, 1183 }, 1184 balance: 50e5, 1185 expectedCoins: []asset.Coins{ 1186 {NewOutput(txHashes[2], 0, 26e5)}, 1187 {NewOutput(txHashes[1], 0, 13e5)}, 1188 {NewOutput(txHashes[0], 0, 11e5)}, 1189 }, 1190 expectedRedeemScripts: [][]dex.Bytes{ 1191 {nil}, 1192 {nil}, 1193 {nil}, 1194 }, 1195 expectedLockedCoins: []*RPCOutpoint{ 1196 {txHashes[0].String(), 0}, 1197 {txHashes[1].String(), 0}, 1198 {txHashes[2].String(), 0}, 1199 }, 1200 }, 1201 { // "split allowed, no split required" 1202 name: "split allowed, no split required", 1203 multiOrder: &asset.MultiOrder{ 1204 Values: []*asset.MultiOrderValue{ 1205 { 1206 Value: 1e6, 1207 MaxSwapCount: 1, 1208 }, 1209 { 1210 Value: 2e6, 1211 MaxSwapCount: 2, 1212 }, 1213 }, 1214 MaxFeeRate: maxFeeRate, 1215 FeeSuggestion: feeSuggestion, 1216 Options: map[string]string{ 1217 multiSplitKey: "true", 1218 }, 1219 }, 1220 allOrNothing: false, 1221 maxLock: 43e5, 1222 utxos: []*ListUnspentResult{ 1223 { 1224 Confirmations: 1, 1225 Spendable: true, 1226 TxID: txIDs[2], 1227 RedeemScript: nil, 1228 ScriptPubKey: scriptPubKeys(2), 1229 Address: addresses(2), 1230 Amount: 1e6 / 1e8, 1231 Vout: 0, 1232 }, 1233 { 1234 Confirmations: 1, 1235 Spendable: true, 1236 TxID: txIDs[0], 1237 RedeemScript: nil, 1238 ScriptPubKey: scriptPubKeys(0), 1239 Address: addresses(0), 1240 Amount: 11e5 / 1e8, 1241 Vout: 0, 1242 }, 1243 { 1244 Confirmations: 1, 1245 Spendable: true, 1246 TxID: txIDs[1], 1247 RedeemScript: nil, 1248 ScriptPubKey: scriptPubKeys(1), 1249 Address: addresses(1), 1250 Amount: 22e5 / 1e8, 1251 Vout: 0, 1252 }, 1253 }, 1254 balance: 43e5, 1255 expectedCoins: []asset.Coins{ 1256 {NewOutput(txHashes[0], 0, 11e5)}, 1257 {NewOutput(txHashes[1], 0, 22e5)}, 1258 }, 1259 expectedRedeemScripts: [][]dex.Bytes{ 1260 {nil}, 1261 {nil}, 1262 }, 1263 expectedLockedCoins: []*RPCOutpoint{ 1264 {txHashes[0].String(), 0}, 1265 {txHashes[1].String(), 0}, 1266 }, 1267 }, 1268 { // "split allowed, can fund both with split" 1269 name: "split allowed, can fund both with split", 1270 multiOrder: &asset.MultiOrder{ 1271 Values: []*asset.MultiOrderValue{ 1272 { 1273 Value: 15e5, 1274 MaxSwapCount: 2, 1275 }, 1276 { 1277 Value: 15e5, 1278 MaxSwapCount: 2, 1279 }, 1280 }, 1281 MaxFeeRate: maxFeeRate, 1282 FeeSuggestion: feeSuggestion, 1283 Options: map[string]string{ 1284 multiSplitKey: "true", 1285 }, 1286 }, 1287 utxos: []*ListUnspentResult{ 1288 { 1289 Confirmations: 1, 1290 Spendable: true, 1291 TxID: txIDs[0], 1292 RedeemScript: nil, 1293 ScriptPubKey: scriptPubKeys(0), 1294 Address: addresses(0), 1295 Amount: 1e6 / 1e8, 1296 Vout: 0, 1297 }, 1298 { 1299 Confirmations: 1, 1300 Spendable: true, 1301 TxID: txIDs[1], 1302 RedeemScript: nil, 1303 ScriptPubKey: scriptPubKeys(1), 1304 Address: addresses(1), 1305 Amount: (2*float64(requiredForOrder(15e5, 2)) + float64(expectedSplitFee(2, 2)) - 1e6) / 1e8, 1306 Vout: 0, 1307 }, 1308 }, 1309 maxLock: 2*uint64(requiredForOrder(15e5, 2)) + expectedSplitFee(2, 2), 1310 balance: 2*uint64(requiredForOrder(15e5, 2)) + expectedSplitFee(2, 2), 1311 expectSendRawTx: true, 1312 expectedInputs: []*wire.TxIn{ 1313 { 1314 PreviousOutPoint: wire.OutPoint{ 1315 Hash: *txHashes[1], 1316 Index: 0, 1317 }, 1318 }, 1319 { 1320 PreviousOutPoint: wire.OutPoint{ 1321 Hash: *txHashes[0], 1322 Index: 0, 1323 }, 1324 }, 1325 }, 1326 expectedOutputs: []*wire.TxOut{ 1327 wire.NewTxOut(requiredForOrder(15e5, 2), []byte{}), 1328 wire.NewTxOut(requiredForOrder(15e5, 2), []byte{}), 1329 }, 1330 expectedSplitFee: expectedSplitFee(2, 2), 1331 expectedRedeemScripts: [][]dex.Bytes{ 1332 {nil}, 1333 {nil}, 1334 }, 1335 }, 1336 { // "split allowed, cannot fund both with split" 1337 name: "split allowed, cannot fund both with split", 1338 multiOrder: &asset.MultiOrder{ 1339 Values: []*asset.MultiOrderValue{ 1340 { 1341 Value: 15e5, 1342 MaxSwapCount: 2, 1343 }, 1344 { 1345 Value: 15e5, 1346 MaxSwapCount: 2, 1347 }, 1348 }, 1349 MaxFeeRate: maxFeeRate, 1350 FeeSuggestion: feeSuggestion, 1351 Options: map[string]string{ 1352 multiSplitKey: "true", 1353 }, 1354 }, 1355 utxos: []*ListUnspentResult{ 1356 { 1357 Confirmations: 1, 1358 Spendable: true, 1359 TxID: txIDs[0], 1360 RedeemScript: nil, 1361 ScriptPubKey: scriptPubKeys(0), 1362 Address: addresses(0), 1363 Amount: 1e6 / 1e8, 1364 Vout: 0, 1365 }, 1366 { 1367 Confirmations: 1, 1368 Spendable: true, 1369 TxID: txIDs[1], 1370 RedeemScript: nil, 1371 ScriptPubKey: scriptPubKeys(1), 1372 Address: addresses(1), 1373 Amount: (2*float64(requiredForOrder(15e5, 2)) + float64(expectedSplitFee(2, 2)) - 1e6) / 1e8, 1374 Vout: 0, 1375 }, 1376 }, 1377 maxLock: 2*uint64(requiredForOrder(15e5, 2)) + expectedSplitFee(2, 2) - 1, 1378 balance: 2*uint64(requiredForOrder(15e5, 2)) + expectedSplitFee(2, 2) - 1, 1379 expectErr: true, 1380 }, 1381 { // "can fund both with split and respect maxLock" 1382 name: "can fund both with split and respect maxLock", 1383 multiOrder: &asset.MultiOrder{ 1384 Values: []*asset.MultiOrderValue{ 1385 { 1386 Value: 15e5, 1387 MaxSwapCount: 2, 1388 }, 1389 { 1390 Value: 15e5, 1391 MaxSwapCount: 2, 1392 }, 1393 }, 1394 MaxFeeRate: maxFeeRate, 1395 FeeSuggestion: feeSuggestion, 1396 Options: map[string]string{ 1397 multiSplitKey: "true", 1398 }, 1399 }, 1400 utxos: []*ListUnspentResult{ 1401 { 1402 Confirmations: 1, 1403 Spendable: true, 1404 TxID: txIDs[0], 1405 RedeemScript: nil, 1406 ScriptPubKey: scriptPubKeys(0), 1407 Address: addresses(0), 1408 Amount: float64(50e5) / 1e8, 1409 Vout: 0, 1410 }, 1411 }, 1412 balance: 50e5, 1413 maxLock: 2*uint64(requiredForOrder(15e5, 2)) + expectedSplitFee(1, 2), 1414 expectSendRawTx: true, 1415 expectedInputs: []*wire.TxIn{ 1416 { 1417 PreviousOutPoint: wire.OutPoint{ 1418 Hash: *txHashes[0], 1419 Index: 0, 1420 }, 1421 }, 1422 }, 1423 expectedOutputs: []*wire.TxOut{ 1424 wire.NewTxOut(requiredForOrder(15e5, 2), []byte{}), 1425 wire.NewTxOut(requiredForOrder(15e5, 2), []byte{}), 1426 }, 1427 expectedChange: 50e5 - (2*uint64(requiredForOrder(15e5, 2)) + expectedSplitFee(1, 3)), 1428 expectedSplitFee: expectedSplitFee(1, 3), 1429 expectedRedeemScripts: [][]dex.Bytes{ 1430 {nil}, 1431 {nil}, 1432 }, 1433 }, 1434 { // "cannot fund both with split and respect maxLock" 1435 name: "cannot fund both with split and respect maxLock", 1436 multiOrder: &asset.MultiOrder{ 1437 Values: []*asset.MultiOrderValue{ 1438 { 1439 Value: 15e5, 1440 MaxSwapCount: 2, 1441 }, 1442 { 1443 Value: 15e5, 1444 MaxSwapCount: 2, 1445 }, 1446 }, 1447 MaxFeeRate: maxFeeRate, 1448 FeeSuggestion: feeSuggestion, 1449 Options: map[string]string{ 1450 multiSplitKey: "true", 1451 }, 1452 }, 1453 utxos: []*ListUnspentResult{ 1454 { 1455 Confirmations: 1, 1456 Spendable: true, 1457 TxID: txIDs[0], 1458 RedeemScript: nil, 1459 ScriptPubKey: scriptPubKeys(0), 1460 Address: addresses(0), 1461 Amount: float64(50e5) / 1e8, 1462 Vout: 0, 1463 }, 1464 }, 1465 balance: 50e5, 1466 maxLock: 2*uint64(requiredForOrder(15e5, 2)) + expectedSplitFee(1, 2) - 1, 1467 expectErr: true, 1468 }, 1469 { // "split allowed, can fund both with split with bond reserves" 1470 name: "split allowed, can fund both with split with bond reserves", 1471 multiOrder: &asset.MultiOrder{ 1472 Values: []*asset.MultiOrderValue{ 1473 { 1474 Value: 15e5, 1475 MaxSwapCount: 2, 1476 }, 1477 { 1478 Value: 15e5, 1479 MaxSwapCount: 2, 1480 }, 1481 }, 1482 MaxFeeRate: maxFeeRate, 1483 FeeSuggestion: feeSuggestion, 1484 Options: map[string]string{ 1485 multiSplitKey: "true", 1486 }, 1487 }, 1488 bondReserves: 2e6, 1489 utxos: []*ListUnspentResult{ 1490 { 1491 Confirmations: 1, 1492 Spendable: true, 1493 TxID: txIDs[0], 1494 RedeemScript: nil, 1495 ScriptPubKey: scriptPubKeys(0), 1496 Address: addresses(0), 1497 Amount: (2*float64(requiredForOrder(15e5, 2)) + 2e6 + float64(expectedSplitFee(1, 3))) / 1e8, 1498 Vout: 0, 1499 }, 1500 }, 1501 balance: 2e6 + 2*uint64(requiredForOrder(15e5, 2)) + expectedSplitFee(1, 3), 1502 maxLock: 2e6 + 2*uint64(requiredForOrder(15e5, 2)) + expectedSplitFee(1, 3), 1503 expectSendRawTx: true, 1504 expectedInputs: []*wire.TxIn{ 1505 { 1506 PreviousOutPoint: wire.OutPoint{ 1507 Hash: *txHashes[0], 1508 Index: 0, 1509 }, 1510 }, 1511 }, 1512 expectedOutputs: []*wire.TxOut{ 1513 wire.NewTxOut(requiredForOrder(15e5, 2), []byte{}), 1514 wire.NewTxOut(requiredForOrder(15e5, 2), []byte{}), 1515 }, 1516 expectedChange: 2e6, 1517 expectedSplitFee: expectedSplitFee(1, 3), 1518 expectedRedeemScripts: [][]dex.Bytes{ 1519 {nil}, 1520 {nil}, 1521 }, 1522 }, 1523 { // "split allowed, cannot fund both with split and keep and bond reserves" 1524 name: "split allowed, cannot fund both with split and keep and bond reserves", 1525 multiOrder: &asset.MultiOrder{ 1526 Values: []*asset.MultiOrderValue{ 1527 { 1528 Value: 15e5, 1529 MaxSwapCount: 2, 1530 }, 1531 { 1532 Value: 15e5, 1533 MaxSwapCount: 2, 1534 }, 1535 }, 1536 MaxFeeRate: maxFeeRate, 1537 FeeSuggestion: feeSuggestion, 1538 Options: map[string]string{ 1539 multiSplitKey: "true", 1540 }, 1541 }, 1542 bondReserves: 2e6, 1543 utxos: []*ListUnspentResult{ 1544 { 1545 Confirmations: 1, 1546 Spendable: true, 1547 TxID: txIDs[0], 1548 RedeemScript: nil, 1549 ScriptPubKey: scriptPubKeys(0), 1550 Address: addresses(0), 1551 Amount: ((2*float64(requiredForOrder(15e5, 2)) + 2e6 + float64(expectedSplitFee(1, 3))) / 1e8) - 1/1e8, 1552 Vout: 0, 1553 }, 1554 }, 1555 balance: 2e6 + 2*uint64(requiredForOrder(15e5, 2)) + expectedSplitFee(1, 3) - 1, 1556 maxLock: 2e6 + 2*uint64(requiredForOrder(15e5, 2)) + expectedSplitFee(1, 3) - 1, 1557 expectErr: true, 1558 }, 1559 { // "split with buffer" 1560 name: "split with buffer", 1561 multiOrder: &asset.MultiOrder{ 1562 Values: []*asset.MultiOrderValue{ 1563 { 1564 Value: 15e5, 1565 MaxSwapCount: 2, 1566 }, 1567 { 1568 Value: 15e5, 1569 MaxSwapCount: 2, 1570 }, 1571 }, 1572 MaxFeeRate: maxFeeRate, 1573 FeeSuggestion: feeSuggestion, 1574 Options: map[string]string{ 1575 multiSplitKey: "true", 1576 multiSplitBufferKey: "10", 1577 }, 1578 }, 1579 utxos: []*ListUnspentResult{ 1580 { 1581 Confirmations: 1, 1582 Spendable: true, 1583 TxID: txIDs[0], 1584 RedeemScript: nil, 1585 ScriptPubKey: scriptPubKeys(0), 1586 Address: addresses(0), 1587 Amount: (2*float64(requiredForOrder(15e5, 2)*110/100) + float64(expectedSplitFee(1, 2))) / 1e8, 1588 Vout: 0, 1589 }, 1590 }, 1591 balance: 2*uint64(requiredForOrder(15e5, 2)*110/100) + expectedSplitFee(1, 2), 1592 maxLock: 2*uint64(requiredForOrder(15e5, 2)*110/100) + expectedSplitFee(1, 2), 1593 expectSendRawTx: true, 1594 expectedInputs: []*wire.TxIn{ 1595 { 1596 PreviousOutPoint: wire.OutPoint{ 1597 Hash: *txHashes[0], 1598 Index: 0, 1599 }, 1600 }, 1601 }, 1602 expectedOutputs: []*wire.TxOut{ 1603 wire.NewTxOut(requiredForOrder(15e5, 2)*110/100, []byte{}), 1604 wire.NewTxOut(requiredForOrder(15e5, 2)*110/100, []byte{}), 1605 }, 1606 expectedSplitFee: expectedSplitFee(1, 2), 1607 expectedRedeemScripts: [][]dex.Bytes{ 1608 {nil}, 1609 {nil}, 1610 }, 1611 }, 1612 { // "split, maxLock too low to fund buffer" 1613 name: "split, maxLock too low to fund buffer", 1614 multiOrder: &asset.MultiOrder{ 1615 Values: []*asset.MultiOrderValue{ 1616 { 1617 Value: 15e5, 1618 MaxSwapCount: 2, 1619 }, 1620 { 1621 Value: 15e5, 1622 MaxSwapCount: 2, 1623 }, 1624 }, 1625 MaxFeeRate: maxFeeRate, 1626 FeeSuggestion: feeSuggestion, 1627 Options: map[string]string{ 1628 multiSplitKey: "true", 1629 multiSplitBufferKey: "10", 1630 }, 1631 }, 1632 utxos: []*ListUnspentResult{ 1633 { 1634 Confirmations: 1, 1635 Spendable: true, 1636 TxID: txIDs[0], 1637 RedeemScript: nil, 1638 ScriptPubKey: scriptPubKeys(0), 1639 Address: addresses(0), 1640 Amount: (2*float64(requiredForOrder(15e5, 2)*110/100) + float64(expectedSplitFee(1, 2))) / 1e8, 1641 Vout: 0, 1642 }, 1643 }, 1644 balance: 2*uint64(requiredForOrder(15e5, 2)*110/100) + expectedSplitFee(1, 2), 1645 maxLock: 2*uint64(requiredForOrder(15e5, 2)*110/100) + expectedSplitFee(1, 2) - 1, 1646 expectErr: true, 1647 }, 1648 { // "only one order needs a split, rest can be funded without" 1649 name: "only one order needs a split, rest can be funded without", 1650 multiOrder: &asset.MultiOrder{ 1651 Values: []*asset.MultiOrderValue{ 1652 { 1653 Value: 1e6, 1654 MaxSwapCount: 2, 1655 }, 1656 { 1657 Value: 1e6, 1658 MaxSwapCount: 2, 1659 }, 1660 { 1661 Value: 1e6, 1662 MaxSwapCount: 2, 1663 }, 1664 }, 1665 MaxFeeRate: maxFeeRate, 1666 FeeSuggestion: feeSuggestion, 1667 Options: map[string]string{ 1668 multiSplitKey: "true", 1669 }, 1670 }, 1671 utxos: []*ListUnspentResult{ 1672 { 1673 Confirmations: 1, 1674 Spendable: true, 1675 TxID: txIDs[0], 1676 RedeemScript: nil, 1677 ScriptPubKey: scriptPubKeys(0), 1678 Address: addresses(0), 1679 Amount: 12e5 / 1e8, 1680 Vout: 0, 1681 }, 1682 { 1683 Confirmations: 1, 1684 Spendable: true, 1685 TxID: txIDs[1], 1686 RedeemScript: nil, 1687 ScriptPubKey: scriptPubKeys(1), 1688 Address: addresses(1), 1689 Amount: 12e5 / 1e8, 1690 Vout: 0, 1691 }, 1692 { 1693 Confirmations: 1, 1694 Spendable: true, 1695 TxID: txIDs[2], 1696 RedeemScript: nil, 1697 ScriptPubKey: scriptPubKeys(2), 1698 Address: addresses(2), 1699 Amount: 120e5 / 1e8, 1700 Vout: 0, 1701 }, 1702 }, 1703 maxLock: 50e5, 1704 balance: 144e5, 1705 expectSendRawTx: true, 1706 expectedInputs: []*wire.TxIn{ 1707 { 1708 PreviousOutPoint: wire.OutPoint{ 1709 Hash: *txHashes[2], 1710 Index: 0, 1711 }, 1712 }, 1713 }, 1714 expectedOutputs: []*wire.TxOut{ 1715 wire.NewTxOut(requiredForOrder(1e6, 2), []byte{}), 1716 wire.NewTxOut(120e5-requiredForOrder(1e6, 2)-int64(expectedSplitFee(1, 2)), []byte{}), 1717 }, 1718 expectedSplitFee: expectedSplitFee(1, 2), 1719 expectedRedeemScripts: [][]dex.Bytes{ 1720 {nil}, 1721 {nil}, 1722 {nil}, 1723 }, 1724 expectedCoins: []asset.Coins{ 1725 {NewOutput(txHashes[0], 0, 12e5)}, 1726 {NewOutput(txHashes[1], 0, 12e5)}, 1727 nil, 1728 }, 1729 }, 1730 { // "only one order needs a split due to bond reserves, rest funded without" 1731 name: "only one order needs a split, rest can be funded without", 1732 multiOrder: &asset.MultiOrder{ 1733 Values: []*asset.MultiOrderValue{ 1734 { 1735 Value: 1e6, 1736 MaxSwapCount: 2, 1737 }, 1738 { 1739 Value: 1e6, 1740 MaxSwapCount: 2, 1741 }, 1742 { 1743 Value: 1e6, 1744 MaxSwapCount: 2, 1745 }, 1746 }, 1747 MaxFeeRate: maxFeeRate, 1748 FeeSuggestion: feeSuggestion, 1749 Options: map[string]string{ 1750 multiSplitKey: "true", 1751 }, 1752 }, 1753 utxos: []*ListUnspentResult{ 1754 { 1755 Confirmations: 1, 1756 Spendable: true, 1757 TxID: txIDs[0], 1758 RedeemScript: nil, 1759 ScriptPubKey: scriptPubKeys(0), 1760 Address: addresses(0), 1761 Amount: 12e5 / 1e8, 1762 Vout: 0, 1763 }, 1764 { 1765 Confirmations: 1, 1766 Spendable: true, 1767 TxID: txIDs[1], 1768 RedeemScript: nil, 1769 ScriptPubKey: scriptPubKeys(1), 1770 Address: addresses(1), 1771 Amount: 12e5 / 1e8, 1772 Vout: 0, 1773 }, 1774 { 1775 Confirmations: 1, 1776 Spendable: true, 1777 TxID: txIDs[2], 1778 RedeemScript: nil, 1779 ScriptPubKey: scriptPubKeys(2), 1780 Address: addresses(2), 1781 Amount: 120e5 / 1e8, 1782 Vout: 0, 1783 }, 1784 }, 1785 maxLock: 0, 1786 bondReserves: 1e6, 1787 balance: 144e5, 1788 expectSendRawTx: true, 1789 expectedInputs: []*wire.TxIn{ 1790 { 1791 PreviousOutPoint: wire.OutPoint{ 1792 Hash: *txHashes[2], 1793 Index: 0, 1794 }, 1795 }, 1796 }, 1797 expectedOutputs: []*wire.TxOut{ 1798 wire.NewTxOut(requiredForOrder(1e6, 2), []byte{}), 1799 wire.NewTxOut(120e5-requiredForOrder(1e6, 2)-int64(expectedSplitFee(1, 2)), []byte{}), 1800 }, 1801 expectedSplitFee: expectedSplitFee(1, 2), 1802 expectedRedeemScripts: [][]dex.Bytes{ 1803 {nil}, 1804 {nil}, 1805 {nil}, 1806 }, 1807 expectedCoins: []asset.Coins{ 1808 {NewOutput(txHashes[0], 0, 12e5)}, 1809 {NewOutput(txHashes[1], 0, 12e5)}, 1810 nil, 1811 }, 1812 }, 1813 } 1814 1815 for _, test := range tests { 1816 node.listUnspent = test.utxos 1817 node.sentRawTx = nil 1818 node.lockedCoins = nil 1819 node.getBalances = &GetBalancesResult{ 1820 Mine: Balances{ 1821 Trusted: toBTC(test.balance), 1822 }, 1823 } 1824 wallet.cm.lockedOutputs = make(map[OutPoint]*UTxO) 1825 wallet.bondReserves.Store(test.bondReserves) 1826 1827 allCoins, _, splitFee, err := wallet.FundMultiOrder(test.multiOrder, test.maxLock) 1828 if test.expectErr { 1829 if err == nil { 1830 t.Fatalf("%s: no error returned", test.name) 1831 } 1832 if strings.Contains(err.Error(), "insufficient funds") { 1833 t.Fatalf("%s: unexpected insufficient funds error", test.name) 1834 } 1835 continue 1836 } 1837 if err != nil { 1838 t.Fatalf("%s: unexpected error: %v", test.name, err) 1839 } 1840 1841 if !test.expectSendRawTx { // no split 1842 if node.sentRawTx != nil { 1843 t.Fatalf("%s: unexpected transaction sent", test.name) 1844 } 1845 if len(allCoins) != len(test.expectedCoins) { 1846 t.Fatalf("%s: expected %d coins, got %d", test.name, len(test.expectedCoins), len(allCoins)) 1847 } 1848 for i := range allCoins { 1849 if len(allCoins[i]) != len(test.expectedCoins[i]) { 1850 t.Fatalf("%s: expected %d coins in set %d, got %d", test.name, len(test.expectedCoins[i]), i, len(allCoins[i])) 1851 } 1852 actual := allCoins[i] 1853 expected := test.expectedCoins[i] 1854 sort.Slice(actual, func(i, j int) bool { 1855 return bytes.Compare(actual[i].ID(), actual[j].ID()) < 0 1856 }) 1857 sort.Slice(expected, func(i, j int) bool { 1858 return bytes.Compare(expected[i].ID(), expected[j].ID()) < 0 1859 }) 1860 for j := range actual { 1861 if !bytes.Equal(actual[j].ID(), expected[j].ID()) { 1862 t.Fatalf("%s: unexpected coin in set %d. expected %s, got %s", test.name, i, expected[j].ID(), actual[j].ID()) 1863 } 1864 if actual[j].Value() != expected[j].Value() { 1865 t.Fatalf("%s: unexpected coin value in set %d. expected %d, got %d", test.name, i, expected[j].Value(), actual[j].Value()) 1866 } 1867 } 1868 } 1869 } else { // expectSplit 1870 if node.sentRawTx == nil { 1871 t.Fatalf("%s: SendRawTransaction not called", test.name) 1872 } 1873 if len(node.sentRawTx.TxIn) != len(test.expectedInputs) { 1874 t.Fatalf("%s: expected %d inputs, got %d", test.name, len(test.expectedInputs), len(node.sentRawTx.TxIn)) 1875 } 1876 for i, actualIn := range node.sentRawTx.TxIn { 1877 expectedIn := test.expectedInputs[i] 1878 if !bytes.Equal(actualIn.PreviousOutPoint.Hash[:], expectedIn.PreviousOutPoint.Hash[:]) { 1879 t.Fatalf("%s: unexpected input %d hash. expected %s, got %s", test.name, i, expectedIn.PreviousOutPoint.Hash, actualIn.PreviousOutPoint.Hash) 1880 } 1881 if actualIn.PreviousOutPoint.Index != expectedIn.PreviousOutPoint.Index { 1882 t.Fatalf("%s: unexpected input %d index. expected %d, got %d", test.name, i, expectedIn.PreviousOutPoint.Index, actualIn.PreviousOutPoint.Index) 1883 } 1884 } 1885 expectedNumOutputs := len(test.expectedOutputs) 1886 if test.expectedChange > 0 { 1887 expectedNumOutputs++ 1888 } 1889 if len(node.sentRawTx.TxOut) != expectedNumOutputs { 1890 t.Fatalf("%s: expected %d outputs, got %d", test.name, expectedNumOutputs, len(node.sentRawTx.TxOut)) 1891 } 1892 1893 for i, expectedOut := range test.expectedOutputs { 1894 actualOut := node.sentRawTx.TxOut[i] 1895 if actualOut.Value != expectedOut.Value { 1896 t.Fatalf("%s: unexpected output %d value. expected %d, got %d", test.name, i, expectedOut.Value, actualOut.Value) 1897 } 1898 } 1899 if test.expectedChange > 0 { 1900 actualOut := node.sentRawTx.TxOut[len(node.sentRawTx.TxOut)-1] 1901 if uint64(actualOut.Value) != test.expectedChange { 1902 t.Fatalf("%s: unexpected change value. expected %d, got %d", test.name, test.expectedChange, actualOut.Value) 1903 } 1904 } 1905 1906 if len(test.multiOrder.Values) != len(allCoins) { 1907 t.Fatalf("%s: expected %d coins, got %d", test.name, len(test.multiOrder.Values), len(allCoins)) 1908 } 1909 splitTxID := node.sentRawTx.TxHash() 1910 1911 // This means all coins are split outputs 1912 if test.expectedCoins == nil { 1913 for i, actualCoin := range allCoins { 1914 actualOut := actualCoin[0].(*Output) 1915 expectedOut := node.sentRawTx.TxOut[i] 1916 if uint64(expectedOut.Value) != actualOut.Val { 1917 t.Fatalf("%s: unexpected output %d value. expected %d, got %d", test.name, i, expectedOut.Value, actualOut.Val) 1918 } 1919 if !bytes.Equal(actualOut.Pt.TxHash[:], splitTxID[:]) { 1920 t.Fatalf("%s: unexpected output %d txid. expected %s, got %s", test.name, i, splitTxID, actualOut.Pt.TxHash) 1921 } 1922 } 1923 } else { 1924 var splitTxOutputIndex int 1925 for i := range allCoins { 1926 actual := allCoins[i] 1927 expected := test.expectedCoins[i] 1928 1929 // This means the coins are the split outputs 1930 if expected == nil { 1931 actualOut := actual[0].(*Output) 1932 expectedOut := node.sentRawTx.TxOut[splitTxOutputIndex] 1933 if uint64(expectedOut.Value) != actualOut.Val { 1934 t.Fatalf("%s: unexpected output %d value. expected %d, got %d", test.name, i, expectedOut.Value, actualOut.Val) 1935 } 1936 if !bytes.Equal(actualOut.Pt.TxHash[:], splitTxID[:]) { 1937 t.Fatalf("%s: unexpected output %d txid. expected %s, got %s", test.name, i, splitTxID, actualOut.Pt.TxHash) 1938 } 1939 splitTxOutputIndex++ 1940 continue 1941 } 1942 1943 if len(actual) != len(expected) { 1944 t.Fatalf("%s: expected %d coins in set %d, got %d", test.name, len(test.expectedCoins[i]), i, len(allCoins[i])) 1945 } 1946 sort.Slice(actual, func(i, j int) bool { 1947 return bytes.Compare(actual[i].ID(), actual[j].ID()) < 0 1948 }) 1949 sort.Slice(expected, func(i, j int) bool { 1950 return bytes.Compare(expected[i].ID(), expected[j].ID()) < 0 1951 }) 1952 for j := range actual { 1953 if !bytes.Equal(actual[j].ID(), expected[j].ID()) { 1954 t.Fatalf("%s: unexpected coin in set %d. expected %s, got %s", test.name, i, expected[j].ID(), actual[j].ID()) 1955 } 1956 if actual[j].Value() != expected[j].Value() { 1957 t.Fatalf("%s: unexpected coin value in set %d. expected %d, got %d", test.name, i, expected[j].Value(), actual[j].Value()) 1958 } 1959 } 1960 } 1961 } 1962 1963 // Each split output should be locked 1964 if len(node.lockedCoins) != len(allCoins)+len(test.expectedInputs) { 1965 t.Fatalf("%s: expected %d locked coins, got %d", test.name, len(allCoins)+len(test.expectedInputs), len(node.lockedCoins)) 1966 } 1967 1968 } 1969 1970 // Check that the right coins are locked and in the fundingCoins map 1971 var totalNumCoins int 1972 for _, coins := range allCoins { 1973 totalNumCoins += len(coins) 1974 } 1975 if totalNumCoins != len(wallet.cm.lockedOutputs) { 1976 t.Fatalf("%s: expected %d funding coins in wallet, got %d", test.name, totalNumCoins, len(wallet.cm.lockedOutputs)) 1977 } 1978 totalNumCoins += len(test.expectedInputs) 1979 if totalNumCoins != len(node.lockedCoins) { 1980 t.Fatalf("%s: expected %d locked coins, got %d", test.name, totalNumCoins, len(node.lockedCoins)) 1981 } 1982 lockedCoins := make(map[RPCOutpoint]any) 1983 for _, coin := range node.lockedCoins { 1984 lockedCoins[*coin] = true 1985 } 1986 checkLockedCoin := func(txHash chainhash.Hash, vout uint32) { 1987 if _, ok := lockedCoins[RPCOutpoint{TxID: txHash.String(), Vout: vout}]; !ok { 1988 t.Fatalf("%s: expected locked coin %s:%d not found", test.name, txHash, vout) 1989 } 1990 } 1991 checkFundingCoin := func(txHash chainhash.Hash, vout uint32) { 1992 if _, ok := wallet.cm.lockedOutputs[OutPoint{TxHash: txHash, Vout: vout}]; !ok { 1993 t.Fatalf("%s: expected locked coin %s:%d not found in wallet", test.name, txHash, vout) 1994 } 1995 } 1996 for _, coins := range allCoins { 1997 for _, coin := range coins { 1998 // decode coin to output 1999 out := coin.(*Output) 2000 checkLockedCoin(out.Pt.TxHash, out.Pt.Vout) 2001 checkFundingCoin(out.Pt.TxHash, out.Pt.Vout) 2002 } 2003 } 2004 for _, expectedIn := range test.expectedInputs { 2005 checkLockedCoin(expectedIn.PreviousOutPoint.Hash, expectedIn.PreviousOutPoint.Index) 2006 } 2007 2008 if test.expectedSplitFee != splitFee { 2009 t.Fatalf("%s: unexpected split fee. expected %d, got %d", test.name, test.expectedSplitFee, splitFee) 2010 } 2011 } 2012 } 2013 2014 func TestMaxFundingFees(t *testing.T) { 2015 runRubric(t, testMaxFundingFees) 2016 } 2017 2018 func testMaxFundingFees(t *testing.T, segwit bool, walletType string) { 2019 wallet, _, shutdown := tNewWallet(segwit, walletType) 2020 defer shutdown() 2021 2022 maxFeeRate := uint64(100) 2023 2024 useSplitOptions := map[string]string{ 2025 multiSplitKey: "true", 2026 } 2027 noSplitOptions := map[string]string{ 2028 multiSplitKey: "false", 2029 } 2030 2031 var inputSize, outputSize uint64 2032 if segwit { 2033 inputSize = dexbtc.RedeemP2WPKHInputTotalSize 2034 outputSize = dexbtc.P2WPKHOutputSize 2035 } else { 2036 inputSize = dexbtc.RedeemP2PKHInputSize 2037 outputSize = dexbtc.P2PKHOutputSize 2038 } 2039 2040 const maxSwaps = 3 2041 const numInputs = 12 2042 maxFundingFees := wallet.MaxFundingFees(maxSwaps, maxFeeRate, useSplitOptions) 2043 expectedFees := maxFeeRate * (inputSize*numInputs + outputSize*(maxSwaps+1) + dexbtc.MinimumTxOverhead) 2044 if maxFundingFees != expectedFees { 2045 t.Fatalf("unexpected max funding fees. expected %d, got %d", expectedFees, maxFundingFees) 2046 } 2047 2048 maxFundingFees = wallet.MaxFundingFees(maxSwaps, maxFeeRate, noSplitOptions) 2049 if maxFundingFees != 0 { 2050 t.Fatalf("unexpected max funding fees. expected 0, got %d", maxFundingFees) 2051 } 2052 } 2053 2054 func TestAvailableFund(t *testing.T) { 2055 runRubric(t, testAvailableFund) 2056 } 2057 2058 func testAvailableFund(t *testing.T, segwit bool, walletType string) { 2059 wallet, node, shutdown := tNewWallet(segwit, walletType) 2060 defer shutdown() 2061 2062 // With an empty list returned, there should be no error, but the value zero 2063 // should be returned. 2064 unspents := make([]*ListUnspentResult, 0) 2065 node.listUnspent = unspents // only needed for Fund, not Balance 2066 node.listLockUnspent = []*RPCOutpoint{} 2067 var bals GetBalancesResult 2068 node.getBalances = &bals 2069 bal, err := wallet.Balance() 2070 if err != nil { 2071 t.Fatalf("error for zero utxos: %v", err) 2072 } 2073 if bal.Available != 0 { 2074 t.Fatalf("expected available = 0, got %d", bal.Available) 2075 } 2076 if bal.Immature != 0 { 2077 t.Fatalf("expected unconf = 0, got %d", bal.Immature) 2078 } 2079 2080 node.getBalancesErr = tErr 2081 _, err = wallet.Balance() 2082 if err == nil { 2083 t.Fatalf("no wallet error for rpc error") 2084 } 2085 node.getBalancesErr = nil 2086 var littleLots uint64 = 12 2087 littleOrder := tLotSize * littleLots 2088 littleFunds := calc.RequiredOrderFunds(littleOrder, dexbtc.RedeemP2PKHInputSize, littleLots, tSwapSizeBase, tSwapSize, tBTC.MaxFeeRate) 2089 littleUTXO := &ListUnspentResult{ 2090 TxID: tTxID, 2091 Address: "1Bggq7Vu5oaoLFV1NNp5KhAzcku83qQhgi", 2092 Amount: float64(littleFunds) / 1e8, 2093 Confirmations: 0, 2094 ScriptPubKey: tP2PKH, 2095 Spendable: true, 2096 Solvable: true, 2097 SafePtr: boolPtr(true), 2098 } 2099 unspents = append(unspents, littleUTXO) 2100 node.listUnspent = unspents 2101 bals.Mine.Trusted = float64(littleFunds) / 1e8 2102 node.getBalances = &bals 2103 lockedVal := uint64(1e6) 2104 node.listLockUnspent = []*RPCOutpoint{ 2105 { 2106 TxID: tTxID, 2107 Vout: 1, 2108 }, 2109 } 2110 2111 msgTx := makeRawTx([]dex.Bytes{{0x01}, {0x02}}, []*wire.TxIn{dummyInput()}) 2112 msgTx.TxOut[1].Value = int64(lockedVal) 2113 txBuf := bytes.NewBuffer(make([]byte, 0, dexbtc.MsgTxVBytes(msgTx))) 2114 msgTx.Serialize(txBuf) 2115 const blockHeight = 5 2116 blockHash, _ := node.addRawTx(blockHeight, msgTx) 2117 2118 node.getTransactionMap = map[string]*GetTransactionResult{ 2119 "any": { 2120 BlockHash: blockHash.String(), 2121 BlockIndex: blockHeight, 2122 Bytes: txBuf.Bytes(), 2123 }} 2124 2125 bal, err = wallet.Balance() 2126 if err != nil { 2127 t.Fatalf("error for 1 utxo: %v", err) 2128 } 2129 if bal.Available != littleFunds-lockedVal { 2130 t.Fatalf("expected available = %d for confirmed utxos, got %d", littleOrder-lockedVal, bal.Available) 2131 } 2132 if bal.Immature != 0 { 2133 t.Fatalf("expected immature = 0, got %d", bal.Immature) 2134 } 2135 if bal.Locked != lockedVal { 2136 t.Fatalf("expected locked = %d, got %d", lockedVal, bal.Locked) 2137 } 2138 2139 var lottaLots uint64 = 100 2140 lottaOrder := tLotSize * lottaLots 2141 // Add funding for an extra input to accommodate the later combined tests. 2142 lottaFunds := calc.RequiredOrderFunds(lottaOrder, 2*dexbtc.RedeemP2PKHInputSize, lottaLots, tSwapSizeBase, tSwapSize, tBTC.MaxFeeRate) 2143 lottaUTXO := &ListUnspentResult{ 2144 TxID: tTxID, 2145 Address: "1Bggq7Vu5oaoLFV1NNp5KhAzcku83qQhgi", 2146 Amount: float64(lottaFunds) / 1e8, 2147 Confirmations: 1, 2148 Vout: 1, 2149 ScriptPubKey: tP2PKH, 2150 Spendable: true, 2151 Solvable: true, 2152 SafePtr: boolPtr(true), 2153 } 2154 unspents = append(unspents, lottaUTXO) 2155 littleUTXO.Confirmations = 1 2156 node.listUnspent = unspents 2157 bals.Mine.Trusted += float64(lottaFunds) / 1e8 2158 node.getBalances = &bals 2159 bal, err = wallet.Balance() 2160 if err != nil { 2161 t.Fatalf("error for 2 utxos: %v", err) 2162 } 2163 if bal.Available != littleFunds+lottaFunds-lockedVal { 2164 t.Fatalf("expected available = %d for 2 outputs, got %d", littleFunds+lottaFunds-lockedVal, bal.Available) 2165 } 2166 if bal.Immature != 0 { 2167 t.Fatalf("expected immature = 0 for 2 outputs, got %d", bal.Immature) 2168 } 2169 2170 ord := &asset.Order{ 2171 Version: version, 2172 Value: 0, 2173 MaxSwapCount: 1, 2174 MaxFeeRate: tBTC.MaxFeeRate, 2175 FeeSuggestion: feeSuggestion, 2176 } 2177 2178 setOrderValue := func(v uint64) { 2179 ord.Value = v 2180 ord.MaxSwapCount = v / tLotSize 2181 } 2182 2183 // Zero value 2184 _, _, _, err = wallet.FundOrder(ord) 2185 if err == nil { 2186 t.Fatalf("no funding error for zero value") 2187 } 2188 2189 // Nothing to spend 2190 node.listUnspent = []*ListUnspentResult{} 2191 setOrderValue(littleOrder) 2192 _, _, _, err = wallet.FundOrder(ord) 2193 if err == nil { 2194 t.Fatalf("no error for zero utxos") 2195 } 2196 node.listUnspent = unspents 2197 2198 // RPC error 2199 node.listUnspentErr = tErr 2200 _, _, _, err = wallet.FundOrder(ord) 2201 if err == nil { 2202 t.Fatalf("no funding error for rpc error") 2203 } 2204 node.listUnspentErr = nil 2205 2206 // Negative response when locking outputs. 2207 // There is no way to error locking outpoints in spv 2208 if walletType != walletTypeSPV { 2209 node.lockUnspentErr = tErr 2210 _, _, _, err = wallet.FundOrder(ord) 2211 if err == nil { 2212 t.Fatalf("no error for lockunspent result = false: %v", err) 2213 } 2214 node.lockUnspentErr = nil 2215 } 2216 2217 // Fund a little bit, with unsafe littleUTXO. 2218 littleUTXO.SafePtr = boolPtr(false) 2219 littleUTXO.Confirmations = 0 2220 node.listUnspent = unspents 2221 spendables, _, _, err := wallet.FundOrder(ord) 2222 if err != nil { 2223 t.Fatalf("error funding small amount: %v", err) 2224 } 2225 if len(spendables) != 1 { 2226 t.Fatalf("expected 1 spendable, got %d", len(spendables)) 2227 } 2228 v := spendables[0].Value() 2229 if v != lottaFunds { // has to pick the larger output 2230 t.Fatalf("expected spendable of value %d, got %d", lottaFunds, v) 2231 } 2232 2233 // Return/unlock the reserved coins to avoid warning in subsequent tests 2234 // about fundingCoins map containing the coins already. i.e. 2235 // "Known order-funding coin %v returned by listunspent" 2236 2237 _ = wallet.ReturnCoins(spendables) 2238 2239 // Now with safe confirmed littleUTXO. 2240 littleUTXO.SafePtr = boolPtr(true) 2241 littleUTXO.Confirmations = 2 2242 node.listUnspent = unspents 2243 spendables, _, _, err = wallet.FundOrder(ord) 2244 if err != nil { 2245 t.Fatalf("error funding small amount: %v", err) 2246 } 2247 if len(spendables) != 1 { 2248 t.Fatalf("expected 1 spendable, got %d", len(spendables)) 2249 } 2250 v = spendables[0].Value() 2251 if v != littleFunds { 2252 t.Fatalf("expected spendable of value %d, got %d", littleFunds, v) 2253 } 2254 _ = wallet.ReturnCoins(spendables) 2255 2256 // Adding a fee bump should now require the larger UTXO. 2257 ord.Options = map[string]string{swapFeeBumpKey: "1.5"} 2258 spendables, _, _, err = wallet.FundOrder(ord) 2259 if err != nil { 2260 t.Fatalf("error funding bumped fees: %v", err) 2261 } 2262 if len(spendables) != 1 { 2263 t.Fatalf("expected 1 spendable, got %d", len(spendables)) 2264 } 2265 v = spendables[0].Value() 2266 if v != lottaFunds { // picks the bigger output because it is confirmed 2267 t.Fatalf("expected bumped fee utxo of value %d, got %d", littleFunds, v) 2268 } 2269 ord.Options = nil 2270 littleUTXO.Confirmations = 0 2271 _ = wallet.ReturnCoins(spendables) 2272 2273 // Make lottaOrder unconfirmed like littleOrder, favoring little now. 2274 lottaUTXO.Confirmations = 0 2275 node.listUnspent = unspents 2276 spendables, _, _, err = wallet.FundOrder(ord) 2277 if err != nil { 2278 t.Fatalf("error funding small amount: %v", err) 2279 } 2280 if len(spendables) != 1 { 2281 t.Fatalf("expected 1 spendable, got %d", len(spendables)) 2282 } 2283 v = spendables[0].Value() 2284 if v != littleFunds { // now picks the smaller output 2285 t.Fatalf("expected spendable of value %d, got %d", littleFunds, v) 2286 } 2287 _ = wallet.ReturnCoins(spendables) 2288 2289 // Fund a lotta bit, covered by just the lottaBit UTXO. 2290 setOrderValue(lottaOrder) 2291 spendables, _, fees, err := wallet.FundOrder(ord) 2292 if err != nil { 2293 t.Fatalf("error funding large amount: %v", err) 2294 } 2295 if len(spendables) != 1 { 2296 t.Fatalf("expected 1 spendable, got %d", len(spendables)) 2297 } 2298 if fees != 0 { 2299 t.Fatalf("expected no fees, got %d", fees) 2300 } 2301 v = spendables[0].Value() 2302 if v != lottaFunds { 2303 t.Fatalf("expected spendable of value %d, got %d", lottaFunds, v) 2304 } 2305 _ = wallet.ReturnCoins(spendables) 2306 2307 // require both spendables 2308 extraLottaOrder := littleOrder + lottaOrder 2309 extraLottaLots := littleLots + lottaLots 2310 setOrderValue(extraLottaOrder) 2311 spendables, _, fees, err = wallet.FundOrder(ord) 2312 if err != nil { 2313 t.Fatalf("error funding large amount: %v", err) 2314 } 2315 if len(spendables) != 2 { 2316 t.Fatalf("expected 2 spendable, got %d", len(spendables)) 2317 } 2318 if fees != 0 { 2319 t.Fatalf("expected no fees, got %d", fees) 2320 } 2321 v = spendables[0].Value() 2322 if v != lottaFunds { 2323 t.Fatalf("expected spendable of value %d, got %d", lottaFunds, v) 2324 } 2325 _ = wallet.ReturnCoins(spendables) 2326 2327 // Not enough to cover transaction fees. 2328 tweak := float64(littleFunds+lottaFunds-calc.RequiredOrderFunds(extraLottaOrder, 2*dexbtc.RedeemP2PKHInputSize, extraLottaLots, tSwapSizeBase, tSwapSize, tBTC.MaxFeeRate)+1) / 1e8 2329 lottaUTXO.Amount -= tweak 2330 node.listUnspent = unspents 2331 _, _, _, err = wallet.FundOrder(ord) 2332 if err == nil { 2333 t.Fatalf("no error when not enough to cover tx fees") 2334 } 2335 lottaUTXO.Amount += tweak 2336 node.listUnspent = unspents 2337 2338 // Prepare for a split transaction. 2339 baggageFees := tBTC.MaxFeeRate * splitTxBaggage 2340 if segwit { 2341 node.changeAddr = tP2WPKHAddr 2342 node.newAddress = tP2WPKHAddr 2343 } else { 2344 node.changeAddr = tP2PKHAddr 2345 node.newAddress = tP2PKHAddr 2346 } 2347 node.walletCfg.useSplitTx = true 2348 // No error when no split performed cuz math. 2349 coins, _, fees, err := wallet.FundOrder(ord) 2350 if err != nil { 2351 t.Fatalf("error for no-split split: %v", err) 2352 } 2353 if fees != 0 { 2354 t.Fatalf("no-split split returned non-zero fees: %d", fees) 2355 } 2356 // Should be both coins. 2357 if len(coins) != 2 { 2358 t.Fatalf("no-split split didn't return both coins") 2359 } 2360 _ = wallet.ReturnCoins(coins) 2361 2362 // No split because not standing order. 2363 ord.Immediate = true 2364 coins, _, fees, err = wallet.FundOrder(ord) 2365 if err != nil { 2366 t.Fatalf("error for no-split split: %v", err) 2367 } 2368 ord.Immediate = false 2369 if len(coins) != 2 { 2370 t.Fatalf("no-split split didn't return both coins") 2371 } 2372 if fees != 0 { 2373 t.Fatalf("no-split split returned non-zero fees: %d", fees) 2374 } 2375 _ = wallet.ReturnCoins(coins) 2376 2377 var inputSize, outputSize uint64 2378 if wallet.segwit { 2379 inputSize = dexbtc.RedeemP2WPKHInputTotalSize 2380 outputSize = dexbtc.P2WPKHOutputSize 2381 } else { 2382 inputSize = dexbtc.RedeemP2PKHInputSize 2383 outputSize = dexbtc.P2PKHOutputSize 2384 } 2385 expectedTxSize := dexbtc.MinimumTxOverhead + 2*inputSize + 2*outputSize 2386 if wallet.segwit { 2387 expectedTxSize -= 1 // double counted wittness flag 2388 } 2389 expectedFees := expectedTxSize * feeSuggestion 2390 2391 // With a little more locked, the split should be performed. 2392 node.signFunc = func(tx *wire.MsgTx) { 2393 signFunc(tx, 0, wallet.segwit) 2394 } 2395 lottaUTXO.Amount += float64(baggageFees) / 1e8 2396 node.listUnspent = unspents 2397 coins, _, fees, err = wallet.FundOrder(ord) 2398 if err != nil { 2399 t.Fatalf("error for split tx: %v", err) 2400 } 2401 // Should be just one coin. 2402 if len(coins) != 1 { 2403 t.Fatalf("split failed - coin count != 1") 2404 } 2405 if node.sentRawTx == nil { 2406 t.Fatalf("split failed - no tx sent") 2407 } 2408 if fees != expectedFees { 2409 t.Fatalf("split returned unexpected fees. wanted %d, got %d", expectedFees, fees) 2410 } 2411 _ = wallet.ReturnCoins(coins) 2412 2413 // The split should also be added if we set the option at order time. 2414 node.walletCfg.useSplitTx = false 2415 ord.Options = map[string]string{splitKey: "true"} 2416 coins, _, fees, err = wallet.FundOrder(ord) 2417 if err != nil { 2418 t.Fatalf("error for forced split tx: %v", err) 2419 } 2420 // Should be just one coin still. 2421 if len(coins) != 1 { 2422 t.Fatalf("forced split failed - coin count != 1") 2423 } 2424 if fees != expectedFees { 2425 t.Fatalf("split returned unexpected fees. wanted %d, got %d", expectedFees, fees) 2426 } 2427 _ = wallet.ReturnCoins(coins) 2428 2429 // // Hit some error paths. 2430 2431 // Split transaction requires valid fee suggestion. 2432 // TODO: 2433 // 1.0: Error when no suggestion. 2434 // ord.FeeSuggestion = 0 2435 // _, _, err = wallet.FundOrder(ord) 2436 // if err == nil { 2437 // t.Fatalf("no error for no fee suggestions on split tx") 2438 // } 2439 ord.FeeSuggestion = tBTC.MaxFeeRate + 1 2440 _, _, _, err = wallet.FundOrder(ord) 2441 if err == nil { 2442 t.Fatalf("no error for no fee suggestions on split tx") 2443 } 2444 // Check success again. 2445 ord.FeeSuggestion = tBTC.MaxFeeRate 2446 coins, _, _, err = wallet.FundOrder(ord) 2447 if err != nil { 2448 t.Fatalf("error fixing split tx: %v", err) 2449 } 2450 if fees != expectedFees { 2451 t.Fatalf("expected fees of %d, got %d", expectedFees, fees) 2452 } 2453 _ = wallet.ReturnCoins(coins) 2454 2455 // GetRawChangeAddress error 2456 node.changeAddrErr = tErr 2457 _, _, _, err = wallet.FundOrder(ord) 2458 if err == nil { 2459 t.Fatalf("no error for split tx change addr error") 2460 } 2461 node.changeAddrErr = nil 2462 2463 // SendRawTx error 2464 node.sendErr = tErr 2465 _, _, _, err = wallet.FundOrder(ord) 2466 if err == nil { 2467 t.Fatalf("no error for split tx send error") 2468 } 2469 node.sendErr = nil 2470 2471 // Success again. 2472 spendables, _, _, err = wallet.FundOrder(ord) 2473 if err != nil { 2474 t.Fatalf("error for split tx recovery run") 2475 } 2476 _ = wallet.ReturnCoins(spendables) 2477 } 2478 2479 // Since ReturnCoins takes the asset.Coin interface, make sure any interface 2480 // is acceptable. 2481 type tCoin struct{ id []byte } 2482 2483 func (c *tCoin) ID() dex.Bytes { 2484 if len(c.id) > 0 { 2485 return c.id 2486 } 2487 return make([]byte, 36) 2488 } 2489 func (c *tCoin) String() string { return hex.EncodeToString(c.id) } 2490 func (c *tCoin) TxID() string { return hex.EncodeToString(c.id) } 2491 func (c *tCoin) Value() uint64 { return 100 } 2492 2493 func TestReturnCoins(t *testing.T) { 2494 wallet, node, shutdown := tNewWallet(true, walletTypeRPC) 2495 defer shutdown() 2496 2497 // Test it with the local output type. 2498 coins := asset.Coins{ 2499 NewOutput(tTxHash, 0, 1), 2500 } 2501 err := wallet.ReturnCoins(coins) 2502 if err != nil { 2503 t.Fatalf("error with output type coins: %v", err) 2504 } 2505 2506 // Should error for no coins. 2507 err = wallet.ReturnCoins(asset.Coins{}) 2508 if err == nil { 2509 t.Fatalf("no error for zero coins") 2510 } 2511 2512 // nil unlocks all 2513 wallet.cm.lockedOutputs[OutPoint{*tTxHash, 0}] = &UTxO{} 2514 err = wallet.ReturnCoins(nil) 2515 if err != nil { 2516 t.Fatalf("error for nil coins: %v", err) 2517 } 2518 if len(wallet.cm.lockedOutputs) != 0 { 2519 t.Errorf("all funding coins not unlocked") 2520 } 2521 2522 // Have the RPC return negative response. 2523 node.lockUnspentErr = tErr 2524 err = wallet.ReturnCoins(coins) 2525 if err == nil { 2526 t.Fatalf("no error for RPC failure") 2527 } 2528 node.lockUnspentErr = nil 2529 2530 // ReturnCoins should accept any type that implements asset.Coin. 2531 err = wallet.ReturnCoins(asset.Coins{&tCoin{}, &tCoin{}}) 2532 if err != nil { 2533 t.Fatalf("error with custom coin type: %v", err) 2534 } 2535 } 2536 2537 func TestFundingCoins(t *testing.T) { 2538 // runRubric(t, testFundingCoins) 2539 testFundingCoins(t, false, walletTypeRPC) 2540 } 2541 2542 func testFundingCoins(t *testing.T, segwit bool, walletType string) { 2543 wallet, node, shutdown := tNewWallet(segwit, walletType) 2544 defer shutdown() 2545 2546 const vout0 = 1 2547 const txBlockHeight = 3 2548 tx0 := makeRawTx([]dex.Bytes{{0x01}, tP2PKH}, []*wire.TxIn{dummyInput()}) 2549 txHash0 := tx0.TxHash() 2550 _, _ = node.addRawTx(txBlockHeight, tx0) 2551 coinID0 := ToCoinID(&txHash0, vout0) 2552 // Make spendable (confs > 0) 2553 node.addRawTx(txBlockHeight+1, dummyTx()) 2554 2555 p2pkhUnspent0 := &ListUnspentResult{ 2556 TxID: txHash0.String(), 2557 Vout: vout0, 2558 ScriptPubKey: tP2PKH, 2559 Spendable: true, 2560 Solvable: true, 2561 SafePtr: boolPtr(true), 2562 Amount: 1, 2563 } 2564 unspents := []*ListUnspentResult{p2pkhUnspent0} 2565 2566 // Add a second funding coin to make sure more than one iteration of the 2567 // utxo loops is required. 2568 const vout1 = 0 2569 tx1 := makeRawTx([]dex.Bytes{tP2PKH, {0x02}}, []*wire.TxIn{dummyInput()}) 2570 txHash1 := tx1.TxHash() 2571 _, _ = node.addRawTx(txBlockHeight, tx1) 2572 coinID1 := ToCoinID(&txHash1, vout1) 2573 // Make spendable (confs > 0) 2574 node.addRawTx(txBlockHeight+1, dummyTx()) 2575 2576 p2pkhUnspent1 := &ListUnspentResult{ 2577 TxID: txHash1.String(), 2578 Vout: vout1, 2579 ScriptPubKey: tP2PKH, 2580 Spendable: true, 2581 Solvable: true, 2582 SafePtr: boolPtr(true), 2583 Amount: 1, 2584 } 2585 unspents = append(unspents, p2pkhUnspent1) 2586 2587 node.listLockUnspent = []*RPCOutpoint{} 2588 node.listUnspent = unspents 2589 coinIDs := []dex.Bytes{coinID0, coinID1} 2590 2591 ensureGood := func() { 2592 t.Helper() 2593 coins, err := wallet.FundingCoins(coinIDs) 2594 if err != nil { 2595 t.Fatalf("FundingCoins error: %v", err) 2596 } 2597 if len(coins) != 2 { 2598 t.Fatalf("expected 2 coins, got %d", len(coins)) 2599 } 2600 } 2601 ensureGood() 2602 2603 ensureErr := func(tag string) { 2604 t.Helper() 2605 // Clear the cache. 2606 wallet.cm.lockedOutputs = make(map[OutPoint]*UTxO) 2607 _, err := wallet.FundingCoins(coinIDs) 2608 if err == nil { 2609 t.Fatalf("%s: no error", tag) 2610 } 2611 } 2612 2613 // No coins 2614 node.listUnspent = []*ListUnspentResult{} 2615 ensureErr("no coins") 2616 node.listUnspent = unspents 2617 2618 // RPC error 2619 node.listUnspentErr = tErr 2620 ensureErr("rpc coins") 2621 node.listUnspentErr = nil 2622 2623 // Bad coin ID. 2624 ogIDs := coinIDs 2625 coinIDs = []dex.Bytes{randBytes(35)} 2626 ensureErr("bad coin ID") 2627 coinIDs = ogIDs 2628 2629 // Coins locked but not in wallet.fundingCoins. 2630 irrelevantTx := dummyTx() 2631 node.addRawTx(txBlockHeight+1, irrelevantTx) 2632 node.listLockUnspent = []*RPCOutpoint{ 2633 {TxID: p2pkhUnspent0.TxID, Vout: p2pkhUnspent0.Vout}, 2634 {TxID: p2pkhUnspent1.TxID, Vout: p2pkhUnspent1.Vout}, 2635 } 2636 node.listUnspent = []*ListUnspentResult{} 2637 2638 txRaw0, _ := serializeMsgTx(tx0) 2639 getTxRes0 := &GetTransactionResult{ 2640 Bytes: txRaw0, 2641 } 2642 txRaw1, _ := serializeMsgTx(tx1) 2643 getTxRes1 := &GetTransactionResult{ 2644 Bytes: txRaw1, 2645 } 2646 2647 node.getTransactionMap = map[string]*GetTransactionResult{ 2648 txHash0.String(): getTxRes0, 2649 txHash1.String(): getTxRes1, 2650 } 2651 2652 ensureGood() 2653 } 2654 2655 func checkMaxOrder(t *testing.T, wallet asset.Wallet, lots, swapVal, maxFees, estWorstCase, estBestCase uint64) { 2656 t.Helper() 2657 maxOrder, err := wallet.MaxOrder(&asset.MaxOrderForm{ 2658 LotSize: tLotSize, 2659 FeeSuggestion: feeSuggestion, 2660 AssetVersion: version, 2661 MaxFeeRate: tBTC.MaxFeeRate, 2662 }) 2663 if err != nil { 2664 t.Fatalf("MaxOrder error: %v", err) 2665 } 2666 checkSwapEstimate(t, maxOrder, lots, swapVal, maxFees, estWorstCase, estBestCase) 2667 } 2668 2669 func checkSwapEstimate(t *testing.T, est *asset.SwapEstimate, lots, swapVal, maxFees, estWorstCase, estBestCase uint64) { 2670 t.Helper() 2671 if est.Lots != lots { 2672 t.Fatalf("Estimate has wrong Lots. wanted %d, got %d", lots, est.Lots) 2673 } 2674 if est.Value != swapVal { 2675 t.Fatalf("Estimate has wrong Value. wanted %d, got %d", swapVal, est.Value) 2676 } 2677 if est.MaxFees != maxFees { 2678 t.Fatalf("Estimate has wrong MaxFees. wanted %d, got %d", maxFees, est.MaxFees) 2679 } 2680 if est.RealisticWorstCase != estWorstCase { 2681 t.Fatalf("Estimate has wrong RealisticWorstCase. wanted %d, got %d", estWorstCase, est.RealisticWorstCase) 2682 } 2683 if est.RealisticBestCase != estBestCase { 2684 t.Fatalf("Estimate has wrong RealisticBestCase. wanted %d, got %d", estBestCase, est.RealisticBestCase) 2685 } 2686 } 2687 2688 func TestFundEdges(t *testing.T) { 2689 wallet, node, shutdown := tNewWallet(false, walletTypeRPC) 2690 defer shutdown() 2691 swapVal := uint64(1e7) 2692 lots := swapVal / tLotSize 2693 2694 // Base Fees 2695 // fee_rate: 34 satoshi / vbyte (MaxFeeRate) 2696 // swap_size: 225 bytes (InitTxSize) 2697 // p2pkh input: 149 bytes (RedeemP2PKHInputSize) 2698 2699 // NOTE: Shouldn't swap_size_base be 73 bytes? 2700 2701 // swap_size_base: 76 bytes (225 - 149 p2pkh input) (InitTxSizeBase) 2702 // lot_size: 1e6 2703 // swap_value: 1e7 2704 // lots = swap_value / lot_size = 10 2705 // total_bytes = first_swap_size + chained_swap_sizes 2706 // chained_swap_sizes = (lots - 1) * swap_size 2707 // first_swap_size = swap_size_base + backing_bytes 2708 // total_bytes = swap_size_base + backing_bytes + (lots - 1) * swap_size 2709 // base_tx_bytes = total_bytes - backing_bytes 2710 // base_tx_bytes = (lots - 1) * swap_size + swap_size_base = 9 * 225 + 76 = 2101 2711 // base_fees = base_tx_bytes * fee_rate = 2101 * 34 = 71434 2712 // backing_bytes: 1x P2PKH inputs = dexbtc.P2PKHInputSize = 149 bytes 2713 // backing_fees: 149 * fee_rate(34 atoms/byte) = 5066 atoms 2714 // total_bytes = base_tx_bytes + backing_bytes = 2101 + 149 = 2250 2715 // total_fees: base_fees + backing_fees = 71434 + 5066 = 76500 atoms 2716 // OR total_bytes * fee_rate = 2250 * 34 = 76500 2717 // base_best_case_bytes = swap_size_base + backing_bytes 2718 // = 76 + 149 = 225 2719 const swapSize = 225 2720 const totalBytes = 2250 2721 const bestCaseBytes = 225 2722 backingFees := uint64(totalBytes) * tBTC.MaxFeeRate // total_bytes * fee_rate 2723 p2pkhUnspent := &ListUnspentResult{ 2724 TxID: tTxID, 2725 Address: tP2PKHAddr, 2726 Amount: float64(swapVal+backingFees-1) / 1e8, 2727 Confirmations: 5, 2728 ScriptPubKey: tP2PKH, 2729 Spendable: true, 2730 Solvable: true, 2731 SafePtr: boolPtr(true), 2732 } 2733 unspents := []*ListUnspentResult{p2pkhUnspent} 2734 node.listUnspent = unspents 2735 ord := &asset.Order{ 2736 Version: version, 2737 Value: swapVal, 2738 MaxSwapCount: lots, 2739 MaxFeeRate: tBTC.MaxFeeRate, 2740 FeeSuggestion: feeSuggestion, 2741 } 2742 2743 var feeReduction uint64 = swapSize * tBTC.MaxFeeRate 2744 estFeeReduction := swapSize * feeSuggestion 2745 splitFees := splitTxBaggage * tBTC.MaxFeeRate 2746 checkMaxOrder(t, wallet, lots-1, swapVal-tLotSize, backingFees+splitFees-feeReduction, 2747 (totalBytes+splitTxBaggage)*feeSuggestion-estFeeReduction, 2748 (bestCaseBytes+splitTxBaggage)*feeSuggestion) 2749 2750 _, _, _, err := wallet.FundOrder(ord) 2751 if err == nil { 2752 t.Fatalf("no error when not enough funds in single p2pkh utxo") 2753 } 2754 // Now add the needed satoshi and try again. 2755 p2pkhUnspent.Amount = float64(swapVal+backingFees) / 1e8 2756 node.listUnspent = unspents 2757 2758 checkMaxOrder(t, wallet, lots, swapVal, backingFees, totalBytes*feeSuggestion, 2759 bestCaseBytes*feeSuggestion) 2760 2761 spendables, _, _, err := wallet.FundOrder(ord) 2762 if err != nil { 2763 t.Fatalf("error when should be enough funding in single p2pkh utxo: %v", err) 2764 } 2765 _ = wallet.ReturnCoins(spendables) 2766 2767 // For a split transaction, we would need to cover the splitTxBaggage as 2768 // well. 2769 node.walletCfg.useSplitTx = true 2770 node.changeAddr = tP2WPKHAddr 2771 node.newAddress = tP2WPKHAddr 2772 node.signFunc = func(tx *wire.MsgTx) { 2773 signFunc(tx, 0, wallet.segwit) 2774 } 2775 backingFees = uint64(totalBytes+splitTxBaggage) * tBTC.MaxFeeRate 2776 // 1 too few atoms 2777 v := swapVal + backingFees - 1 2778 p2pkhUnspent.Amount = float64(v) / 1e8 2779 node.listUnspent = unspents 2780 2781 coins, _, _, err := wallet.FundOrder(ord) 2782 if err != nil { 2783 t.Fatalf("error when skipping split tx due to baggage: %v", err) 2784 } 2785 if coins[0].Value() != v { 2786 t.Fatalf("split performed when baggage wasn't covered") 2787 } 2788 _ = wallet.ReturnCoins(spendables) 2789 2790 // Just enough. 2791 v = swapVal + backingFees 2792 p2pkhUnspent.Amount = float64(v) / 1e8 2793 node.listUnspent = unspents 2794 2795 checkMaxOrder(t, wallet, lots, swapVal, backingFees, 2796 (totalBytes+splitTxBaggage)*feeSuggestion, 2797 (bestCaseBytes+splitTxBaggage)*feeSuggestion) 2798 2799 coins, _, _, err = wallet.FundOrder(ord) 2800 if err != nil { 2801 t.Fatalf("error funding split tx: %v", err) 2802 } 2803 if coins[0].Value() == v { 2804 t.Fatalf("split performed when baggage wasn't covered") 2805 } 2806 _ = wallet.ReturnCoins(spendables) 2807 node.walletCfg.useSplitTx = false 2808 2809 // P2SH(P2PKH) p2sh pkScript = 23 bytes, p2pkh pkScript (redeemscript) = 25 bytes 2810 // sigScript = signature(1 + 73) + pubkey(1 + 33) + redeemscript(1 + 25) = 134 2811 // P2SH input size = overhead(40) + sigScriptData(1 + 134) = 40 + 135 = 175 bytes 2812 // backing fees: 175 bytes * fee_rate(34) = 5950 satoshi 2813 // Use 1 P2SH AND 1 P2PKH from the previous test. 2814 // total: 71434 + 5950 + 5066 = 82450 satoshi 2815 p2shRedeem, _ := hex.DecodeString("76a914db1755408acd315baa75c18ebbe0e8eaddf64a9788ac") // 25, p2pkh redeem script 2816 scriptAddr := "37XDx4CwPVEg5mC3awSPGCKA5Fe5FdsAS2" 2817 p2shScriptPubKey, _ := hex.DecodeString("a9143ff6a24a50135f69be9ffed744443da08408fc1a87") // 23, p2sh pkScript 2818 backingFees = 82450 2819 halfSwap := swapVal / 2 2820 p2shUnspent := &ListUnspentResult{ 2821 TxID: tTxID, 2822 Address: scriptAddr, 2823 Amount: float64(halfSwap) / 1e8, 2824 Confirmations: 10, 2825 ScriptPubKey: p2shScriptPubKey, 2826 RedeemScript: p2shRedeem, 2827 Spendable: true, 2828 Solvable: true, 2829 SafePtr: boolPtr(true), 2830 } 2831 p2pkhUnspent.Amount = float64(halfSwap+backingFees-1) / 1e8 2832 unspents = []*ListUnspentResult{p2pkhUnspent, p2shUnspent} 2833 node.listUnspent = unspents 2834 _, _, _, err = wallet.FundOrder(ord) 2835 if err == nil { 2836 t.Fatalf("no error when not enough funds in two utxos") 2837 } 2838 p2pkhUnspent.Amount = float64(halfSwap+backingFees) / 1e8 2839 node.listUnspent = unspents 2840 _, _, _, err = wallet.FundOrder(ord) 2841 if err != nil { 2842 t.Fatalf("error when should be enough funding in two utxos: %v", err) 2843 } 2844 _ = wallet.ReturnCoins(spendables) 2845 2846 // P2WPKH witness: RedeemP2WPKHInputWitnessWeight = 109 2847 // P2WPKH input size = overhead(40) + no sigScript(1+0) + witness(ceil(109/4)) = 69 vbytes 2848 // backing fees: 69 * fee_rate(34) = 2346 satoshi 2849 // total: base_fees(71434) + 2346 = 73780 satoshi 2850 backingFees = 73780 2851 p2wpkhAddr := tP2WPKHAddr 2852 p2wpkhPkScript, _ := hex.DecodeString("0014054a40a6aa7c1f6bd15f286debf4f33cef0e21a1") 2853 p2wpkhUnspent := &ListUnspentResult{ 2854 TxID: tTxID, 2855 Address: p2wpkhAddr, 2856 Amount: float64(swapVal+backingFees-1) / 1e8, 2857 Confirmations: 3, 2858 ScriptPubKey: p2wpkhPkScript, 2859 Spendable: true, 2860 Solvable: true, 2861 SafePtr: boolPtr(true), 2862 } 2863 unspents = []*ListUnspentResult{p2wpkhUnspent} 2864 node.listUnspent = unspents 2865 _, _, _, err = wallet.FundOrder(ord) 2866 if err == nil { 2867 t.Fatalf("no error when not enough funds in single p2wpkh utxo") 2868 } 2869 p2wpkhUnspent.Amount = float64(swapVal+backingFees) / 1e8 2870 node.listUnspent = unspents 2871 _, _, _, err = wallet.FundOrder(ord) 2872 if err != nil { 2873 t.Fatalf("error when should be enough funding in single p2wpkh utxo: %v", err) 2874 } 2875 _ = wallet.ReturnCoins(spendables) 2876 2877 // P2WSH(P2WPKH) 2878 // p2wpkh redeem script length, btc.P2WPKHPkScriptSize: 22 2879 // witness: version(1) + signature(1 + 73) + pubkey(1 + 33) + redeemscript(1 + 22) = 132 2880 // input size: overhead(40) + no sigScript(1+0) + witness(132)/4 = 74 vbyte 2881 // backing fees: 74 * 34 = 2516 satoshi 2882 // total: base_fees(71434) + 2516 = 73950 satoshi 2883 backingFees = 73950 2884 p2wpkhRedeemScript, _ := hex.DecodeString("0014b71554f9a66ef4fa4dbeddb9fa491f5a1d938ebc") //22 2885 p2wshAddr := "bc1q9heng7q275grmy483cueqrr00dvyxpd8w6kes3nzptm7087d6lvsvffpqf" 2886 p2wshPkScript, _ := hex.DecodeString("00202df334780af5103d92a78e39900c6f7b584305a776ad9846620af7e79fcdd7d9") //34 2887 p2wpshUnspent := &ListUnspentResult{ 2888 TxID: tTxID, 2889 Address: p2wshAddr, 2890 Amount: float64(swapVal+backingFees-1) / 1e8, 2891 Confirmations: 7, 2892 ScriptPubKey: p2wshPkScript, 2893 RedeemScript: p2wpkhRedeemScript, 2894 Spendable: true, 2895 Solvable: true, 2896 SafePtr: boolPtr(true), 2897 } 2898 unspents = []*ListUnspentResult{p2wpshUnspent} 2899 node.listUnspent = unspents 2900 _, _, _, err = wallet.FundOrder(ord) 2901 if err == nil { 2902 t.Fatalf("no error when not enough funds in single p2wsh utxo") 2903 } 2904 p2wpshUnspent.Amount = float64(swapVal+backingFees) / 1e8 2905 node.listUnspent = unspents 2906 _, _, _, err = wallet.FundOrder(ord) 2907 if err != nil { 2908 t.Fatalf("error when should be enough funding in single p2wsh utxo: %v", err) 2909 } 2910 _ = wallet.ReturnCoins(spendables) 2911 } 2912 2913 func TestFundEdgesSegwit(t *testing.T) { 2914 wallet, node, shutdown := tNewWallet(true, walletTypeRPC) 2915 defer shutdown() 2916 swapVal := uint64(1e7) 2917 lots := swapVal / tLotSize 2918 2919 // Base Fees 2920 // fee_rate: 34 satoshi / vbyte (MaxFeeRate) 2921 2922 // swap_size: 153 bytes (InitTxSizeSegwit) 2923 // p2wpkh input, incl. marker and flag: 69 bytes (RedeemP2WPKHInputSize + ((RedeemP2WPKHInputWitnessWeight + 2 + 3) / 4)) 2924 // swap_size_base: 84 bytes (153 - 69 p2pkh input) (InitTxSizeBaseSegwit) 2925 2926 // lot_size: 1e6 2927 // swap_value: 1e7 2928 // lots = swap_value / lot_size = 10 2929 // total_bytes = first_swap_size + chained_swap_sizes 2930 // chained_swap_sizes = (lots - 1) * swap_size 2931 // first_swap_size = swap_size_base + backing_bytes 2932 // total_bytes = swap_size_base + backing_bytes + (lots - 1) * swap_size 2933 // base_tx_bytes = total_bytes - backing_bytes 2934 // base_tx_bytes = (lots - 1) * swap_size + swap_size_base = 9 * 153 + 84 = 1461 2935 // base_fees = base_tx_bytes * fee_rate = 1461 * 34 = 49674 2936 // backing_bytes: 1x P2WPKH-spending input = p2wpkh input = 69 bytes 2937 // backing_fees: 69 * fee_rate(34 atoms/byte) = 2346 atoms 2938 // total_bytes = base_tx_bytes + backing_bytes = 1461 + 69 = 1530 2939 // total_fees: base_fees + backing_fees = 49674 + 2346 = 52020 atoms 2940 // OR total_bytes * fee_rate = 1530 * 34 = 52020 2941 // base_best_case_bytes = swap_size_base + backing_bytes 2942 // = 84 + 69 = 153 2943 const swapSize = 153 2944 const totalBytes = 1530 2945 const bestCaseBytes = 153 2946 backingFees := uint64(totalBytes) * tBTC.MaxFeeRate // total_bytes * fee_rate 2947 p2wpkhUnspent := &ListUnspentResult{ 2948 TxID: tTxID, 2949 Address: tP2WPKHAddr, 2950 Amount: float64(swapVal+backingFees-1) / 1e8, 2951 Confirmations: 5, 2952 ScriptPubKey: tP2WPKH, 2953 Spendable: true, 2954 Solvable: true, 2955 SafePtr: boolPtr(true), 2956 } 2957 unspents := []*ListUnspentResult{p2wpkhUnspent} 2958 node.listUnspent = unspents 2959 ord := &asset.Order{ 2960 Version: version, 2961 Value: swapVal, 2962 MaxSwapCount: lots, 2963 MaxFeeRate: tBTC.MaxFeeRate, 2964 FeeSuggestion: feeSuggestion, 2965 } 2966 2967 var feeReduction uint64 = swapSize * tBTC.MaxFeeRate 2968 estFeeReduction := swapSize * feeSuggestion 2969 splitFees := splitTxBaggageSegwit * tBTC.MaxFeeRate 2970 checkMaxOrder(t, wallet, lots-1, swapVal-tLotSize, backingFees+splitFees-feeReduction, 2971 (totalBytes+splitTxBaggageSegwit)*feeSuggestion-estFeeReduction, 2972 (bestCaseBytes+splitTxBaggageSegwit)*feeSuggestion) 2973 2974 _, _, _, err := wallet.FundOrder(ord) 2975 if err == nil { 2976 t.Fatalf("no error when not enough funds in single p2wpkh utxo") 2977 } 2978 // Now add the needed satoshi and try again. 2979 p2wpkhUnspent.Amount = float64(swapVal+backingFees) / 1e8 2980 node.listUnspent = unspents 2981 2982 checkMaxOrder(t, wallet, lots, swapVal, backingFees, totalBytes*feeSuggestion, 2983 bestCaseBytes*feeSuggestion) 2984 2985 spendables, _, _, err := wallet.FundOrder(ord) 2986 if err != nil { 2987 t.Fatalf("error when should be enough funding in single p2wpkh utxo: %v", err) 2988 } 2989 _ = wallet.ReturnCoins(spendables) 2990 2991 // For a split transaction, we would need to cover the splitTxBaggage as 2992 // well. 2993 node.walletCfg.useSplitTx = true 2994 node.changeAddr = tP2WPKHAddr 2995 node.newAddress = tP2WPKHAddr 2996 node.signFunc = func(tx *wire.MsgTx) { 2997 signFunc(tx, 0, wallet.segwit) 2998 } 2999 backingFees = uint64(totalBytes+splitTxBaggageSegwit) * tBTC.MaxFeeRate 3000 v := swapVal + backingFees - 1 3001 p2wpkhUnspent.Amount = float64(v) / 1e8 3002 node.listUnspent = unspents 3003 coins, _, _, err := wallet.FundOrder(ord) 3004 if err != nil { 3005 t.Fatalf("error when skipping split tx because not enough to cover baggage: %v", err) 3006 } 3007 if coins[0].Value() != v { 3008 t.Fatalf("split performed when baggage wasn't covered") 3009 } 3010 _ = wallet.ReturnCoins(spendables) 3011 // Now get the split. 3012 v = swapVal + backingFees 3013 p2wpkhUnspent.Amount = float64(v) / 1e8 3014 node.listUnspent = unspents 3015 3016 checkMaxOrder(t, wallet, lots, swapVal, backingFees, 3017 (totalBytes+splitTxBaggageSegwit)*feeSuggestion, 3018 (bestCaseBytes+splitTxBaggageSegwit)*feeSuggestion) 3019 3020 coins, _, _, err = wallet.FundOrder(ord) 3021 if err != nil { 3022 t.Fatalf("error funding split tx: %v", err) 3023 } 3024 _ = wallet.ReturnCoins(spendables) 3025 if coins[0].Value() == v { 3026 t.Fatalf("split performed when baggage wasn't covered") 3027 } 3028 node.walletCfg.useSplitTx = false 3029 } 3030 3031 func TestSwap(t *testing.T) { 3032 runRubric(t, testSwap) 3033 } 3034 3035 func testSwap(t *testing.T, segwit bool, walletType string) { 3036 wallet, node, shutdown := tNewWallet(segwit, walletType) 3037 defer shutdown() 3038 3039 swapVal := toSatoshi(5) 3040 coins := asset.Coins{ 3041 NewOutput(tTxHash, 0, toSatoshi(3)), 3042 NewOutput(tTxHash, 0, toSatoshi(3)), 3043 } 3044 addrStr := tP2PKHAddr 3045 if segwit { 3046 addrStr = tP2WPKHAddr 3047 } 3048 3049 node.newAddress = addrStr 3050 node.changeAddr = addrStr 3051 3052 privBytes, _ := hex.DecodeString("b07209eec1a8fb6cfe5cb6ace36567406971a75c330db7101fb21bc679bc5330") 3053 privKey, _ := btcec.PrivKeyFromBytes(privBytes) 3054 wif, err := btcutil.NewWIF(privKey, &chaincfg.MainNetParams, true) 3055 if err != nil { 3056 t.Fatalf("error encoding wif: %v", err) 3057 } 3058 node.privKeyForAddr = wif 3059 3060 secretHash, _ := hex.DecodeString("5124208c80d33507befa517c08ed01aa8d33adbf37ecd70fb5f9352f7a51a88d") 3061 contract := &asset.Contract{ 3062 Address: addrStr, 3063 Value: swapVal, 3064 SecretHash: secretHash, 3065 LockTime: uint64(time.Now().Unix()), 3066 } 3067 3068 swaps := &asset.Swaps{ 3069 Inputs: coins, 3070 Contracts: []*asset.Contract{contract}, 3071 LockChange: true, 3072 FeeRate: tBTC.MaxFeeRate, 3073 } 3074 3075 // Aim for 3 signature cycles. 3076 sigSizer := 0 3077 node.signFunc = func(tx *wire.MsgTx) { 3078 var sizeTweak int 3079 if sigSizer%2 == 0 { 3080 sizeTweak = -2 3081 } 3082 sigSizer++ 3083 signFunc(tx, sizeTweak, wallet.segwit) 3084 } 3085 3086 // This time should succeed. 3087 _, changeCoin, feesPaid, err := wallet.Swap(swaps) 3088 if err != nil { 3089 t.Fatalf("swap error: %v", err) 3090 } 3091 3092 // Make sure the change coin is locked. 3093 if len(node.lockedCoins) != 1 { 3094 t.Fatalf("did not lock change coin") 3095 } 3096 txHash, _, _ := decodeCoinID(changeCoin.ID()) 3097 if node.lockedCoins[0].TxID != txHash.String() { 3098 t.Fatalf("wrong coin locked during swap") 3099 } 3100 3101 // Fees should be returned. 3102 minFees := tBTC.MaxFeeRate * dexbtc.MsgTxVBytes(node.sentRawTx) 3103 if feesPaid < minFees { 3104 t.Fatalf("sent fees, %d, less than required fees, %d", feesPaid, minFees) 3105 } 3106 3107 // Not enough funds 3108 swaps.Inputs = coins[:1] 3109 _, _, _, err = wallet.Swap(swaps) 3110 if err == nil { 3111 t.Fatalf("no error for listunspent not enough funds") 3112 } 3113 swaps.Inputs = coins 3114 3115 // AddressPKH error 3116 node.newAddressErr = tErr 3117 _, _, _, err = wallet.Swap(swaps) 3118 if err == nil { 3119 t.Fatalf("no error for getnewaddress rpc error") 3120 } 3121 node.newAddressErr = nil 3122 3123 // ChangeAddress error 3124 node.changeAddrErr = tErr 3125 _, _, _, err = wallet.Swap(swaps) 3126 if err == nil { 3127 t.Fatalf("no error for getrawchangeaddress rpc error") 3128 } 3129 node.changeAddrErr = nil 3130 3131 // SignTx error 3132 node.signTxErr = tErr 3133 _, _, _, err = wallet.Swap(swaps) 3134 if err == nil { 3135 t.Fatalf("no error for signrawtransactionwithwallet rpc error") 3136 } 3137 node.signTxErr = nil 3138 3139 // incomplete signatures 3140 node.sigIncomplete = true 3141 _, _, _, err = wallet.Swap(swaps) 3142 if err == nil { 3143 t.Fatalf("no error for incomplete signature rpc error") 3144 } 3145 node.sigIncomplete = false 3146 3147 // Make sure we can succeed again. 3148 _, _, _, err = wallet.Swap(swaps) 3149 if err != nil { 3150 t.Fatalf("re-swap error: %v", err) 3151 } 3152 } 3153 3154 type TAuditInfo struct{} 3155 3156 func (ai *TAuditInfo) Recipient() string { return tP2PKHAddr } 3157 func (ai *TAuditInfo) Expiration() time.Time { return time.Time{} } 3158 func (ai *TAuditInfo) Coin() asset.Coin { return &tCoin{} } 3159 func (ai *TAuditInfo) Contract() dex.Bytes { return nil } 3160 func (ai *TAuditInfo) SecretHash() dex.Bytes { return nil } 3161 3162 func TestRedeem(t *testing.T) { 3163 runRubric(t, testRedeem) 3164 } 3165 3166 func testRedeem(t *testing.T, segwit bool, walletType string) { 3167 wallet, node, shutdown := tNewWallet(segwit, walletType) 3168 defer shutdown() 3169 swapVal := toSatoshi(5) 3170 3171 secret, _, _, contract, addr, _, lockTime := makeSwapContract(segwit, time.Hour*12) 3172 3173 coin := NewOutput(tTxHash, 0, swapVal) 3174 ci := &asset.AuditInfo{ 3175 Coin: coin, 3176 Contract: contract, 3177 Recipient: addr.String(), 3178 Expiration: lockTime, 3179 } 3180 3181 redemption := &asset.Redemption{ 3182 Spends: ci, 3183 Secret: secret, 3184 } 3185 3186 privBytes, _ := hex.DecodeString("b07209eec1a8fb6cfe5cb6ace36567406971a75c330db7101fb21bc679bc5330") 3187 privKey, _ := btcec.PrivKeyFromBytes(privBytes) 3188 wif, err := btcutil.NewWIF(privKey, &chaincfg.MainNetParams, true) 3189 if err != nil { 3190 t.Fatalf("error encoding wif: %v", err) 3191 } 3192 3193 addrStr := tP2PKHAddr 3194 if segwit { 3195 addrStr = tP2WPKHAddr 3196 } 3197 3198 node.changeAddr = addrStr 3199 node.newAddress = addrStr 3200 node.privKeyForAddr = wif 3201 3202 redemptions := &asset.RedeemForm{ 3203 Redemptions: []*asset.Redemption{redemption}, 3204 } 3205 3206 _, _, feesPaid, err := wallet.Redeem(redemptions) 3207 if err != nil { 3208 t.Fatalf("redeem error: %v", err) 3209 } 3210 3211 // Check that fees are returned. 3212 minFees := optimalFeeRate * dexbtc.MsgTxVBytes(node.sentRawTx) 3213 if feesPaid < minFees { 3214 t.Fatalf("sent fees, %d, less than expected minimum fees, %d", feesPaid, minFees) 3215 } 3216 3217 // No audit info 3218 redemption.Spends = nil 3219 _, _, _, err = wallet.Redeem(redemptions) 3220 if err == nil { 3221 t.Fatalf("no error for nil AuditInfo") 3222 } 3223 redemption.Spends = ci 3224 3225 // Spoofing AuditInfo is not allowed. 3226 redemption.Spends = &asset.AuditInfo{} 3227 _, _, _, err = wallet.Redeem(redemptions) 3228 if err == nil { 3229 t.Fatalf("no error for spoofed AuditInfo") 3230 } 3231 redemption.Spends = ci 3232 3233 // Wrong secret hash 3234 redemption.Secret = randBytes(32) 3235 _, _, _, err = wallet.Redeem(redemptions) 3236 if err == nil { 3237 t.Fatalf("no error for wrong secret") 3238 } 3239 redemption.Secret = secret 3240 3241 // too low of value 3242 coin.Val = 200 3243 _, _, _, err = wallet.Redeem(redemptions) 3244 if err == nil { 3245 t.Fatalf("no error for redemption not worth the fees") 3246 } 3247 coin.Val = swapVal 3248 3249 // New address error 3250 node.newAddressErr = tErr 3251 _, _, _, err = wallet.Redeem(redemptions) 3252 if err == nil { 3253 t.Fatalf("no error for new address error") 3254 } 3255 node.newAddressErr = nil 3256 3257 // Missing priv key error 3258 node.privKeyForAddrErr = tErr 3259 _, _, _, err = wallet.Redeem(redemptions) 3260 if err == nil { 3261 t.Fatalf("no error for missing private key") 3262 } 3263 node.privKeyForAddrErr = nil 3264 3265 // Send error 3266 node.sendErr = tErr 3267 _, _, _, err = wallet.Redeem(redemptions) 3268 if err == nil { 3269 t.Fatalf("no error for send error") 3270 } 3271 node.sendErr = nil 3272 3273 // Wrong hash 3274 var h chainhash.Hash 3275 h[0] = 0x01 3276 node.badSendHash = &h 3277 _, _, _, err = wallet.Redeem(redemptions) 3278 if err == nil { 3279 t.Fatalf("no error for wrong return hash") 3280 } 3281 node.badSendHash = nil 3282 } 3283 3284 func TestSignMessage(t *testing.T) { 3285 runRubric(t, testSignMessage) 3286 } 3287 3288 func testSignMessage(t *testing.T, segwit bool, walletType string) { 3289 wallet, node, shutdown := tNewWallet(segwit, walletType) 3290 defer shutdown() 3291 3292 vout := uint32(5) 3293 privBytes, _ := hex.DecodeString("b07209eec1a8fb6cfe5cb6ace36567406971a75c330db7101fb21bc679bc5330") 3294 privKey, pubKey := btcec.PrivKeyFromBytes(privBytes) 3295 wif, err := btcutil.NewWIF(privKey, &chaincfg.MainNetParams, true) 3296 if err != nil { 3297 t.Fatalf("error encoding wif: %v", err) 3298 } 3299 3300 msg := randBytes(36) 3301 msgHash := chainhash.HashB(msg) 3302 pk := pubKey.SerializeCompressed() 3303 signature := ecdsa.Sign(privKey, msgHash) 3304 sig := signature.Serialize() 3305 3306 pt := NewOutPoint(tTxHash, vout) 3307 utxo := &UTxO{Address: tP2PKHAddr} 3308 wallet.cm.lockedOutputs[pt] = utxo 3309 node.privKeyForAddr = wif 3310 node.signMsgFunc = func(params []json.RawMessage) (json.RawMessage, error) { 3311 if len(params) != 2 { 3312 t.Fatalf("expected 2 params, found %d", len(params)) 3313 } 3314 var sentKey string 3315 var sentMsg dex.Bytes 3316 err := json.Unmarshal(params[0], &sentKey) 3317 if err != nil { 3318 t.Fatalf("unmarshal error: %v", err) 3319 } 3320 _ = sentMsg.UnmarshalJSON(params[1]) 3321 if sentKey != wif.String() { 3322 t.Fatalf("received wrong key. expected '%s', got '%s'", wif.String(), sentKey) 3323 } 3324 var checkMsg dex.Bytes = msg 3325 if sentMsg.String() != checkMsg.String() { 3326 t.Fatalf("received wrong message. expected '%s', got '%s'", checkMsg.String(), sentMsg.String()) 3327 } 3328 msgHash := chainhash.HashB(sentMsg) 3329 sig := ecdsa.Sign(wif.PrivKey, msgHash) 3330 r, _ := json.Marshal(base64.StdEncoding.EncodeToString(sig.Serialize())) 3331 return r, nil 3332 } 3333 3334 var coin asset.Coin = NewOutput(tTxHash, vout, 5e7) 3335 pubkeys, sigs, err := wallet.SignMessage(coin, msg) 3336 if err != nil { 3337 t.Fatalf("SignMessage error: %v", err) 3338 } 3339 if len(pubkeys) != 1 { 3340 t.Fatalf("expected 1 pubkey, received %d", len(pubkeys)) 3341 } 3342 if len(sigs) != 1 { 3343 t.Fatalf("expected 1 sig, received %d", len(sigs)) 3344 } 3345 if !bytes.Equal(pk, pubkeys[0]) { 3346 t.Fatalf("wrong pubkey. expected %x, got %x", pubkeys[0], pk) 3347 } 3348 if !bytes.Equal(sig, sigs[0]) { 3349 t.Fatalf("wrong signature. exptected %x, got %x", sigs[0], sig) 3350 } 3351 3352 // Unknown UTXO 3353 delete(wallet.cm.lockedOutputs, pt) 3354 _, _, err = wallet.SignMessage(coin, msg) 3355 if err == nil { 3356 t.Fatalf("no error for unknown utxo") 3357 } 3358 wallet.cm.lockedOutputs[pt] = utxo 3359 3360 // dumpprivkey error 3361 node.privKeyForAddrErr = tErr 3362 _, _, err = wallet.SignMessage(coin, msg) 3363 if err == nil { 3364 t.Fatalf("no error for dumpprivkey rpc error") 3365 } 3366 node.privKeyForAddrErr = nil 3367 3368 // bad coin 3369 badCoin := &tCoin{id: make([]byte, 15)} 3370 _, _, err = wallet.SignMessage(badCoin, msg) 3371 if err == nil { 3372 t.Fatalf("no error for bad coin") 3373 } 3374 } 3375 3376 func TestAuditContract(t *testing.T) { 3377 runRubric(t, testAuditContract) 3378 } 3379 3380 func testAuditContract(t *testing.T, segwit bool, walletType string) { 3381 wallet, _, shutdown := tNewWallet(segwit, walletType) 3382 defer shutdown() 3383 secretHash, _ := hex.DecodeString("5124208c80d33507befa517c08ed01aa8d33adbf37ecd70fb5f9352f7a51a88d") 3384 lockTime := time.Now().Add(time.Hour * 12) 3385 addr, _ := btcutil.DecodeAddress(tP2PKHAddr, &chaincfg.MainNetParams) 3386 if segwit { 3387 addr, _ = btcutil.DecodeAddress(tP2WPKHAddr, &chaincfg.MainNetParams) 3388 } 3389 3390 contract, err := dexbtc.MakeContract(addr, addr, secretHash, lockTime.Unix(), segwit, &chaincfg.MainNetParams) 3391 if err != nil { 3392 t.Fatalf("error making swap contract: %v", err) 3393 } 3394 3395 var contractAddr btcutil.Address 3396 if segwit { 3397 h := sha256.Sum256(contract) 3398 contractAddr, _ = btcutil.NewAddressWitnessScriptHash(h[:], &chaincfg.MainNetParams) 3399 } else { 3400 contractAddr, _ = btcutil.NewAddressScriptHash(contract, &chaincfg.MainNetParams) 3401 } 3402 pkScript, _ := txscript.PayToAddrScript(contractAddr) 3403 tx := makeRawTx([]dex.Bytes{pkScript}, []*wire.TxIn{dummyInput()}) 3404 txData, err := serializeMsgTx(tx) 3405 if err != nil { 3406 t.Fatalf("error making contract tx data: %v", err) 3407 } 3408 3409 txHash := tx.TxHash() 3410 const vout = 0 3411 3412 audit, err := wallet.AuditContract(ToCoinID(&txHash, vout), contract, txData, false) 3413 if err != nil { 3414 t.Fatalf("audit error: %v", err) 3415 } 3416 if audit.Recipient != addr.String() { 3417 t.Fatalf("wrong recipient. wanted '%s', got '%s'", addr, audit.Recipient) 3418 } 3419 if !bytes.Equal(audit.Contract, contract) { 3420 t.Fatalf("contract not set to coin redeem script") 3421 } 3422 if audit.Expiration.Equal(lockTime) { 3423 t.Fatalf("wrong lock time. wanted %d, got %d", lockTime.Unix(), audit.Expiration.Unix()) 3424 } 3425 3426 // Invalid txid 3427 _, err = wallet.AuditContract(make([]byte, 15), contract, txData, false) 3428 if err == nil { 3429 t.Fatalf("no error for bad txid") 3430 } 3431 3432 // Wrong contract 3433 pkh, _ := hex.DecodeString("c6a704f11af6cbee8738ff19fc28cdc70aba0b82") 3434 wrongAddr, _ := btcutil.NewAddressPubKeyHash(pkh, &chaincfg.MainNetParams) 3435 badContract, _ := txscript.PayToAddrScript(wrongAddr) 3436 _, err = wallet.AuditContract(ToCoinID(&txHash, vout), badContract, nil, false) 3437 if err == nil { 3438 t.Fatalf("no error for wrong contract") 3439 } 3440 } 3441 3442 type tReceipt struct { 3443 coin *tCoin 3444 contract []byte 3445 expiration uint64 3446 } 3447 3448 func (r *tReceipt) Expiration() time.Time { return time.Unix(int64(r.expiration), 0).UTC() } 3449 func (r *tReceipt) Coin() asset.Coin { return r.coin } 3450 func (r *tReceipt) Contract() dex.Bytes { return r.contract } 3451 3452 func TestFindRedemption(t *testing.T) { 3453 runRubric(t, testFindRedemption) 3454 } 3455 3456 func testFindRedemption(t *testing.T, segwit bool, walletType string) { 3457 wallet, node, shutdown := tNewWallet(segwit, walletType) 3458 defer shutdown() 3459 3460 contractHeight := node.GetBestBlockHeight() + 1 3461 otherTxid := "7a7b3b5c3638516bc8e7f19b4a3dec00f052a599fed5036c2b89829de2367bb6" 3462 otherTxHash, _ := chainhash.NewHashFromStr(otherTxid) 3463 contractVout := uint32(1) 3464 3465 secret, _, pkScript, contract, addr, _, _ := makeSwapContract(segwit, time.Hour*12) 3466 otherScript, _ := txscript.PayToAddrScript(addr) 3467 3468 var redemptionWitness, otherWitness [][]byte 3469 var redemptionSigScript, otherSigScript []byte 3470 if segwit { 3471 redemptionWitness = dexbtc.RedeemP2WSHContract(contract, randBytes(73), randBytes(33), secret) 3472 otherWitness = [][]byte{randBytes(73), randBytes(33)} 3473 } else { 3474 redemptionSigScript, _ = dexbtc.RedeemP2SHContract(contract, randBytes(73), randBytes(33), secret) 3475 otherSigScript, _ = txscript.NewScriptBuilder(). 3476 AddData(randBytes(73)). 3477 AddData(randBytes(33)). 3478 Script() 3479 } 3480 3481 // Prepare the "blockchain" 3482 inputs := []*wire.TxIn{makeRPCVin(otherTxHash, 0, otherSigScript, otherWitness)} 3483 // Add the contract transaction. Put the pay-to-contract script at index 1. 3484 contractTx := makeRawTx([]dex.Bytes{otherScript, pkScript}, inputs) 3485 contractTxHash := contractTx.TxHash() 3486 coinID := ToCoinID(&contractTxHash, contractVout) 3487 blockHash, _ := node.addRawTx(contractHeight, contractTx) 3488 txHex, err := makeTxHex([]dex.Bytes{otherScript, pkScript}, inputs) 3489 if err != nil { 3490 t.Fatalf("error generating hex for contract tx: %v", err) 3491 } 3492 getTxRes := &GetTransactionResult{ 3493 BlockHash: blockHash.String(), 3494 BlockIndex: contractHeight, 3495 Bytes: txHex, 3496 } 3497 node.getTransactionMap = map[string]*GetTransactionResult{ 3498 "any": getTxRes} 3499 3500 // Add an intermediate block for good measure. 3501 node.addRawTx(contractHeight+1, makeRawTx([]dex.Bytes{otherScript}, inputs)) 3502 3503 // Now add the redemption. 3504 redeemVin := makeRPCVin(&contractTxHash, contractVout, redemptionSigScript, redemptionWitness) 3505 inputs = append(inputs, redeemVin) 3506 redeemBlockHash, _ := node.addRawTx(contractHeight+2, makeRawTx([]dex.Bytes{otherScript}, inputs)) 3507 node.getCFilterScripts[*redeemBlockHash] = [][]byte{pkScript} 3508 3509 // Update currentTip from "RPC". Normally run() would do this. 3510 wallet.reportNewTip(tCtx, &BlockVector{ 3511 Hash: *redeemBlockHash, 3512 Height: contractHeight + 2, 3513 }) 3514 3515 // Check find redemption result. 3516 _, checkSecret, err := wallet.FindRedemption(tCtx, coinID, nil) 3517 if err != nil { 3518 t.Fatalf("error finding redemption: %v", err) 3519 } 3520 if !bytes.Equal(checkSecret, secret) { 3521 t.Fatalf("wrong secret. expected %x, got %x", secret, checkSecret) 3522 } 3523 3524 // gettransaction error 3525 node.getTransactionErr = tErr 3526 _, _, err = wallet.FindRedemption(tCtx, coinID, nil) 3527 if err == nil { 3528 t.Fatalf("no error for gettransaction rpc error") 3529 } 3530 node.getTransactionErr = nil 3531 3532 // timeout finding missing redemption 3533 redeemVin.PreviousOutPoint.Hash = *otherTxHash 3534 delete(node.getCFilterScripts, *redeemBlockHash) 3535 timedCtx, cancel := context.WithTimeout(tCtx, 500*time.Millisecond) // 0.5 seconds is long enough 3536 defer cancel() 3537 _, k, err := wallet.FindRedemption(timedCtx, coinID, nil) 3538 if timedCtx.Err() == nil || k != nil { 3539 // Expected ctx to cancel after timeout and no secret should be found. 3540 t.Fatalf("unexpected result for missing redemption: secret: %v, err: %v", k, err) 3541 } 3542 3543 node.blockchainMtx.Lock() 3544 redeemVin.PreviousOutPoint.Hash = contractTxHash 3545 node.getCFilterScripts[*redeemBlockHash] = [][]byte{pkScript} 3546 node.blockchainMtx.Unlock() 3547 3548 // Canceled context 3549 deadCtx, cancelCtx := context.WithCancel(tCtx) 3550 cancelCtx() 3551 _, _, err = wallet.FindRedemption(deadCtx, coinID, nil) 3552 if err == nil { 3553 t.Fatalf("no error for canceled context") 3554 } 3555 3556 // Expect FindRedemption to error because of bad input sig. 3557 node.blockchainMtx.Lock() 3558 redeemVin.Witness = [][]byte{randBytes(100)} 3559 redeemVin.SignatureScript = randBytes(100) 3560 3561 node.blockchainMtx.Unlock() 3562 _, _, err = wallet.FindRedemption(tCtx, coinID, nil) 3563 if err == nil { 3564 t.Fatalf("no error for wrong redemption") 3565 } 3566 node.blockchainMtx.Lock() 3567 redeemVin.Witness = redemptionWitness 3568 redeemVin.SignatureScript = redemptionSigScript 3569 node.blockchainMtx.Unlock() 3570 3571 // Sanity check to make sure it passes again. 3572 _, _, err = wallet.FindRedemption(tCtx, coinID, nil) 3573 if err != nil { 3574 t.Fatalf("error after clearing errors: %v", err) 3575 } 3576 } 3577 3578 func TestRefund(t *testing.T) { 3579 runRubric(t, testRefund) 3580 } 3581 3582 func testRefund(t *testing.T, segwit bool, walletType string) { 3583 wallet, node, shutdown := tNewWallet(segwit, walletType) 3584 defer shutdown() 3585 3586 _, _, pkScript, contract, addr, _, _ := makeSwapContract(segwit, time.Hour*12) 3587 3588 bigTxOut := newTxOutResult(nil, 1e8, 2) 3589 node.txOutRes = bigTxOut // rpc 3590 node.changeAddr = addr.String() 3591 node.newAddress = addr.String() 3592 const feeSuggestion = 100 3593 3594 privBytes, _ := hex.DecodeString("b07209eec1a8fb6cfe5cb6ace36567406971a75c330db7101fb21bc679bc5330") 3595 privKey, _ := btcec.PrivKeyFromBytes(privBytes) 3596 wif, err := btcutil.NewWIF(privKey, &chaincfg.MainNetParams, true) 3597 if err != nil { 3598 t.Fatalf("error encoding wif: %v", err) 3599 } 3600 node.privKeyForAddr = wif 3601 3602 tx := makeRawTx([]dex.Bytes{pkScript}, []*wire.TxIn{dummyInput()}) 3603 const vout = 0 3604 tx.TxOut[vout].Value = 1e8 3605 txHash := tx.TxHash() 3606 outPt := NewOutPoint(&txHash, vout) 3607 blockHash, _ := node.addRawTx(1, tx) 3608 node.getCFilterScripts[*blockHash] = [][]byte{pkScript} 3609 node.getTransactionErr = WalletTransactionNotFound 3610 3611 contractOutput := NewOutput(&txHash, 0, 1e8) 3612 _, err = wallet.Refund(contractOutput.ID(), contract, feeSuggestion) 3613 if err != nil { 3614 t.Fatalf("refund error: %v", err) 3615 } 3616 3617 // Invalid coin 3618 badReceipt := &tReceipt{ 3619 coin: &tCoin{id: make([]byte, 15)}, 3620 } 3621 _, err = wallet.Refund(badReceipt.coin.id, badReceipt.Contract(), feeSuggestion) 3622 if err == nil { 3623 t.Fatalf("no error for bad receipt") 3624 } 3625 3626 ensureErr := func(tag string) { 3627 delete(node.checkpoints, outPt) 3628 _, err = wallet.Refund(contractOutput.ID(), contract, feeSuggestion) 3629 if err == nil { 3630 t.Fatalf("no error for %q", tag) 3631 } 3632 } 3633 3634 // gettxout error 3635 node.txOutErr = tErr 3636 node.getCFilterScripts[*blockHash] = nil 3637 ensureErr("no utxo") 3638 node.getCFilterScripts[*blockHash] = [][]byte{pkScript} 3639 node.txOutErr = nil 3640 3641 // bad contract 3642 badContractOutput := NewOutput(tTxHash, 0, 1e8) 3643 badContract := randBytes(50) 3644 _, err = wallet.Refund(badContractOutput.ID(), badContract, feeSuggestion) 3645 if err == nil { 3646 t.Fatalf("no error for bad contract") 3647 } 3648 3649 // Too small. 3650 node.txOutRes = newTxOutResult(nil, 100, 2) 3651 tx.TxOut[0].Value = 2 3652 ensureErr("value < fees") 3653 node.txOutRes = bigTxOut 3654 tx.TxOut[0].Value = 1e8 3655 3656 // getrawchangeaddress error 3657 node.newAddressErr = tErr 3658 ensureErr("new address error") 3659 node.newAddressErr = nil 3660 3661 // signature error 3662 node.privKeyForAddrErr = tErr 3663 ensureErr("dumpprivkey error") 3664 node.privKeyForAddrErr = nil 3665 3666 // send error 3667 node.sendErr = tErr 3668 ensureErr("send error") 3669 node.sendErr = nil 3670 3671 // bad checkhash 3672 var badHash chainhash.Hash 3673 badHash[0] = 0x05 3674 node.badSendHash = &badHash 3675 ensureErr("checkhash error") 3676 node.badSendHash = nil 3677 3678 // Sanity check that we can succeed again. 3679 _, err = wallet.Refund(contractOutput.ID(), contract, feeSuggestion) 3680 if err != nil { 3681 t.Fatalf("re-refund error: %v", err) 3682 } 3683 3684 // TODO test spv spent 3685 } 3686 3687 func TestLockUnlock(t *testing.T) { 3688 runRubric(t, testLockUnlock) 3689 } 3690 3691 func testLockUnlock(t *testing.T, segwit bool, walletType string) { 3692 w, node, shutdown := tNewWallet(segwit, walletType) 3693 defer shutdown() 3694 3695 pw := []byte("pass") 3696 wallet := &ExchangeWalletFullNode{w, &authAddOn{w.node}} 3697 3698 // just checking that the errors come through. 3699 err := wallet.Unlock(pw) 3700 if err != nil { 3701 t.Fatalf("unlock error: %v", err) 3702 } 3703 node.unlockErr = tErr 3704 err = wallet.Unlock(pw) 3705 if err == nil { 3706 t.Fatalf("no error for walletpassphrase error") 3707 } 3708 3709 // Locking can't error on SPV. 3710 if walletType == walletTypeRPC { 3711 err = wallet.Lock() 3712 if err != nil { 3713 t.Fatalf("lock error: %v", err) 3714 } 3715 node.lockErr = tErr 3716 err = wallet.Lock() 3717 if err == nil { 3718 t.Fatalf("no error for walletlock rpc error") 3719 } 3720 } 3721 } 3722 3723 type tSenderType byte 3724 3725 const ( 3726 tSendSender tSenderType = iota 3727 tWithdrawSender 3728 ) 3729 3730 func testSender(t *testing.T, senderType tSenderType, segwit bool, walletType string) { 3731 wallet, node, shutdown := tNewWallet(segwit, walletType) 3732 defer shutdown() 3733 const feeSuggestion = 100 3734 sender := func(addr string, val uint64) (asset.Coin, error) { 3735 return wallet.Send(addr, val, defaultFee) 3736 } 3737 if senderType == tWithdrawSender { 3738 sender = func(addr string, val uint64) (asset.Coin, error) { 3739 return wallet.Withdraw(addr, val, feeSuggestion) 3740 } 3741 } 3742 3743 node.signFunc = func(tx *wire.MsgTx) { 3744 signFunc(tx, 0, wallet.segwit) 3745 } 3746 3747 addr := btcAddr(segwit) 3748 node.setTxFee = true 3749 node.changeAddr = btcAddr(segwit).String() 3750 3751 pkScript, _ := txscript.PayToAddrScript(addr) 3752 tx := makeRawTx([]dex.Bytes{randBytes(5), pkScript}, []*wire.TxIn{dummyInput()}) 3753 txHash := tx.TxHash() 3754 3755 changeAddr := btcAddr(segwit) 3756 node.changeAddr = changeAddr.String() 3757 3758 expectedFees := func(numInputs int) uint64 { 3759 txSize := dexbtc.MinimumTxOverhead 3760 if segwit { 3761 txSize += (dexbtc.P2WPKHOutputSize * 2) + (numInputs * dexbtc.RedeemP2WPKHInputTotalSize) 3762 } else { 3763 txSize += (dexbtc.P2PKHOutputSize * 2) + (numInputs * dexbtc.RedeemP2PKHInputSize) 3764 } 3765 return uint64(txSize * feeSuggestion) 3766 } 3767 3768 expectedSentVal := func(sendVal, fees uint64) uint64 { 3769 if senderType == tSendSender { 3770 return sendVal 3771 } 3772 return sendVal - fees 3773 } 3774 3775 expectedChangeVal := func(totalInput, sendVal, fees uint64) uint64 { 3776 if senderType == tSendSender { 3777 return totalInput - sendVal - fees 3778 } 3779 return totalInput - sendVal 3780 } 3781 3782 requiredVal := func(sendVal, fees uint64) uint64 { 3783 if senderType == tSendSender { 3784 return sendVal + fees 3785 } 3786 return sendVal 3787 } 3788 3789 type test struct { 3790 name string 3791 val uint64 3792 unspents []*ListUnspentResult 3793 bondReserves uint64 3794 3795 expectedInputs []*OutPoint 3796 expectSentVal uint64 3797 expectChange uint64 3798 expectErr bool 3799 } 3800 tests := []test{ 3801 { 3802 name: "plenty of funds", 3803 val: toSatoshi(5), 3804 unspents: []*ListUnspentResult{{ 3805 TxID: txHash.String(), 3806 Address: addr.String(), 3807 Amount: 100, 3808 Confirmations: 1, 3809 Vout: 0, 3810 ScriptPubKey: pkScript, 3811 SafePtr: boolPtr(true), 3812 Spendable: true, 3813 }}, 3814 expectedInputs: []*OutPoint{ 3815 {TxHash: txHash, Vout: 0}, 3816 }, 3817 expectSentVal: expectedSentVal(toSatoshi(5), expectedFees(1)), 3818 expectChange: expectedChangeVal(toSatoshi(100), toSatoshi(5), expectedFees(1)), 3819 }, 3820 { 3821 name: "just enough change for bond reserves", 3822 val: toSatoshi(5), 3823 unspents: []*ListUnspentResult{{ 3824 TxID: txHash.String(), 3825 Address: addr.String(), 3826 Amount: 5.2, 3827 Confirmations: 1, 3828 Vout: 0, 3829 ScriptPubKey: pkScript, 3830 SafePtr: boolPtr(true), 3831 Spendable: true, 3832 }}, 3833 expectedInputs: []*OutPoint{ 3834 {TxHash: txHash, 3835 Vout: 0}, 3836 }, 3837 expectSentVal: expectedSentVal(toSatoshi(5), expectedFees(1)), 3838 expectChange: expectedChangeVal(toSatoshi(5.2), toSatoshi(5), expectedFees(1)), 3839 bondReserves: expectedChangeVal(toSatoshi(5.2), toSatoshi(5), expectedFees(1)), 3840 }, 3841 { 3842 name: "not enough change for bond reserves", 3843 val: toSatoshi(5), 3844 unspents: []*ListUnspentResult{{ 3845 TxID: txHash.String(), 3846 Address: addr.String(), 3847 Amount: 5.2, 3848 Confirmations: 1, 3849 Vout: 0, 3850 ScriptPubKey: pkScript, 3851 SafePtr: boolPtr(true), 3852 Spendable: true, 3853 }}, 3854 expectedInputs: []*OutPoint{ 3855 {TxHash: txHash, Vout: 0}, 3856 }, 3857 bondReserves: expectedChangeVal(toSatoshi(5.2), toSatoshi(5), expectedFees(1)) + 1, 3858 expectErr: true, 3859 }, 3860 { 3861 name: "1 satoshi less than needed", 3862 val: toSatoshi(5), 3863 unspents: []*ListUnspentResult{{ 3864 TxID: txHash.String(), 3865 Address: addr.String(), 3866 Amount: toBTC(requiredVal(toSatoshi(5), expectedFees(1)) - 1), 3867 Confirmations: 1, 3868 Vout: 0, 3869 ScriptPubKey: pkScript, 3870 SafePtr: boolPtr(true), 3871 Spendable: true, 3872 }}, 3873 expectErr: true, 3874 }, 3875 { 3876 name: "exact amount needed", 3877 val: toSatoshi(5), 3878 unspents: []*ListUnspentResult{{ 3879 TxID: txHash.String(), 3880 Address: addr.String(), 3881 Amount: toBTC(requiredVal(toSatoshi(5), expectedFees(1))), 3882 Confirmations: 1, 3883 Vout: 0, 3884 ScriptPubKey: pkScript, 3885 SafePtr: boolPtr(true), 3886 Spendable: true, 3887 }}, 3888 expectedInputs: []*OutPoint{ 3889 {TxHash: txHash, Vout: 0}, 3890 }, 3891 expectSentVal: expectedSentVal(toSatoshi(5), expectedFees(1)), 3892 expectChange: 0, 3893 }, 3894 } 3895 3896 for _, test := range tests { 3897 node.listUnspent = test.unspents 3898 wallet.bondReserves.Store(test.bondReserves) 3899 3900 _, err := sender(addr.String(), test.val) 3901 if test.expectErr { 3902 if err == nil { 3903 t.Fatalf("%s: no error for expected error", test.name) 3904 } 3905 continue 3906 } 3907 if err != nil { 3908 t.Fatalf("%s: unexpected error: %v", test.name, err) 3909 } 3910 3911 tx := node.sentRawTx 3912 if len(test.expectedInputs) != len(tx.TxIn) { 3913 t.Fatalf("expected %d inputs, got %d", len(test.expectedInputs), len(tx.TxIn)) 3914 } 3915 3916 for i, input := range tx.TxIn { 3917 if input.PreviousOutPoint.Hash != test.expectedInputs[i].TxHash || 3918 input.PreviousOutPoint.Index != test.expectedInputs[i].Vout { 3919 t.Fatalf("expected input %d to be %v, got %v", i, test.expectedInputs[i], input.PreviousOutPoint) 3920 } 3921 } 3922 3923 if test.expectChange > 0 && len(tx.TxOut) != 2 { 3924 t.Fatalf("expected 2 outputs, got %d", len(tx.TxOut)) 3925 } 3926 if test.expectChange == 0 && len(tx.TxOut) != 1 { 3927 t.Fatalf("expected 2 outputs, got %d", len(tx.TxOut)) 3928 } 3929 3930 if tx.TxOut[0].Value != int64(test.expectSentVal) { 3931 t.Fatalf("expected sent value to be %d, got %d", test.expectSentVal, tx.TxOut[0].Value) 3932 } 3933 3934 if test.expectChange > 0 && tx.TxOut[1].Value != int64(test.expectChange) { 3935 t.Fatalf("expected change value to be %d, got %d", test.expectChange, tx.TxOut[1].Value) 3936 } 3937 } 3938 } 3939 3940 func TestEstimateSendTxFee(t *testing.T) { 3941 runRubric(t, testEstimateSendTxFee) 3942 } 3943 3944 func testEstimateSendTxFee(t *testing.T, segwit bool, walletType string) { 3945 wallet, node, shutdown := tNewWallet(segwit, walletType) 3946 defer shutdown() 3947 3948 addr := btcAddr(segwit) 3949 sendAddr := addr.String() 3950 node.changeAddr = btcAddr(segwit).String() 3951 pkScript, _ := txscript.PayToAddrScript(addr) 3952 3953 unspentVal := 100 // BTC 3954 unspents := []*ListUnspentResult{{ 3955 Address: addr.String(), 3956 Amount: float64(unspentVal), 3957 Confirmations: 1, 3958 Vout: 1, 3959 ScriptPubKey: pkScript, 3960 SafePtr: boolPtr(true), 3961 Spendable: true, 3962 }} 3963 node.listUnspent = unspents 3964 var bals GetBalancesResult 3965 node.getBalances = &bals 3966 bals.Mine.Trusted = float64(unspentVal) 3967 unspentSats := toSatoshi(unspents[0].Amount) 3968 3969 tx := wire.NewMsgTx(wire.TxVersion) 3970 tx.AddTxOut(wire.NewTxOut(int64(unspentSats), pkScript)) 3971 3972 // single ouptput size. 3973 opSize := dexbtc.P2PKHOutputSize 3974 if segwit { 3975 opSize = dexbtc.P2WPKHOutputSize 3976 } 3977 3978 // bSize is the size for a single input. 3979 witnessWeight := 4 3980 bSize := dexbtc.TxInOverhead + 3981 wire.VarIntSerializeSize(uint64(dexbtc.RedeemP2PKHSigScriptSize)) + 3982 dexbtc.RedeemP2PKHSigScriptSize + (witnessWeight-1)/witnessWeight 3983 if segwit { 3984 bSize = dexbtc.TxInOverhead + 1 + (dexbtc.RedeemP2WPKHInputWitnessWeight+ 3985 (witnessWeight-1))/witnessWeight 3986 } 3987 3988 txSize := bSize + tx.SerializeSize() 3989 minEstFee := optimalFeeRate * uint64(txSize) 3990 3991 // This should return fee estimate for one output. 3992 node.txFee = minEstFee 3993 3994 estimate, _, err := wallet.EstimateSendTxFee(sendAddr, unspentSats, optimalFeeRate, true, false) 3995 if err != nil { 3996 t.Fatal(err) 3997 } 3998 if estimate != minEstFee { 3999 t.Fatalf("expected estimate to be %v, got %v)", minEstFee, estimate) 4000 } 4001 4002 // This should return fee estimate for two output. 4003 minEstFeeWithEstChangeFee := uint64(txSize+opSize) * optimalFeeRate 4004 node.txFee = minEstFeeWithEstChangeFee 4005 estimate, _, err = wallet.EstimateSendTxFee(sendAddr, unspentSats/2, optimalFeeRate, false, false) 4006 if err != nil { 4007 t.Fatal(err) 4008 } 4009 if estimate != minEstFeeWithEstChangeFee { 4010 t.Fatalf("expected estimate to be %v, got %v)", minEstFeeWithEstChangeFee, estimate) 4011 } 4012 4013 dust := 0.00000016 4014 node.listUnspent[0].Amount += dust 4015 // This should return fee estimate for one output with dust added to fee. 4016 minFeeWithDust := minEstFee + toSatoshi(dust) 4017 node.txFee = minFeeWithDust 4018 estimate, _, err = wallet.EstimateSendTxFee(sendAddr, unspentSats, optimalFeeRate, true, false) 4019 if err != nil { 4020 t.Fatal(err) 4021 } 4022 if estimate != minFeeWithDust { 4023 t.Fatalf("expected estimate to be %v, got %v)", minFeeWithDust, estimate) 4024 } 4025 4026 // Invalid address 4027 _, valid, _ := wallet.EstimateSendTxFee("invalidsendAddr", unspentSats, optimalFeeRate, true, false) 4028 if valid { 4029 t.Fatal("Expected false for invalid send address") 4030 } 4031 4032 // Successful estimation without an address 4033 _, _, err = wallet.EstimateSendTxFee("", unspentSats, optimalFeeRate, true, false) 4034 if err != nil { 4035 t.Fatalf("Error for estimation without an address: %v", err) 4036 } 4037 4038 // Zero send amount 4039 _, _, err = wallet.EstimateSendTxFee(sendAddr, 0, optimalFeeRate, true, false) 4040 if err == nil { 4041 t.Fatal("Expected an error for zero send amount") 4042 } 4043 4044 // Output value is dust. 4045 _, _, err = wallet.EstimateSendTxFee(sendAddr, 500, optimalFeeRate, true, false) 4046 if err == nil { 4047 t.Fatal("Expected an error for dust output") 4048 } 4049 } 4050 4051 func TestWithdraw(t *testing.T) { 4052 runRubric(t, func(t *testing.T, segwit bool, walletType string) { 4053 testSender(t, tWithdrawSender, segwit, walletType) 4054 }) 4055 } 4056 4057 func TestSend(t *testing.T) { 4058 runRubric(t, func(t *testing.T, segwit bool, walletType string) { 4059 testSender(t, tSendSender, segwit, walletType) 4060 }) 4061 } 4062 4063 func TestConfirmations(t *testing.T) { 4064 runRubric(t, testConfirmations) 4065 } 4066 4067 func testConfirmations(t *testing.T, segwit bool, walletType string) { 4068 wallet, node, shutdown := tNewWallet(segwit, walletType) 4069 defer shutdown() 4070 4071 // coinID := make([]byte, 36) 4072 // copy(coinID[:32], tTxHash[:]) 4073 4074 _, _, pkScript, contract, _, _, _ := makeSwapContract(segwit, time.Hour*12) 4075 const tipHeight = 10 4076 const swapHeight = 2 4077 const spendHeight = 4 4078 const expConfs = tipHeight - swapHeight + 1 4079 4080 tx := makeRawTx([]dex.Bytes{pkScript}, []*wire.TxIn{dummyInput()}) 4081 blockHash, swapBlock := node.addRawTx(swapHeight, tx) 4082 txHash := tx.TxHash() 4083 coinID := ToCoinID(&txHash, 0) 4084 // Simulate a spending transaction, and advance the tip so that the swap 4085 // has two confirmations. 4086 spendingTx := dummyTx() 4087 spendingTx.TxIn[0].PreviousOutPoint.Hash = txHash 4088 spendingBlockHash, _ := node.addRawTx(spendHeight, spendingTx) 4089 4090 // Prime the blockchain 4091 for i := int64(1); i <= tipHeight; i++ { 4092 node.addRawTx(i, dummyTx()) 4093 } 4094 4095 matchTime := swapBlock.Header.Timestamp 4096 4097 // Bad coin id 4098 _, _, err := wallet.SwapConfirmations(context.Background(), randBytes(35), contract, matchTime) 4099 if err == nil { 4100 t.Fatalf("no error for bad coin ID") 4101 } 4102 4103 // Short path. 4104 txOutRes := &btcjson.GetTxOutResult{ 4105 Confirmations: expConfs, 4106 BestBlock: blockHash.String(), 4107 } 4108 node.txOutRes = txOutRes 4109 node.getCFilterScripts[*blockHash] = [][]byte{pkScript} 4110 confs, _, err := wallet.SwapConfirmations(context.Background(), coinID, contract, matchTime) 4111 if err != nil { 4112 t.Fatalf("error for gettransaction path: %v", err) 4113 } 4114 if confs != expConfs { 4115 t.Fatalf("confs not retrieved from gettxout path. expected %d, got %d", expConfs, confs) 4116 } 4117 4118 // no tx output found 4119 node.txOutRes = nil 4120 node.getCFilterScripts[*blockHash] = nil 4121 node.getTransactionErr = tErr 4122 _, _, err = wallet.SwapConfirmations(context.Background(), coinID, contract, matchTime) 4123 if err == nil { 4124 t.Fatalf("no error for gettransaction error") 4125 } 4126 node.getCFilterScripts[*blockHash] = [][]byte{pkScript} 4127 node.getTransactionErr = nil 4128 txB, _ := serializeMsgTx(tx) 4129 4130 node.getTransactionMap = map[string]*GetTransactionResult{ 4131 "any": { 4132 BlockHash: blockHash.String(), 4133 Bytes: txB, 4134 }} 4135 4136 node.getCFilterScripts[*spendingBlockHash] = [][]byte{pkScript} 4137 node.walletTxSpent = true 4138 _, spent, err := wallet.SwapConfirmations(context.Background(), coinID, contract, matchTime) 4139 if err != nil { 4140 t.Fatalf("error for spent swap: %v", err) 4141 } 4142 if !spent { 4143 t.Fatalf("swap not spent") 4144 } 4145 } 4146 4147 func TestSendEdges(t *testing.T) { 4148 runRubric(t, testSendEdges) 4149 } 4150 4151 func testSendEdges(t *testing.T, segwit bool, walletType string) { 4152 wallet, node, shutdown := tNewWallet(segwit, walletType) 4153 defer shutdown() 4154 4155 const feeRate uint64 = 3 4156 4157 const swapVal = 2e8 // leaving untyped. NewTxOut wants int64 4158 4159 var addr, contractAddr btcutil.Address 4160 var dexReqFees, dustCoverage uint64 4161 4162 addr = btcAddr(segwit) 4163 4164 if segwit { 4165 contractAddr, _ = btcutil.NewAddressWitnessScriptHash(randBytes(32), &chaincfg.MainNetParams) 4166 // See dexbtc.IsDust for the source of this dustCoverage voodoo. 4167 dustCoverage = (dexbtc.P2WPKHOutputSize + 41 + (107 / 4)) * feeRate * 3 4168 dexReqFees = dexbtc.InitTxSizeSegwit * feeRate 4169 } else { 4170 contractAddr, _ = btcutil.NewAddressScriptHash(randBytes(20), &chaincfg.MainNetParams) 4171 dustCoverage = (dexbtc.P2PKHOutputSize + 41 + 107) * feeRate * 3 4172 dexReqFees = dexbtc.InitTxSize * feeRate 4173 } 4174 4175 pkScript, _ := txscript.PayToAddrScript(contractAddr) 4176 4177 newBaseTx := func() *wire.MsgTx { 4178 baseTx := wire.NewMsgTx(wire.TxVersion) 4179 baseTx.AddTxIn(wire.NewTxIn(new(wire.OutPoint), nil, nil)) 4180 baseTx.AddTxOut(wire.NewTxOut(swapVal, pkScript)) 4181 return baseTx 4182 } 4183 4184 node.signFunc = func(tx *wire.MsgTx) { 4185 signFunc(tx, 0, wallet.segwit) 4186 } 4187 4188 tests := []struct { 4189 name string 4190 funding uint64 4191 expChange bool 4192 }{ 4193 { 4194 name: "not enough for change output", 4195 funding: swapVal + dexReqFees - 1, 4196 }, 4197 { 4198 // Still dust here, but a different path. 4199 name: "exactly enough for change output", 4200 funding: swapVal + dexReqFees, 4201 }, 4202 { 4203 name: "more than enough for change output but still dust", 4204 funding: swapVal + dexReqFees + 1, 4205 }, 4206 { 4207 name: "1 atom short to not be dust", 4208 funding: swapVal + dexReqFees + dustCoverage - 1, 4209 }, 4210 { 4211 name: "exactly enough to not be dust", 4212 funding: swapVal + dexReqFees + dustCoverage, 4213 expChange: true, 4214 }, 4215 } 4216 4217 for _, tt := range tests { 4218 tx, err := wallet.sendWithReturn(newBaseTx(), addr, tt.funding, swapVal, feeRate) 4219 if err != nil { 4220 t.Fatalf("sendWithReturn error: %v", err) 4221 } 4222 4223 if len(tx.TxOut) == 1 && tt.expChange { 4224 t.Fatalf("%s: no change added", tt.name) 4225 } else if len(tx.TxOut) == 2 && !tt.expChange { 4226 t.Fatalf("%s: change output added for dust. Output value = %d", tt.name, tx.TxOut[1].Value) 4227 } 4228 } 4229 } 4230 4231 func TestSyncStatus(t *testing.T) { 4232 runRubric(t, testSyncStatus) 4233 } 4234 4235 func testSyncStatus(t *testing.T, segwit bool, walletType string) { 4236 wallet, node, shutdown := tNewWallet(segwit, walletType) 4237 defer shutdown() 4238 4239 // full node 4240 node.getBlockchainInfo = &GetBlockchainInfoResult{ 4241 Headers: 100, 4242 Blocks: 99, // full node allowed to be synced when 1 block behind 4243 } 4244 4245 // spv 4246 blkHash, msgBlock := node.addRawTx(100, dummyTx()) 4247 node.birthdayTime = msgBlock.Header.Timestamp.Add(-time.Minute) // SPV, wallet birthday is passed 4248 node.mainchain[100] = blkHash // SPV, actually has to reach target 4249 4250 ss, err := wallet.SyncStatus() 4251 if err != nil { 4252 t.Fatalf("SyncStatus error (synced expected): %v", err) 4253 } 4254 if !ss.Synced { 4255 t.Fatalf("synced = false") 4256 } 4257 if ss.BlockProgress() < 1 { 4258 t.Fatalf("progress not complete when loading last block") 4259 } 4260 4261 node.getBlockchainInfoErr = tErr // rpc 4262 node.blockchainMtx.Lock() 4263 node.getBestBlockHashErr = tErr // spv BestBlock() 4264 node.blockchainMtx.Unlock() 4265 delete(node.mainchain, 100) // force spv to BestBlock() with no wallet block 4266 _, err = wallet.SyncStatus() 4267 if err == nil { 4268 t.Fatalf("SyncStatus error not propagated") 4269 } 4270 node.getBlockchainInfoErr = nil 4271 node.blockchainMtx.Lock() 4272 node.getBestBlockHashErr = nil 4273 node.blockchainMtx.Unlock() 4274 4275 wallet.tipAtConnect = 100 4276 node.getBlockchainInfo = &GetBlockchainInfoResult{ 4277 Headers: 200, 4278 Blocks: 150, 4279 } 4280 node.addRawTx(150, makeRawTx([]dex.Bytes{randBytes(1)}, []*wire.TxIn{dummyInput()})) // spv needs this for BestBlock 4281 ss, err = wallet.SyncStatus() 4282 if err != nil { 4283 t.Fatalf("SyncStatus error (half-synced): %v", err) 4284 } 4285 if ss.Synced { 4286 t.Fatalf("synced = true for 50 blocks to go") 4287 } 4288 if ss.BlockProgress() > 0.500001 || ss.BlockProgress() < 0.4999999 { 4289 t.Fatalf("progress out of range. Expected 0.5, got %.2f", ss.BlockProgress()) 4290 } 4291 } 4292 4293 func TestPreSwap(t *testing.T) { 4294 runRubric(t, testPreSwap) 4295 } 4296 4297 func testPreSwap(t *testing.T, segwit bool, walletType string) { 4298 wallet, node, shutdown := tNewWallet(segwit, walletType) 4299 defer shutdown() 4300 4301 // See math from TestFundEdges. 10 lots with max fee rate of 34 sats/vbyte. 4302 4303 swapVal := uint64(1e7) 4304 lots := swapVal / tLotSize // 10 lots 4305 4306 // var swapSize = 225 4307 var totalBytes uint64 = 2250 4308 var bestCaseBytes uint64 = 225 4309 pkScript := tP2PKH 4310 if segwit { 4311 // swapSize = 153 4312 totalBytes = 1530 4313 bestCaseBytes = 153 4314 pkScript = tP2WPKH 4315 } 4316 4317 backingFees := totalBytes * tBTC.MaxFeeRate // total_bytes * fee_rate 4318 4319 minReq := swapVal + backingFees 4320 4321 unspent := &ListUnspentResult{ 4322 TxID: tTxID, 4323 Address: tP2PKHAddr, 4324 Confirmations: 5, 4325 ScriptPubKey: pkScript, 4326 Spendable: true, 4327 Solvable: true, 4328 SafePtr: boolPtr(true), 4329 } 4330 unspents := []*ListUnspentResult{unspent} 4331 4332 setFunds := func(v uint64) { 4333 unspent.Amount = float64(v) / 1e8 4334 node.listUnspent = unspents 4335 } 4336 4337 form := &asset.PreSwapForm{ 4338 Version: version, 4339 LotSize: tLotSize, 4340 Lots: lots, 4341 MaxFeeRate: tBTC.MaxFeeRate, 4342 Immediate: false, 4343 FeeSuggestion: feeSuggestion, 4344 // Redeem fields unneeded 4345 } 4346 4347 setFunds(minReq) 4348 4349 // Initial success. 4350 preSwap, err := wallet.PreSwap(form) 4351 if err != nil { 4352 t.Fatalf("PreSwap error: %v", err) 4353 } 4354 4355 maxFees := totalBytes * tBTC.MaxFeeRate 4356 estHighFees := totalBytes * feeSuggestion 4357 estLowFees := bestCaseBytes * feeSuggestion 4358 checkSwapEstimate(t, preSwap.Estimate, lots, swapVal, maxFees, estHighFees, estLowFees) 4359 4360 // Too little funding is an error. 4361 setFunds(minReq - 1) 4362 _, err = wallet.PreSwap(form) 4363 if err == nil { 4364 t.Fatalf("no PreSwap error for not enough funds") 4365 } 4366 setFunds(minReq) 4367 4368 // Success again. 4369 _, err = wallet.PreSwap(form) 4370 if err != nil { 4371 t.Fatalf("PreSwap error: %v", err) 4372 } 4373 } 4374 4375 func TestPreRedeem(t *testing.T) { 4376 runRubric(t, testPreRedeem) 4377 } 4378 4379 func testPreRedeem(t *testing.T, segwit bool, walletType string) { 4380 wallet, _, shutdown := tNewWallet(segwit, walletType) 4381 defer shutdown() 4382 4383 preRedeem, err := wallet.PreRedeem(&asset.PreRedeemForm{ 4384 Version: version, 4385 Lots: 5, 4386 }) 4387 // Shouldn't actually be any path to error. 4388 if err != nil { 4389 t.Fatalf("PreRedeem non-segwit error: %v", err) 4390 } 4391 4392 // Just a couple of sanity checks. 4393 if preRedeem.Estimate.RealisticBestCase >= preRedeem.Estimate.RealisticWorstCase { 4394 t.Fatalf("best case > worst case") 4395 } 4396 } 4397 4398 func TestTryRedemptionRequests(t *testing.T) { 4399 // runRubric(t, testTryRedemptionRequests) 4400 testTryRedemptionRequests(t, true, walletTypeSPV) 4401 } 4402 4403 func testTryRedemptionRequests(t *testing.T, segwit bool, walletType string) { 4404 wallet, node, shutdown := tNewWallet(segwit, walletType) 4405 defer shutdown() 4406 4407 const swapVout = 1 4408 4409 randHash := func() *chainhash.Hash { 4410 var h chainhash.Hash 4411 copy(h[:], randBytes(32)) 4412 return &h 4413 } 4414 4415 otherScript, _ := txscript.PayToAddrScript(btcAddr(segwit)) 4416 otherInput := []*wire.TxIn{makeRPCVin(randHash(), 0, randBytes(5), nil)} 4417 otherTx := func() *wire.MsgTx { 4418 return makeRawTx([]dex.Bytes{otherScript}, otherInput) 4419 } 4420 4421 addBlocks := func(n int) { 4422 var h int64 4423 // Make dummy transactions. 4424 for i := 0; i < n; i++ { 4425 node.addRawTx(h, otherTx()) 4426 h++ 4427 } 4428 } 4429 4430 getTx := func(blockHeight int64, txIdx int) (*wire.MsgTx, *chainhash.Hash) { 4431 if blockHeight == -1 { 4432 // mempool 4433 txHash := randHash() 4434 tx := otherTx() 4435 node.mempoolTxs[*txHash] = tx 4436 return tx, nil 4437 } 4438 blockHash, blk := node.getBlockAtHeight(blockHeight) 4439 for len(blk.msgBlock.Transactions) <= txIdx { 4440 blk.msgBlock.Transactions = append(blk.msgBlock.Transactions, otherTx()) 4441 } 4442 return blk.msgBlock.Transactions[txIdx], blockHash 4443 } 4444 4445 type tRedeem struct { 4446 redeemTxIdx, redeemVin int 4447 swapHeight, redeemBlockHeight int64 4448 notRedeemed bool 4449 } 4450 4451 redeemReq := func(r *tRedeem) *FindRedemptionReq { 4452 var swapBlockHash *chainhash.Hash 4453 var swapHeight int64 4454 if r.swapHeight >= 0 { 4455 swapHeight = r.swapHeight 4456 swapBlockHash, _ = node.getBlockAtHeight(swapHeight) 4457 } 4458 4459 swapTxHash := randHash() 4460 secret, _, pkScript, contract, _, _, _ := makeSwapContract(segwit, time.Hour*12) 4461 4462 if !r.notRedeemed { 4463 redeemTx, redeemBlockHash := getTx(r.redeemBlockHeight, r.redeemTxIdx) 4464 4465 // redemptionSigScript, _ := dexbtc.RedeemP2SHContract(contract, randBytes(73), randBytes(33), secret) 4466 for len(redeemTx.TxIn) < r.redeemVin { 4467 redeemTx.TxIn = append(redeemTx.TxIn, makeRPCVin(randHash(), 0, nil, nil)) 4468 } 4469 4470 var redemptionSigScript []byte 4471 var redemptionWitness [][]byte 4472 if segwit { 4473 redemptionWitness = dexbtc.RedeemP2WSHContract(contract, randBytes(73), randBytes(33), secret) 4474 } else { 4475 redemptionSigScript, _ = dexbtc.RedeemP2SHContract(contract, randBytes(73), randBytes(33), secret) 4476 } 4477 4478 redeemTx.TxIn = append(redeemTx.TxIn, makeRPCVin(swapTxHash, swapVout, redemptionSigScript, redemptionWitness)) 4479 if redeemBlockHash != nil { 4480 node.getCFilterScripts[*redeemBlockHash] = [][]byte{pkScript} 4481 } 4482 } 4483 4484 req := &FindRedemptionReq{ 4485 outPt: NewOutPoint(swapTxHash, swapVout), 4486 blockHash: swapBlockHash, 4487 blockHeight: int32(swapHeight), 4488 resultChan: make(chan *FindRedemptionResult, 1), 4489 pkScript: pkScript, 4490 contractHash: hashContract(segwit, contract), 4491 } 4492 wallet.rf.redemptions[req.outPt] = req 4493 return req 4494 } 4495 4496 type test struct { 4497 numBlocks int 4498 startBlockHeight int64 4499 redeems []*tRedeem 4500 forcedErr bool 4501 canceledCtx bool 4502 } 4503 4504 isMempoolTest := func(tt *test) bool { 4505 for _, r := range tt.redeems { 4506 if r.redeemBlockHeight == -1 { 4507 return true 4508 } 4509 } 4510 return false 4511 } 4512 4513 tests := []*test{ 4514 { // Normal redemption 4515 numBlocks: 2, 4516 redeems: []*tRedeem{{ 4517 redeemBlockHeight: 1, 4518 redeemTxIdx: 1, 4519 redeemVin: 1, 4520 }}, 4521 }, 4522 { // Mempool redemption 4523 numBlocks: 2, 4524 redeems: []*tRedeem{{ 4525 redeemBlockHeight: -1, 4526 redeemTxIdx: 2, 4527 redeemVin: 2, 4528 }}, 4529 }, 4530 { // A couple of redemptions, both in tip. 4531 numBlocks: 3, 4532 startBlockHeight: 1, 4533 redeems: []*tRedeem{{ 4534 redeemBlockHeight: 2, 4535 }, { 4536 redeemBlockHeight: 2, 4537 }}, 4538 }, 4539 { // A couple of redemptions, spread apart. 4540 numBlocks: 6, 4541 redeems: []*tRedeem{{ 4542 redeemBlockHeight: 2, 4543 }, { 4544 redeemBlockHeight: 4, 4545 }}, 4546 }, 4547 { // nil start block 4548 numBlocks: 5, 4549 startBlockHeight: -1, 4550 redeems: []*tRedeem{{ 4551 swapHeight: 4, 4552 redeemBlockHeight: 4, 4553 }}, 4554 }, 4555 { // A mix of mined and mempool redeems 4556 numBlocks: 3, 4557 startBlockHeight: 1, 4558 redeems: []*tRedeem{{ 4559 redeemBlockHeight: -1, 4560 }, { 4561 redeemBlockHeight: 2, 4562 }}, 4563 }, 4564 { // One found, one not found. 4565 numBlocks: 4, 4566 startBlockHeight: 1, 4567 redeems: []*tRedeem{{ 4568 redeemBlockHeight: 2, 4569 }, { 4570 notRedeemed: true, 4571 }}, 4572 }, 4573 { // One found in mempool, one not found. 4574 numBlocks: 4, 4575 startBlockHeight: 1, 4576 redeems: []*tRedeem{{ 4577 redeemBlockHeight: -1, 4578 }, { 4579 notRedeemed: true, 4580 }}, 4581 }, 4582 { // Swap not mined. 4583 numBlocks: 3, 4584 startBlockHeight: 1, 4585 redeems: []*tRedeem{{ 4586 swapHeight: -1, 4587 redeemBlockHeight: -1, 4588 }}, 4589 }, 4590 { // Fatal error 4591 numBlocks: 2, 4592 forcedErr: true, 4593 redeems: []*tRedeem{{ 4594 redeemBlockHeight: 1, 4595 }}, 4596 }, 4597 { // Canceled context. 4598 numBlocks: 2, 4599 canceledCtx: true, 4600 redeems: []*tRedeem{{ 4601 redeemBlockHeight: 1, 4602 }}, 4603 }, 4604 } 4605 4606 for _, tt := range tests { 4607 // Skip tests where we're expected to see mempool in SPV. 4608 if walletType == walletTypeSPV && isMempoolTest(tt) { 4609 continue 4610 } 4611 4612 node.truncateChains() 4613 wallet.rf.redemptions = make(map[OutPoint]*FindRedemptionReq) 4614 node.blockchainMtx.Lock() 4615 node.getBestBlockHashErr = nil 4616 if tt.forcedErr { 4617 node.getBestBlockHashErr = tErr 4618 } 4619 node.blockchainMtx.Unlock() 4620 addBlocks(tt.numBlocks) 4621 var startBlock *chainhash.Hash 4622 if tt.startBlockHeight >= 0 { 4623 startBlock, _ = node.getBlockAtHeight(tt.startBlockHeight) 4624 } 4625 4626 ctx := tCtx 4627 if tt.canceledCtx { 4628 timedCtx, cancel := context.WithTimeout(tCtx, time.Second) 4629 ctx = timedCtx 4630 cancel() 4631 } 4632 4633 reqs := make([]*FindRedemptionReq, 0, len(tt.redeems)) 4634 for _, redeem := range tt.redeems { 4635 reqs = append(reqs, redeemReq(redeem)) 4636 } 4637 4638 wallet.rf.tryRedemptionRequests(ctx, startBlock, reqs) 4639 4640 for i, req := range reqs { 4641 select { 4642 case res := <-req.resultChan: 4643 if res.err != nil { 4644 if !tt.forcedErr { 4645 t.Fatalf("result error: %v", res.err) 4646 } 4647 } else if tt.canceledCtx { 4648 t.Fatalf("got success with canceled context") 4649 } 4650 default: 4651 redeem := tt.redeems[i] 4652 if !redeem.notRedeemed && !tt.canceledCtx { 4653 t.Fatalf("redemption not found") 4654 } 4655 } 4656 } 4657 } 4658 } 4659 4660 func TestPrettyBTC(t *testing.T) { 4661 type test struct { 4662 v uint64 4663 exp string 4664 } 4665 4666 tests := []*test{{ 4667 v: 1, 4668 exp: "0.00000001", 4669 }, { 4670 v: 1e8, 4671 exp: "1", 4672 }, { 4673 v: 100000001, 4674 exp: "1.00000001", 4675 }, { 4676 v: 0, 4677 exp: "0", 4678 }, { 4679 v: 123000, 4680 exp: "0.00123", 4681 }, { 4682 v: 100123000, 4683 exp: "1.00123", 4684 }} 4685 4686 for _, tt := range tests { 4687 if prettyBTC(tt.v) != tt.exp { 4688 t.Fatalf("prettyBTC(%d) = %s != %q", tt.v, prettyBTC(tt.v), tt.exp) 4689 } 4690 } 4691 } 4692 4693 // TestAccelerateOrder tests the entire acceleration workflow, including 4694 // AccelerateOrder, PreAccelerate, and AccelerationEstimate 4695 func TestAccelerateOrder(t *testing.T) { 4696 runRubric(t, testAccelerateOrder) 4697 } 4698 4699 func testAccelerateOrder(t *testing.T, segwit bool, walletType string) { 4700 w, node, shutdown := tNewWallet(segwit, walletType) 4701 defer shutdown() 4702 4703 wallet := &ExchangeWalletAccelerator{&ExchangeWalletFullNode{w, &authAddOn{w.node}}} 4704 4705 var blockHash100 chainhash.Hash 4706 copy(blockHash100[:], encode.RandomBytes(32)) 4707 node.verboseBlocks[blockHash100] = &msgBlockWithHeight{height: 100, msgBlock: &wire.MsgBlock{ 4708 Header: wire.BlockHeader{Timestamp: time.Now()}, 4709 }} 4710 node.mainchain[100] = &blockHash100 4711 4712 if segwit { 4713 node.changeAddr = tP2WPKHAddr 4714 node.newAddress = tP2WPKHAddr 4715 } else { 4716 node.changeAddr = tP2PKHAddr 4717 node.newAddress = tP2PKHAddr 4718 } 4719 4720 node.signFunc = func(tx *wire.MsgTx) { 4721 signFunc(tx, 0, wallet.segwit) 4722 } 4723 4724 sumFees := func(fees []float64, confs []uint64) uint64 { 4725 var totalFees uint64 4726 for i, fee := range fees { 4727 if confs[i] == 0 { 4728 totalFees += toSatoshi(fee) 4729 } 4730 } 4731 return totalFees 4732 } 4733 4734 sumTxSizes := func(txs []*wire.MsgTx, confirmations []uint64) uint64 { 4735 var totalSize uint64 4736 for i, tx := range txs { 4737 if confirmations[i] == 0 { 4738 totalSize += dexbtc.MsgTxVBytes(tx) 4739 } 4740 } 4741 4742 return totalSize 4743 } 4744 4745 loadTxsIntoNode := func(txs []*wire.MsgTx, fees []float64, confs []uint64, node *testData, withinTimeLimit bool, t *testing.T) { 4746 t.Helper() 4747 if len(txs) != len(fees) || len(txs) != len(confs) { 4748 t.Fatalf("len(txs) = %d, len(fees) = %d, len(confs) = %d", len(txs), len(fees), len(confs)) 4749 } 4750 4751 serializedTxs := make([][]byte, 0, len(txs)) 4752 for _, tx := range txs { 4753 serializedTx, err := serializeMsgTx(tx) 4754 if err != nil { 4755 t.Fatalf("unexpected error: %v", err) 4756 } 4757 serializedTxs = append(serializedTxs, serializedTx) 4758 } 4759 4760 currentTime := time.Now().Unix() 4761 for i := range txs { 4762 var blockHash string 4763 if confs[i] == 1 { 4764 blockHash = blockHash100.String() 4765 } 4766 node.getTransactionMap[txs[i].TxHash().String()] = &GetTransactionResult{ 4767 TxID: txs[i].TxHash().String(), 4768 Bytes: serializedTxs[i], 4769 BlockHash: blockHash, 4770 Confirmations: confs[i]} 4771 4772 if withinTimeLimit { 4773 node.getTransactionMap[txs[i].TxHash().String()].Time = uint64(currentTime) - minTimeBeforeAcceleration + 3 4774 } else { 4775 node.getTransactionMap[txs[i].TxHash().String()].Time = uint64(currentTime) - minTimeBeforeAcceleration - 1000 4776 } 4777 } 4778 } 4779 4780 // getAccelerationParams returns a chain of 4 swap transactions where the change 4781 // output of the last transaction has a certain value. If addAcceleration true, 4782 // the third transaction will be an acceleration transaction instead of one 4783 // that initiates a swap. 4784 getAccelerationParams := func(changeVal int64, addChangeToUnspent, addAcceleration bool, fees []float64, node *testData) ([]dex.Bytes, []dex.Bytes, dex.Bytes, []*wire.MsgTx) { 4785 txs := make([]*wire.MsgTx, 4) 4786 4787 // In order to be able to test using the SPV wallet, we need to properly 4788 // set the size of each of the outputs. The SPV wallet will parse these 4789 // transactions and calculate the fee on its own instead of just returning 4790 // what was set in getTransactionMap. 4791 changeOutputAmounts := make([]int64, 4) 4792 changeOutputAmounts[3] = changeVal 4793 swapAmount := int64(2e6) 4794 for i := 2; i >= 0; i-- { 4795 changeAmount := int64(toSatoshi(fees[i+1])) + changeOutputAmounts[i+1] 4796 if !(i == 1 && addAcceleration) { 4797 changeAmount += swapAmount 4798 } 4799 changeOutputAmounts[i] = changeAmount 4800 } 4801 4802 // The initial transaction in the chain has multiple inputs. 4803 fundingCoinsTotalOutput := changeOutputAmounts[0] + swapAmount + int64(toSatoshi(fees[0])) 4804 fundingTx := wire.MsgTx{ 4805 TxIn: []*wire.TxIn{{ 4806 PreviousOutPoint: wire.OutPoint{ 4807 Hash: *tTxHash, 4808 Index: 0, 4809 }}, 4810 {PreviousOutPoint: wire.OutPoint{ 4811 Hash: *tTxHash, 4812 Index: 1, 4813 }}, 4814 }, 4815 TxOut: []*wire.TxOut{{ 4816 Value: fundingCoinsTotalOutput / 2, 4817 }, { 4818 Value: fundingCoinsTotalOutput - fundingCoinsTotalOutput/2, 4819 }}, 4820 } 4821 fudingTxHex, _ := serializeMsgTx(&fundingTx) 4822 node.getTransactionMap[fundingTx.TxHash().String()] = &GetTransactionResult{Bytes: fudingTxHex, BlockHash: blockHash100.String()} 4823 4824 txs[0] = &wire.MsgTx{ 4825 TxIn: []*wire.TxIn{{ 4826 PreviousOutPoint: wire.OutPoint{ 4827 Hash: fundingTx.TxHash(), 4828 Index: 0, 4829 }}, 4830 {PreviousOutPoint: wire.OutPoint{ 4831 Hash: fundingTx.TxHash(), 4832 Index: 1, 4833 }}, 4834 }, 4835 TxOut: []*wire.TxOut{{ 4836 Value: changeOutputAmounts[0], 4837 }, { 4838 Value: swapAmount, 4839 }}, 4840 } 4841 for i := 1; i < 4; i++ { 4842 txs[i] = &wire.MsgTx{ 4843 TxIn: []*wire.TxIn{{ 4844 PreviousOutPoint: wire.OutPoint{ 4845 Hash: txs[i-1].TxHash(), 4846 Index: 0, 4847 }}, 4848 }, 4849 TxOut: []*wire.TxOut{{ 4850 Value: changeOutputAmounts[i], 4851 }}, 4852 } 4853 if !(i == 2 && addAcceleration) { 4854 txs[i].TxOut = append(txs[i].TxOut, &wire.TxOut{Value: swapAmount}) 4855 } 4856 } 4857 4858 swapCoins := make([]dex.Bytes, 0, len(txs)) 4859 accelerationCoins := make([]dex.Bytes, 0, 1) 4860 var changeCoin dex.Bytes 4861 for i, tx := range txs { 4862 hash := tx.TxHash() 4863 4864 if i == 2 && addAcceleration { 4865 accelerationCoins = append(accelerationCoins, ToCoinID(&hash, 0)) 4866 } else { 4867 ToCoinID(&hash, 0) 4868 swapCoins = append(swapCoins, ToCoinID(&hash, 0)) 4869 } 4870 4871 if i == len(txs)-1 { 4872 changeCoin = ToCoinID(&hash, 0) 4873 if addChangeToUnspent { 4874 node.listUnspent = append(node.listUnspent, &ListUnspentResult{ 4875 TxID: hash.String(), 4876 Vout: 0, 4877 }) 4878 } 4879 } 4880 } 4881 4882 return swapCoins, accelerationCoins, changeCoin, txs 4883 } 4884 4885 addUTXOToNode := func(confs uint32, segwit bool, amount uint64, node *testData) { 4886 var scriptPubKey []byte 4887 if segwit { 4888 scriptPubKey = tP2WPKH 4889 } else { 4890 scriptPubKey = tP2PKH 4891 } 4892 4893 node.listUnspent = append(node.listUnspent, &ListUnspentResult{ 4894 TxID: hex.EncodeToString(encode.RandomBytes(32)), 4895 Address: "1Bggq7Vu5oaoLFV1NNp5KhAzcku83qQhgi", 4896 Amount: toBTC(amount), 4897 Confirmations: confs, 4898 ScriptPubKey: scriptPubKey, 4899 Spendable: true, 4900 Solvable: true, 4901 }) 4902 4903 var prevChainHash chainhash.Hash 4904 copy(prevChainHash[:], encode.RandomBytes(32)) 4905 4906 tx := &wire.MsgTx{ 4907 TxIn: []*wire.TxIn{{ 4908 PreviousOutPoint: wire.OutPoint{ 4909 Hash: prevChainHash, 4910 Index: 0, 4911 }}, 4912 }, 4913 TxOut: []*wire.TxOut{ 4914 { 4915 Value: int64(amount), 4916 }, 4917 }} 4918 unspentTxHex, err := serializeMsgTx(tx) 4919 if err != nil { 4920 panic(fmt.Sprintf("could not serialize: %v", err)) 4921 } 4922 var blockHash string 4923 if confs == 0 { 4924 blockHash = blockHash100.String() 4925 } 4926 4927 node.getTransactionMap[node.listUnspent[len(node.listUnspent)-1].TxID] = &GetTransactionResult{ 4928 TxID: tx.TxHash().String(), 4929 Bytes: unspentTxHex, 4930 BlockHash: blockHash, 4931 Confirmations: uint64(confs)} 4932 } 4933 4934 totalInputOutput := func(tx *wire.MsgTx) (uint64, uint64) { 4935 var in, out uint64 4936 for _, input := range tx.TxIn { 4937 inputGtr, found := node.getTransactionMap[input.PreviousOutPoint.Hash.String()] 4938 if !found { 4939 t.Fatalf("tx id not found: %v", input.PreviousOutPoint.Hash.String()) 4940 } 4941 inputTx, err := msgTxFromHex(inputGtr.Bytes.String()) 4942 if err != nil { 4943 t.Fatalf("failed to deserialize tx: %v", err) 4944 } 4945 in += uint64(inputTx.TxOut[input.PreviousOutPoint.Index].Value) 4946 } 4947 4948 for _, output := range node.sentRawTx.TxOut { 4949 out += uint64(output.Value) 4950 } 4951 4952 return in, out 4953 } 4954 4955 calculateChangeTxSize := func(hasChange, segwit bool, numInputs int) uint64 { 4956 baseSize := dexbtc.MinimumTxOverhead 4957 4958 var inputSize, witnessSize, outputSize int 4959 if segwit { 4960 witnessSize = dexbtc.RedeemP2WPKHInputWitnessWeight*numInputs + 2 4961 inputSize = dexbtc.RedeemP2WPKHInputSize * numInputs 4962 outputSize = dexbtc.P2WPKHOutputSize 4963 } else { 4964 inputSize = numInputs * dexbtc.RedeemP2PKHInputSize 4965 outputSize = dexbtc.P2PKHOutputSize 4966 } 4967 4968 baseSize += inputSize 4969 if hasChange { 4970 baseSize += outputSize 4971 } 4972 4973 txWeight := baseSize*4 + witnessSize 4974 txSize := (txWeight + 3) / 4 4975 return uint64(txSize) 4976 } 4977 4978 var changeAmount int64 = 21350 4979 var newFeeRate uint64 = 50 4980 fees := []float64{0.00002, 0.000005, 0.00001, 0.00001} 4981 confs := []uint64{0, 0, 0, 0} 4982 _, _, _, txs := getAccelerationParams(changeAmount, false, false, fees, node) 4983 expectedFees := (sumTxSizes(txs, confs)+calculateChangeTxSize(false, segwit, 1))*newFeeRate - sumFees(fees, confs) 4984 _, _, _, txs = getAccelerationParams(changeAmount, true, false, fees, node) 4985 expectedFeesWithChange := (sumTxSizes(txs, confs)+calculateChangeTxSize(true, segwit, 1))*newFeeRate - sumFees(fees, confs) 4986 4987 // See dexbtc.IsDust for the source of this dustCoverage voodoo. 4988 var dustCoverage uint64 4989 if segwit { 4990 dustCoverage = (dexbtc.P2WPKHOutputSize + 41 + (107 / 4)) * 3 * newFeeRate 4991 } else { 4992 dustCoverage = (dexbtc.P2PKHOutputSize + 41 + 107) * 3 * newFeeRate 4993 } 4994 4995 type utxo struct { 4996 amount uint64 4997 confs uint32 4998 } 4999 5000 tests := []struct { 5001 name string 5002 changeNotInUnspent bool 5003 addPreviousAcceleration bool 5004 numUtxosUsed int 5005 changeAmount int64 5006 lockChange bool 5007 scrambleSwapCoins bool 5008 expectChange bool 5009 utxos []utxo 5010 fees []float64 5011 confs []uint64 5012 requiredForRemainingSwaps uint64 5013 expectChangeLocked bool 5014 txTimeWithinLimit bool 5015 5016 // needed to test AccelerateOrder and AccelerationEstimate 5017 expectAccelerateOrderErr bool 5018 expectAccelerationEstimateErr bool 5019 newFeeRate uint64 5020 5021 // needed to test PreAccelerate 5022 suggestedFeeRate uint64 5023 expectPreAccelerateErr bool 5024 expectTooEarly bool 5025 }{ 5026 { 5027 name: "change not in utxo set", 5028 changeAmount: int64(expectedFees), 5029 fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, 5030 confs: []uint64{0, 0, 0, 0}, 5031 newFeeRate: 50, 5032 expectAccelerationEstimateErr: true, 5033 expectAccelerateOrderErr: true, 5034 expectPreAccelerateErr: true, 5035 changeNotInUnspent: true, 5036 }, 5037 { 5038 name: "just enough without change", 5039 changeAmount: int64(expectedFees), 5040 fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, 5041 confs: confs, 5042 newFeeRate: newFeeRate, 5043 suggestedFeeRate: 30, 5044 }, 5045 { 5046 name: "works with acceleration", 5047 changeAmount: int64(expectedFees), 5048 fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, 5049 confs: confs, 5050 newFeeRate: newFeeRate, 5051 addPreviousAcceleration: true, 5052 }, 5053 { 5054 name: "scramble swap coins", 5055 changeAmount: int64(expectedFees), 5056 fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, 5057 confs: []uint64{0, 0, 0, 0}, 5058 newFeeRate: newFeeRate, 5059 scrambleSwapCoins: true, 5060 }, 5061 { 5062 name: "not enough with just change", 5063 changeAmount: int64(expectedFees - 1), 5064 fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, 5065 confs: []uint64{0, 0, 0, 0}, 5066 newFeeRate: newFeeRate, 5067 expectAccelerationEstimateErr: true, 5068 expectAccelerateOrderErr: true, 5069 }, 5070 { 5071 name: "add greater than dust amount to change", 5072 changeAmount: int64(expectedFeesWithChange + dustCoverage), 5073 fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, 5074 confs: []uint64{0, 0, 0, 0}, 5075 newFeeRate: newFeeRate, 5076 expectChange: true, 5077 }, 5078 { 5079 name: "add dust amount to change", 5080 changeAmount: int64(expectedFeesWithChange + dustCoverage - 1), 5081 fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, 5082 confs: []uint64{0, 0, 0, 0}, 5083 newFeeRate: newFeeRate, 5084 expectChange: false, 5085 }, 5086 { 5087 name: "don't accelerate confirmed transactions", 5088 changeAmount: int64(expectedFeesWithChange + dustCoverage), 5089 fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, 5090 confs: []uint64{1, 1, 0, 0}, 5091 newFeeRate: newFeeRate, 5092 expectChange: true, 5093 }, 5094 { 5095 name: "not enough", 5096 changeAmount: int64(expectedFees - 1), 5097 fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, 5098 confs: []uint64{0, 0, 0, 0}, 5099 newFeeRate: newFeeRate, 5100 expectAccelerationEstimateErr: true, 5101 expectAccelerateOrderErr: true, 5102 }, 5103 { 5104 name: "not enough with 0-conf utxo in wallet", 5105 changeAmount: int64(expectedFees - 1), 5106 fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, 5107 confs: []uint64{0, 0, 0, 0}, 5108 newFeeRate: newFeeRate, 5109 expectAccelerationEstimateErr: true, 5110 expectAccelerateOrderErr: true, 5111 utxos: []utxo{{ 5112 confs: 0, 5113 amount: 5e9, 5114 }}, 5115 }, 5116 { 5117 name: "enough with 1-conf utxo in wallet", 5118 changeAmount: int64(expectedFees - 1), 5119 fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, 5120 confs: []uint64{0, 0, 0, 0}, 5121 newFeeRate: newFeeRate, 5122 numUtxosUsed: 1, 5123 expectChange: true, 5124 utxos: []utxo{{ 5125 confs: 1, 5126 amount: 2e6, 5127 }}, 5128 }, 5129 { 5130 name: "not enough for remaining swaps", 5131 changeAmount: int64(expectedFees - 1), 5132 fees: fees, 5133 confs: []uint64{0, 0, 0, 0}, 5134 newFeeRate: newFeeRate, 5135 numUtxosUsed: 1, 5136 expectAccelerationEstimateErr: true, 5137 expectAccelerateOrderErr: true, 5138 expectChange: true, 5139 requiredForRemainingSwaps: 2e6, 5140 utxos: []utxo{{ 5141 confs: 1, 5142 amount: 2e6, 5143 }}, 5144 }, 5145 { 5146 name: "enough for remaining swaps", 5147 changeAmount: int64(expectedFees - 1), 5148 fees: fees, 5149 confs: []uint64{0, 0, 0, 0}, 5150 newFeeRate: newFeeRate, 5151 numUtxosUsed: 2, 5152 expectChange: true, 5153 expectChangeLocked: true, 5154 requiredForRemainingSwaps: 2e6, 5155 utxos: []utxo{ 5156 { 5157 confs: 1, 5158 amount: 2e6, 5159 }, 5160 { 5161 confs: 1, 5162 amount: 1e6, 5163 }, 5164 }, 5165 }, 5166 { 5167 name: "locked change, required for remaining >0", 5168 changeAmount: int64(expectedFees - 1), 5169 fees: fees, 5170 confs: []uint64{0, 0, 0, 0}, 5171 newFeeRate: newFeeRate, 5172 numUtxosUsed: 2, 5173 expectChange: true, 5174 expectChangeLocked: true, 5175 requiredForRemainingSwaps: 2e6, 5176 lockChange: true, 5177 utxos: []utxo{ 5178 { 5179 confs: 1, 5180 amount: 2e6, 5181 }, 5182 { 5183 confs: 1, 5184 amount: 1e6, 5185 }, 5186 }, 5187 }, 5188 { 5189 name: "locked change, required for remaining == 0", 5190 changeAmount: int64(expectedFees - 1), 5191 fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, 5192 confs: []uint64{0, 0, 0, 0}, 5193 newFeeRate: newFeeRate, 5194 numUtxosUsed: 2, 5195 expectChange: true, 5196 expectAccelerationEstimateErr: true, 5197 expectAccelerateOrderErr: true, 5198 expectPreAccelerateErr: true, 5199 lockChange: true, 5200 utxos: []utxo{ 5201 { 5202 confs: 1, 5203 amount: 2e6, 5204 }, 5205 { 5206 confs: 1, 5207 amount: 1e6, 5208 }, 5209 }, 5210 }, 5211 { 5212 name: "tx time within limit", 5213 txTimeWithinLimit: true, 5214 expectTooEarly: true, 5215 changeAmount: int64(expectedFees), 5216 fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, 5217 confs: confs, 5218 newFeeRate: newFeeRate, 5219 suggestedFeeRate: 30, 5220 }, 5221 } 5222 5223 for _, test := range tests { 5224 node.listUnspent = []*ListUnspentResult{} 5225 swapCoins, accelerations, changeCoin, txs := getAccelerationParams(test.changeAmount, !test.changeNotInUnspent, test.addPreviousAcceleration, test.fees, node) 5226 expectedFees := (sumTxSizes(txs, test.confs)+calculateChangeTxSize(test.expectChange, segwit, 1+test.numUtxosUsed))*test.newFeeRate - sumFees(test.fees, test.confs) 5227 if !test.expectChange { 5228 expectedFees = uint64(test.changeAmount) 5229 } 5230 if test.scrambleSwapCoins { 5231 temp := swapCoins[0] 5232 swapCoins[0] = swapCoins[2] 5233 swapCoins[2] = swapCoins[1] 5234 swapCoins[1] = temp 5235 } 5236 if test.lockChange { 5237 changeTxID, changeVout, _ := decodeCoinID(changeCoin) 5238 node.listLockUnspent = []*RPCOutpoint{ 5239 { 5240 TxID: changeTxID.String(), 5241 Vout: changeVout, 5242 }, 5243 } 5244 } 5245 for _, utxo := range test.utxos { 5246 addUTXOToNode(utxo.confs, segwit, utxo.amount, node) 5247 } 5248 5249 loadTxsIntoNode(txs, test.fees, test.confs, node, test.txTimeWithinLimit, t) 5250 5251 testAccelerateOrder := func() { 5252 change, txID, err := wallet.AccelerateOrder(swapCoins, accelerations, changeCoin, test.requiredForRemainingSwaps, test.newFeeRate) 5253 if test.expectAccelerateOrderErr { 5254 if err == nil { 5255 t.Fatalf("%s: expected AccelerateOrder error but did not get", test.name) 5256 } 5257 return 5258 } 5259 if err != nil { 5260 t.Fatalf("%s: unexpected error: %v", test.name, err) 5261 } 5262 5263 in, out := totalInputOutput(node.sentRawTx) 5264 lastTxFee := in - out 5265 if expectedFees != lastTxFee { 5266 t.Fatalf("%s: expected fee to be %d but got %d", test.name, expectedFees, lastTxFee) 5267 } 5268 5269 if node.sentRawTx.TxHash().String() != txID { 5270 t.Fatalf("%s: expected tx id %s, but got %s", test.name, node.sentRawTx.TxHash().String(), txID) 5271 } 5272 5273 if test.expectChange { 5274 if out == 0 { 5275 t.Fatalf("%s: expected change but did not get", test.name) 5276 } 5277 if change == nil { 5278 t.Fatalf("%s: expected change, but got nil", test.name) 5279 } 5280 5281 changeScript := node.sentRawTx.TxOut[0].PkScript 5282 5283 changeAddr, err := btcutil.DecodeAddress(node.changeAddr, &chaincfg.MainNetParams) 5284 if err != nil { 5285 t.Fatalf("%s: unexpected error: %v", test.name, err) 5286 } 5287 expectedChangeScript, err := txscript.PayToAddrScript(changeAddr) 5288 if err != nil { 5289 t.Fatalf("%s: unexpected error: %v", test.name, err) 5290 } 5291 5292 if !bytes.Equal(changeScript, expectedChangeScript) { 5293 t.Fatalf("%s: expected change script != actual", test.name) 5294 } 5295 5296 changeTxHash, changeVout, err := decodeCoinID(change.ID()) 5297 if err != nil { 5298 t.Fatalf("%s: unexpected error: %v", test.name, err) 5299 } 5300 if *changeTxHash != node.sentRawTx.TxHash() { 5301 t.Fatalf("%s: change tx hash %s != expected: %s", test.name, changeTxHash, node.sentRawTx.TxHash()) 5302 } 5303 if changeVout != 0 { 5304 t.Fatalf("%s: change vout %v != expected: 0", test.name, changeVout) 5305 } 5306 5307 var changeLocked bool 5308 for _, coin := range node.lockedCoins { 5309 if changeVout == coin.Vout && changeTxHash.String() == coin.TxID { 5310 changeLocked = true 5311 } 5312 } 5313 if changeLocked != test.expectChangeLocked { 5314 t.Fatalf("%s: expected change locked = %v, but was %v", test.name, test.expectChangeLocked, changeLocked) 5315 } 5316 } else { 5317 if out > 0 { 5318 t.Fatalf("%s: not expecting change but got %v", test.name, out) 5319 } 5320 if change != nil { 5321 t.Fatalf("%s: not expecting change but accelerate returned: %+v", test.name, change) 5322 } 5323 } 5324 5325 if test.requiredForRemainingSwaps > out { 5326 t.Fatalf("%s: %d needed of remaining swaps, but output was only %d", test.name, test.requiredForRemainingSwaps, out) 5327 } 5328 } 5329 testAccelerateOrder() 5330 5331 testAccelerationEstimate := func() { 5332 estimate, err := wallet.AccelerationEstimate(swapCoins, accelerations, changeCoin, test.requiredForRemainingSwaps, test.newFeeRate) 5333 if test.expectAccelerationEstimateErr { 5334 if err == nil { 5335 t.Fatalf("%s: expected AccelerationEstimate error but did not get", test.name) 5336 } 5337 return 5338 } 5339 if err != nil { 5340 t.Fatalf("%s: unexpected error: %v", test.name, err) 5341 } 5342 if estimate != expectedFees { 5343 t.Fatalf("%s: estimate %v != expected fees %v", test.name, estimate, expectedFees) 5344 } 5345 } 5346 testAccelerationEstimate() 5347 5348 testPreAccelerate := func() { 5349 currentRate, suggestedRange, earlyAcceleration, err := wallet.PreAccelerate(swapCoins, accelerations, changeCoin, test.requiredForRemainingSwaps, test.suggestedFeeRate) 5350 if test.expectPreAccelerateErr { 5351 if err == nil { 5352 t.Fatalf("%s: expected PreAccelerate error but did not get", test.name) 5353 } 5354 return 5355 } 5356 if err != nil { 5357 t.Fatalf("%s: unexpected error: %v", test.name, err) 5358 } 5359 5360 if test.expectTooEarly != (earlyAcceleration != nil) { 5361 t.Fatalf("%s: expected early acceleration %v, but got %v", test.name, test.expectTooEarly, earlyAcceleration) 5362 } 5363 5364 var totalSize, totalFee uint64 5365 for i, tx := range txs { 5366 if test.confs[i] == 0 { 5367 totalSize += dexbtc.MsgTxVBytes(tx) 5368 totalFee += toSatoshi(test.fees[i]) 5369 } 5370 } 5371 5372 expectedRate := totalFee / totalSize 5373 if expectedRate != currentRate { 5374 t.Fatalf("%s: expected current rate %v != actual %v", test.name, expectedRate, currentRate) 5375 } 5376 5377 totalFeePossible := totalFee + uint64(test.changeAmount) - test.requiredForRemainingSwaps 5378 var numConfirmedUTXO int 5379 for _, utxo := range test.utxos { 5380 if utxo.confs > 0 { 5381 totalFeePossible += utxo.amount 5382 numConfirmedUTXO++ 5383 } 5384 } 5385 totalSize += calculateChangeTxSize(test.expectChange, segwit, 1+numConfirmedUTXO) 5386 maxRatePossible := totalFeePossible / totalSize 5387 5388 expectedRangeHigh := expectedRate * 5 5389 if test.suggestedFeeRate > expectedRate { 5390 expectedRangeHigh = test.suggestedFeeRate * 5 5391 } 5392 if maxRatePossible < expectedRangeHigh { 5393 expectedRangeHigh = maxRatePossible 5394 } 5395 5396 expectedRangeLowX := float64(expectedRate+1) / float64(expectedRate) 5397 if suggestedRange.Start.X != expectedRangeLowX { 5398 t.Fatalf("%s: start of range should be %v on X, got: %v", test.name, expectedRangeLowX, suggestedRange.Start.X) 5399 } 5400 5401 if suggestedRange.Start.Y != float64(expectedRate+1) { 5402 t.Fatalf("%s: expected start of range on Y to be: %v, but got %v", test.name, float64(expectedRate), suggestedRange.Start.Y) 5403 } 5404 5405 if suggestedRange.End.Y != float64(expectedRangeHigh) { 5406 t.Fatalf("%s: expected end of range on Y to be: %v, but got %v", test.name, float64(expectedRangeHigh), suggestedRange.End.Y) 5407 } 5408 5409 expectedRangeHighX := float64(expectedRangeHigh) / float64(expectedRate) 5410 if suggestedRange.End.X != expectedRangeHighX { 5411 t.Fatalf("%s: expected end of range on X to be: %v, but got %v", test.name, float64(expectedRate), suggestedRange.End.X) 5412 } 5413 } 5414 testPreAccelerate() 5415 } 5416 } 5417 5418 func TestGetTxFee(t *testing.T) { 5419 runRubric(t, testGetTxFee) 5420 } 5421 5422 func testGetTxFee(t *testing.T, segwit bool, walletType string) { 5423 w, node, shutdown := tNewWallet(segwit, walletType) 5424 defer shutdown() 5425 5426 inputTx := &wire.MsgTx{ 5427 TxIn: []*wire.TxIn{{}}, 5428 TxOut: []*wire.TxOut{ 5429 { 5430 Value: 2e6, 5431 }, 5432 { 5433 Value: 3e6, 5434 }, 5435 }, 5436 } 5437 txBytes, err := serializeMsgTx(inputTx) 5438 if err != nil { 5439 t.Fatalf("unexpected error: %v", err) 5440 } 5441 5442 node.getTransactionMap = map[string]*GetTransactionResult{ 5443 "any": { 5444 Bytes: txBytes, 5445 }, 5446 } 5447 5448 tests := []struct { 5449 name string 5450 tx *wire.MsgTx 5451 expectErr bool 5452 expectedFee uint64 5453 getTxErr bool 5454 }{ 5455 { 5456 name: "ok", 5457 tx: &wire.MsgTx{ 5458 TxIn: []*wire.TxIn{{ 5459 PreviousOutPoint: wire.OutPoint{ 5460 Hash: inputTx.TxHash(), 5461 Index: 0, 5462 }}, 5463 {PreviousOutPoint: wire.OutPoint{ 5464 Hash: inputTx.TxHash(), 5465 Index: 1, 5466 }}, 5467 }, 5468 TxOut: []*wire.TxOut{{ 5469 Value: 4e6, 5470 }}, 5471 }, 5472 expectedFee: 1e6, 5473 }, 5474 { 5475 name: "get transaction error", 5476 tx: &wire.MsgTx{ 5477 TxIn: []*wire.TxIn{{ 5478 PreviousOutPoint: wire.OutPoint{ 5479 Hash: inputTx.TxHash(), 5480 Index: 0, 5481 }}, 5482 {PreviousOutPoint: wire.OutPoint{ 5483 Hash: inputTx.TxHash(), 5484 Index: 1, 5485 }}, 5486 }, 5487 TxOut: []*wire.TxOut{{ 5488 Value: 4e6, 5489 }}, 5490 }, 5491 getTxErr: true, 5492 expectErr: true, 5493 }, 5494 { 5495 name: "invalid prev output index error", 5496 tx: &wire.MsgTx{ 5497 TxIn: []*wire.TxIn{{ 5498 PreviousOutPoint: wire.OutPoint{ 5499 Hash: inputTx.TxHash(), 5500 Index: 0, 5501 }}, 5502 {PreviousOutPoint: wire.OutPoint{ 5503 Hash: inputTx.TxHash(), 5504 Index: 2, 5505 }}, 5506 }, 5507 TxOut: []*wire.TxOut{{ 5508 Value: 4e6, 5509 }}, 5510 }, 5511 expectErr: true, 5512 }, 5513 { 5514 name: "tx out > in error", 5515 tx: &wire.MsgTx{ 5516 TxIn: []*wire.TxIn{{ 5517 PreviousOutPoint: wire.OutPoint{ 5518 Hash: inputTx.TxHash(), 5519 Index: 0, 5520 }}, 5521 {PreviousOutPoint: wire.OutPoint{ 5522 Hash: inputTx.TxHash(), 5523 Index: 2, 5524 }}, 5525 }, 5526 TxOut: []*wire.TxOut{{ 5527 Value: 8e6, 5528 }}, 5529 }, 5530 expectErr: true, 5531 }, 5532 } 5533 5534 for _, test := range tests { 5535 node.getTransactionErr = nil 5536 if test.getTxErr { 5537 node.getTransactionErr = errors.New("") 5538 } 5539 5540 fee, err := w.getTxFee(test.tx) 5541 if test.expectErr { 5542 if err == nil { 5543 t.Fatalf("%s: expected error but did not get", test.name) 5544 } 5545 continue 5546 } 5547 if err != nil { 5548 t.Fatalf("%s: unexpected error: %v", test.name, err) 5549 } 5550 if fee != test.expectedFee { 5551 t.Fatalf("%s: expected fee %d, but got %d", test.name, test.expectedFee, fee) 5552 } 5553 } 5554 } 5555 5556 func TestTooEarlyToAccelerate(t *testing.T) { 5557 type tx struct { 5558 secondsBeforeNow uint64 5559 confirmations uint64 5560 } 5561 tests := []struct { 5562 name string 5563 accelerations []tx 5564 swaps []tx 5565 expectedReturn *asset.EarlyAcceleration 5566 expectError bool 5567 }{ 5568 { 5569 name: "all confirmed", 5570 accelerations: []tx{ 5571 {secondsBeforeNow: minTimeBeforeAcceleration + 800, 5572 confirmations: 2}, 5573 {secondsBeforeNow: minTimeBeforeAcceleration + 300, 5574 confirmations: 1}}, 5575 swaps: []tx{ 5576 {secondsBeforeNow: minTimeBeforeAcceleration + 1000, 5577 confirmations: 2}, 5578 {secondsBeforeNow: minTimeBeforeAcceleration + 500, 5579 confirmations: 1}}, 5580 expectError: true, 5581 }, 5582 { 5583 name: "no accelerations, not too early", 5584 accelerations: []tx{}, 5585 swaps: []tx{ 5586 {secondsBeforeNow: minTimeBeforeAcceleration + 1000, 5587 confirmations: 2}, 5588 {secondsBeforeNow: minTimeBeforeAcceleration + 500, 5589 confirmations: 0}, 5590 {secondsBeforeNow: minTimeBeforeAcceleration + 300, 5591 confirmations: 0}, 5592 {secondsBeforeNow: minTimeBeforeAcceleration + 800, 5593 confirmations: 2}}, 5594 }, 5595 { 5596 name: "acceleration after unconfirmed swap, not too early", 5597 accelerations: []tx{ 5598 {secondsBeforeNow: minTimeBeforeAcceleration + 300, 5599 confirmations: 0}}, 5600 swaps: []tx{ 5601 {secondsBeforeNow: minTimeBeforeAcceleration + 1000, 5602 confirmations: 2}, 5603 {secondsBeforeNow: minTimeBeforeAcceleration + 800, 5604 confirmations: 2}, 5605 {secondsBeforeNow: minTimeBeforeAcceleration + 500, 5606 confirmations: 0}}, 5607 }, 5608 { 5609 name: "acceleration after unconfirmed swap, too early", 5610 accelerations: []tx{ 5611 {secondsBeforeNow: minTimeBeforeAcceleration - 300, 5612 confirmations: 0}}, 5613 swaps: []tx{ 5614 {secondsBeforeNow: minTimeBeforeAcceleration + 1000, 5615 confirmations: 2}, 5616 {secondsBeforeNow: minTimeBeforeAcceleration + 800, 5617 confirmations: 2}, 5618 {secondsBeforeNow: minTimeBeforeAcceleration + 500, 5619 confirmations: 0}}, 5620 expectedReturn: &asset.EarlyAcceleration{ 5621 TimePast: minTimeBeforeAcceleration - 300, 5622 WasAccelerated: true, 5623 }, 5624 }, 5625 { 5626 name: "no accelerations, too early", 5627 accelerations: []tx{}, 5628 swaps: []tx{ 5629 {secondsBeforeNow: minTimeBeforeAcceleration + 1000, 5630 confirmations: 2}, 5631 {secondsBeforeNow: minTimeBeforeAcceleration + 800, 5632 confirmations: 2}, 5633 {secondsBeforeNow: minTimeBeforeAcceleration - 300, 5634 confirmations: 0}, 5635 {secondsBeforeNow: minTimeBeforeAcceleration - 500, 5636 confirmations: 0}}, 5637 expectedReturn: &asset.EarlyAcceleration{ 5638 TimePast: minTimeBeforeAcceleration - 300, 5639 WasAccelerated: false, 5640 }, 5641 }, 5642 { 5643 name: "only accelerations are unconfirmed, too early", 5644 accelerations: []tx{ 5645 {secondsBeforeNow: minTimeBeforeAcceleration - 300, 5646 confirmations: 0}}, 5647 swaps: []tx{ 5648 {secondsBeforeNow: minTimeBeforeAcceleration + 1000, 5649 confirmations: 2}, 5650 {secondsBeforeNow: minTimeBeforeAcceleration + 800, 5651 confirmations: 2}, 5652 }, 5653 expectedReturn: &asset.EarlyAcceleration{ 5654 TimePast: minTimeBeforeAcceleration - 300, 5655 WasAccelerated: true, 5656 }, 5657 }, 5658 } 5659 5660 for _, test := range tests { 5661 swapTxs := make([]*GetTransactionResult, 0, len(test.swaps)) 5662 accelerationTxs := make([]*GetTransactionResult, 0, len(test.accelerations)) 5663 now := time.Now().Unix() 5664 5665 for _, swap := range test.swaps { 5666 var txHash chainhash.Hash 5667 copy(txHash[:], encode.RandomBytes(32)) 5668 swapTxs = append(swapTxs, &GetTransactionResult{ 5669 TxID: txHash.String(), 5670 Confirmations: swap.confirmations, 5671 Time: uint64(now) - swap.secondsBeforeNow, 5672 }) 5673 } 5674 5675 for _, acceleration := range test.accelerations { 5676 var txHash chainhash.Hash 5677 copy(txHash[:], encode.RandomBytes(32)) 5678 accelerationTxs = append(accelerationTxs, &GetTransactionResult{ 5679 TxID: txHash.String(), 5680 Confirmations: acceleration.confirmations, 5681 Time: uint64(now) - acceleration.secondsBeforeNow, 5682 }) 5683 } 5684 5685 earlyAcceleration, err := tooEarlyToAccelerate(swapTxs, accelerationTxs) 5686 if test.expectError { 5687 if err == nil { 5688 t.Fatalf("%s: expected error but did not get", test.name) 5689 } 5690 continue 5691 } 5692 if err != nil { 5693 t.Fatalf("%s: unexpected error: %v", test.name, err) 5694 } 5695 5696 if test.expectedReturn == nil && earlyAcceleration == nil { 5697 continue 5698 } 5699 if test.expectedReturn == nil && earlyAcceleration != nil { 5700 t.Fatalf("%s: expected return to be nil, but got %+v", test.name, earlyAcceleration) 5701 } 5702 if test.expectedReturn != nil && earlyAcceleration == nil { 5703 t.Fatalf("%s: expected return to not be nil, but got nil", test.name) 5704 } 5705 if test.expectedReturn.TimePast != earlyAcceleration.TimePast || 5706 test.expectedReturn.WasAccelerated != earlyAcceleration.WasAccelerated { 5707 t.Fatalf("%s: expected %+v, got %+v", test.name, test.expectedReturn, earlyAcceleration) 5708 } 5709 } 5710 } 5711 5712 type tReconfigurer struct { 5713 *rpcClient 5714 restart bool 5715 err error 5716 } 5717 5718 func (r *tReconfigurer) Reconfigure(walletCfg *asset.WalletConfig, currentAddress string) (restartRequired bool, err error) { 5719 return r.restart, r.err 5720 } 5721 5722 func TestReconfigure(t *testing.T) { 5723 wallet, _, shutdown := tNewWallet(false, walletTypeRPC) 5724 // We don't need the wallet running, and we need to write to the node field, 5725 // which is used in the block loop. 5726 shutdown() 5727 5728 reconfigurer := &tReconfigurer{rpcClient: wallet.node.(*rpcClient)} 5729 wallet.baseWallet.setNode(reconfigurer) 5730 5731 cfg := &asset.WalletConfig{ 5732 Settings: map[string]string{ 5733 "redeemconftarget": "3", 5734 }, 5735 } 5736 5737 restart, err := wallet.Reconfigure(tCtx, cfg, "") 5738 if err != nil { 5739 t.Fatalf("initial Reconfigure error: %v", err) 5740 } 5741 if restart { 5742 t.Fatal("restart = false not propagated") 5743 } 5744 if wallet.redeemConfTarget() != 3 { 5745 t.Fatal("redeemconftarget not updated", wallet.redeemConfTarget()) 5746 } 5747 5748 cfg.Settings["redeemconftarget"] = "2" 5749 5750 reconfigurer.err = tErr 5751 if _, err = wallet.Reconfigure(tCtx, cfg, ""); err == nil { 5752 t.Fatal("node reconfigure error not propagated") 5753 } 5754 reconfigurer.err = nil 5755 // Redeem target should be unchanged 5756 if wallet.redeemConfTarget() != 3 { 5757 t.Fatal("redeemconftarget updated for node reconfigure error", wallet.redeemConfTarget()) 5758 } 5759 5760 reconfigurer.restart = true 5761 if restart, err := wallet.Reconfigure(tCtx, cfg, ""); err != nil { 5762 t.Fatal("Reconfigure error for restart = true") 5763 } else if !restart { 5764 t.Fatal("restart = true not propagated") 5765 } 5766 reconfigurer.restart = false 5767 5768 // One last success, and make sure baseWalletConfig is updated. 5769 if _, err := wallet.Reconfigure(tCtx, cfg, ""); err != nil { 5770 t.Fatalf("Reconfigure error for final success: %v", err) 5771 } 5772 // Redeem target should be changed now. 5773 if wallet.redeemConfTarget() != 2 { 5774 t.Fatal("redeemconftarget not updated in final success", wallet.redeemConfTarget()) 5775 } 5776 } 5777 5778 func TestConfirmRedemption(t *testing.T) { 5779 segwit := true 5780 wallet, node, shutdown := tNewWallet(segwit, walletTypeRPC) 5781 defer shutdown() 5782 5783 swapVal := toSatoshi(5) 5784 5785 secret, _, _, contract, addr, _, lockTime := makeSwapContract(segwit, time.Hour*12) 5786 5787 coin := NewOutput(tTxHash, 0, swapVal) 5788 ci := &asset.AuditInfo{ 5789 Coin: coin, 5790 Contract: contract, 5791 Recipient: addr.String(), 5792 Expiration: lockTime, 5793 } 5794 5795 redemption := &asset.Redemption{ 5796 Spends: ci, 5797 Secret: secret, 5798 } 5799 5800 coinID := coin.ID() 5801 5802 privBytes, _ := hex.DecodeString("b07209eec1a8fb6cfe5cb6ace36567406971a75c330db7101fb21bc679bc5330") 5803 privKey, _ := btcec.PrivKeyFromBytes(privBytes) 5804 wif, err := btcutil.NewWIF(privKey, &chaincfg.MainNetParams, true) 5805 if err != nil { 5806 t.Fatalf("error encoding wif: %v", err) 5807 } 5808 5809 node.newAddress = tP2WPKHAddr 5810 node.privKeyForAddr = wif 5811 5812 tests := []struct { 5813 name string 5814 redemption *asset.Redemption 5815 coinID []byte 5816 wantErr bool 5817 wantConfs uint64 5818 txOutRes *btcjson.GetTxOutResult 5819 getTransactionResult *GetTransactionResult 5820 txOutErr error 5821 getTransactionErr error 5822 }{{ 5823 name: "ok and found", 5824 coinID: coinID, 5825 redemption: redemption, 5826 getTransactionResult: new(GetTransactionResult), 5827 }, { 5828 name: "ok spent by someone but not sure who", 5829 coinID: coinID, 5830 redemption: redemption, 5831 wantConfs: requiredRedeemConfirms, 5832 }, { 5833 name: "ok but sending new tx", 5834 coinID: coinID, 5835 redemption: redemption, 5836 txOutRes: new(btcjson.GetTxOutResult), 5837 }, { 5838 name: "decode coin error", 5839 redemption: redemption, 5840 wantErr: true, 5841 }, { 5842 name: "error finding contract output", 5843 coinID: coinID, 5844 redemption: redemption, 5845 txOutErr: errors.New(""), 5846 wantErr: true, 5847 }, { 5848 name: "error finding redeem tx", 5849 coinID: coinID, 5850 redemption: redemption, 5851 getTransactionErr: errors.New(""), 5852 wantErr: true, 5853 }, { 5854 name: "redemption error", 5855 coinID: coinID, 5856 redemption: func() *asset.Redemption { 5857 ci := &asset.AuditInfo{ 5858 Coin: coin, 5859 // Contract: contract, 5860 Recipient: addr.String(), 5861 Expiration: lockTime, 5862 } 5863 5864 return &asset.Redemption{ 5865 Spends: ci, 5866 Secret: secret, 5867 } 5868 }(), 5869 txOutRes: new(btcjson.GetTxOutResult), 5870 wantErr: true, 5871 }} 5872 for _, test := range tests { 5873 node.txOutRes = test.txOutRes 5874 node.txOutErr = test.txOutErr 5875 node.getTransactionErr = test.getTransactionErr 5876 node.getTransactionMap[tTxID] = test.getTransactionResult 5877 5878 status, err := wallet.ConfirmRedemption(test.coinID, test.redemption, 0) 5879 if test.wantErr { 5880 if err == nil { 5881 t.Fatalf("%q: expected error", test.name) 5882 } 5883 continue 5884 } 5885 if err != nil { 5886 t.Fatalf("%q: unexpected error: %v", test.name, err) 5887 } 5888 if status.Confs != test.wantConfs { 5889 t.Fatalf("%q: wanted %d confs but got %d", test.name, test.wantConfs, status.Confs) 5890 } 5891 } 5892 } 5893 5894 func TestAddressRecycling(t *testing.T) { 5895 w, td, shutdown := tNewWallet(false, walletTypeSPV) 5896 defer shutdown() 5897 5898 compareAddrLists := func(tag string, exp, actual []string) { 5899 if len(exp) != len(actual) { 5900 t.Fatalf("%s: Wrong number of recycled addrs. Expected %d, got %d", tag, len(exp), len(actual)) 5901 } 5902 unfound := make(map[string]bool, len(exp)) 5903 for _, addr := range exp { 5904 unfound[addr] = true 5905 } 5906 for _, addr := range actual { 5907 delete(unfound, addr) 5908 } 5909 if len(unfound) > 0 { 5910 t.Fatalf("%s: Wrong addresses stored. Expected %+v, got %+v", tag, exp, actual) 5911 } 5912 } 5913 5914 checkAddrs := func(tag string, expAddrs ...string) { 5915 memList := make([]string, 0, len(w.ar.addrs)) 5916 for addr := range w.ar.addrs { 5917 memList = append(memList, addr) 5918 } 5919 compareAddrLists(tag, expAddrs, memList) 5920 } 5921 5922 addr1, _ := btcutil.NewAddressPubKeyHash(encode.RandomBytes(20), &chaincfg.MainNetParams) 5923 addr2, _ := btcutil.NewAddressPubKeyHash(encode.RandomBytes(20), &chaincfg.MainNetParams) 5924 5925 w.ReturnRedemptionAddress(addr1.String()) 5926 5927 checkAddrs("first single addr", addr1.String()) 5928 5929 td.ownsAddress = true 5930 redemptionAddr, _ := w.RedemptionAddress() 5931 if redemptionAddr != addr1.String() { 5932 t.Fatalf("recycled address not returned for redemption address") 5933 } 5934 5935 contracts := make([][]byte, 2) 5936 contracts[0], _ = dexbtc.MakeContract(addr1, addr2, encode.RandomBytes(32), time.Now().Unix(), false, &chaincfg.MainNetParams) 5937 contracts[1], _ = dexbtc.MakeContract(addr2, addr1, encode.RandomBytes(32), time.Now().Unix(), false, &chaincfg.MainNetParams) 5938 5939 w.ReturnRefundContracts(contracts) 5940 5941 checkAddrs("two addrs", addr1.String(), addr2.String()) 5942 5943 // Check that unowned addresses are not returned 5944 td.ownsAddress = false 5945 td.newAddress = "1JoiKZz2QRd47ARtcYgvgxC9jhnre9aphv" 5946 priv, _ := btcec.NewPrivateKey() 5947 td.privKeyForAddr, _ = btcutil.NewWIF(priv, &chaincfg.MainNetParams, true) 5948 redemptionAddr, err := w.RedemptionAddress() 5949 if err != nil { 5950 t.Fatalf("RedemptionAddress error: %v", err) 5951 } 5952 if redemptionAddr == addr1.String() || redemptionAddr == addr2.String() { 5953 t.Fatalf("unowned address returned (1)") 5954 } 5955 redemptionAddr, _ = w.RedemptionAddress() 5956 if redemptionAddr == addr1.String() || redemptionAddr == addr2.String() { 5957 t.Fatalf("unowned address returned (2)") 5958 } 5959 5960 checkAddrs("after unowned") 5961 5962 // Check address loading. 5963 w.ReturnRefundContracts(contracts) 5964 5965 w.ar.WriteRecycledAddrsToFile() 5966 b, _ := os.ReadFile(w.ar.recyclePath) 5967 var fileAddrs []string 5968 for _, addr := range strings.Split(string(b), "\n") { 5969 if addr == "" { 5970 continue 5971 } 5972 fileAddrs = append(fileAddrs, addr) 5973 } 5974 compareAddrLists("filecheck", []string{addr1.String(), addr2.String()}, fileAddrs) 5975 5976 otherW, _ := newUnconnectedWallet(w.cloneParams, &WalletConfig{}) 5977 if len(otherW.ar.addrs) != 2 { 5978 t.Fatalf("newly opened wallet didn't load recycled addrs") 5979 } 5980 5981 } 5982 5983 func TestFindBond(t *testing.T) { 5984 wallet, node, shutdown := tNewWallet(false, walletTypeRPC) 5985 defer shutdown() 5986 5987 node.signFunc = func(tx *wire.MsgTx) { 5988 signFunc(tx, 0, wallet.segwit) 5989 } 5990 5991 privBytes, _ := hex.DecodeString("b07209eec1a8fb6cfe5cb6ace36567406971a75c330db7101fb21bc679bc5330") 5992 bondKey, _ := btcec.PrivKeyFromBytes(privBytes) 5993 5994 amt := uint64(500_000) 5995 acctID := [32]byte{} 5996 lockTime := time.Now().Add(time.Hour * 12) 5997 utxo := &ListUnspentResult{ 5998 TxID: tTxID, 5999 Address: tP2PKHAddr, 6000 Amount: 1.0, 6001 Confirmations: 1, 6002 Spendable: true, 6003 ScriptPubKey: decodeString("76a914e114d5bb20cdbd75f3726f27c10423eb1332576288ac"), 6004 } 6005 node.listUnspent = []*ListUnspentResult{utxo} 6006 node.changeAddr = tP2PKHAddr 6007 node.newAddress = tP2PKHAddr 6008 6009 bond, _, err := wallet.MakeBondTx(0, amt, 200, lockTime, bondKey, acctID[:]) 6010 if err != nil { 6011 t.Fatal(err) 6012 } 6013 6014 txRes := func(tx []byte) *GetTransactionResult { 6015 return &GetTransactionResult{ 6016 BlockHash: hex.EncodeToString(randBytes(32)), 6017 Bytes: tx, 6018 } 6019 } 6020 6021 newBondTx := func() *wire.MsgTx { 6022 msgTx, err := msgTxFromBytes(bond.SignedTx) 6023 if err != nil { 6024 t.Fatal(err) 6025 } 6026 return msgTx 6027 } 6028 tooFewOutputs := newBondTx() 6029 tooFewOutputs.TxOut = tooFewOutputs.TxOut[2:] 6030 tooFewOutputsBytes, err := serializeMsgTx(tooFewOutputs) 6031 if err != nil { 6032 t.Fatal(err) 6033 } 6034 6035 badBondScript := newBondTx() 6036 badBondScript.TxOut[1].PkScript = badBondScript.TxOut[1].PkScript[1:] 6037 badBondScriptBytes, err := serializeMsgTx(badBondScript) 6038 if err != nil { 6039 t.Fatal(err) 6040 } 6041 6042 noBondMatch := newBondTx() 6043 noBondMatch.TxOut[0].PkScript = noBondMatch.TxOut[0].PkScript[1:] 6044 noBondMatchBytes, err := serializeMsgTx(noBondMatch) 6045 if err != nil { 6046 t.Fatal(err) 6047 } 6048 6049 node.addRawTx(1, newBondTx()) 6050 verboseBlocks := node.verboseBlocks 6051 for _, blk := range verboseBlocks { 6052 blk.msgBlock.Header.Timestamp = time.Now() 6053 } 6054 6055 tests := []struct { 6056 name string 6057 coinID []byte 6058 txRes *GetTransactionResult 6059 bestBlockErr error 6060 getTransactionErr error 6061 verboseBlocks map[chainhash.Hash]*msgBlockWithHeight 6062 searchUntil time.Time 6063 wantErr bool 6064 }{{ 6065 name: "ok", 6066 coinID: bond.CoinID, 6067 txRes: txRes(bond.SignedTx), 6068 }, { 6069 name: "ok with find blocks", 6070 coinID: bond.CoinID, 6071 getTransactionErr: asset.CoinNotFoundError, 6072 }, { 6073 name: "bad coin id", 6074 coinID: make([]byte, 0), 6075 txRes: txRes(bond.SignedTx), 6076 wantErr: true, 6077 }, { 6078 name: "missing an output", 6079 coinID: bond.CoinID, 6080 txRes: txRes(tooFewOutputsBytes), 6081 wantErr: true, 6082 }, { 6083 name: "bad bond commitment script", 6084 coinID: bond.CoinID, 6085 txRes: txRes(badBondScriptBytes), 6086 wantErr: true, 6087 }, { 6088 name: "bond script does not match commitment", 6089 coinID: bond.CoinID, 6090 txRes: txRes(noBondMatchBytes), 6091 wantErr: true, 6092 }, { 6093 name: "bad msgtx", 6094 coinID: bond.CoinID, 6095 txRes: txRes(bond.SignedTx[1:]), 6096 wantErr: true, 6097 }, { 6098 name: "get best block error", 6099 coinID: bond.CoinID, 6100 getTransactionErr: asset.CoinNotFoundError, 6101 bestBlockErr: errors.New("some error"), 6102 wantErr: true, 6103 }, { 6104 name: "block not found", 6105 coinID: bond.CoinID, 6106 getTransactionErr: asset.CoinNotFoundError, 6107 verboseBlocks: map[chainhash.Hash]*msgBlockWithHeight{}, 6108 wantErr: true, 6109 }, { 6110 name: "did not find by search until time", 6111 coinID: bond.CoinID, 6112 getTransactionErr: asset.CoinNotFoundError, 6113 searchUntil: time.Now().Add(time.Hour), 6114 wantErr: true, 6115 }} 6116 6117 for _, test := range tests { 6118 t.Run(test.name, func(t *testing.T) { 6119 node.getTransactionMap["any"] = test.txRes 6120 node.getBestBlockHashErr = test.bestBlockErr 6121 node.verboseBlocks = verboseBlocks 6122 if test.verboseBlocks != nil { 6123 node.verboseBlocks = test.verboseBlocks 6124 } 6125 bd, err := wallet.FindBond(tCtx, test.coinID, test.searchUntil) 6126 if test.wantErr { 6127 if err == nil { 6128 t.Fatal("expected error") 6129 } 6130 return 6131 } 6132 if err != nil { 6133 t.Fatalf("unexpected error: %v", err) 6134 } 6135 if !bd.CheckPrivKey(bondKey) { 6136 t.Fatal("pkh not equal") 6137 } 6138 }) 6139 } 6140 } 6141 6142 func TestIDUnknownTx(t *testing.T) { 6143 t.Run("non-segwit", func(t *testing.T) { 6144 testIDUnknownTx(t, false) 6145 }) 6146 t.Run("segwit", func(t *testing.T) { 6147 testIDUnknownTx(t, true) 6148 }) 6149 } 6150 6151 func testIDUnknownTx(t *testing.T, segwit bool) { 6152 // Swap Tx - any tx with p2sh outputs that is not a bond. 6153 _, _, swapPKScript, _, _, _, _ := makeSwapContract(true, time.Hour*12) 6154 swapTx := &wire.MsgTx{ 6155 TxIn: []*wire.TxIn{wire.NewTxIn(&wire.OutPoint{}, nil, nil)}, 6156 TxOut: []*wire.TxOut{wire.NewTxOut(int64(toSatoshi(1)), swapPKScript)}, 6157 } 6158 6159 // Redeem Tx 6160 addrStr := tP2PKHAddr 6161 if segwit { 6162 addrStr = tP2WPKHAddr 6163 } 6164 addr, _ := decodeAddress(addrStr, &chaincfg.MainNetParams) 6165 swapContract, _ := dexbtc.MakeContract(addr, addr, randBytes(32), time.Now().Unix(), segwit, &chaincfg.MainNetParams) 6166 txIn := wire.NewTxIn(&wire.OutPoint{}, nil, nil) 6167 if segwit { 6168 txIn.Witness = dexbtc.RedeemP2WSHContract(swapContract, randBytes(73), randBytes(33), randBytes(32)) 6169 } else { 6170 txIn.SignatureScript, _ = dexbtc.RedeemP2SHContract(swapContract, randBytes(73), randBytes(33), randBytes(32)) 6171 } 6172 redeemFee := 0.0000143 6173 redemptionTx := &wire.MsgTx{ 6174 TxIn: []*wire.TxIn{txIn}, 6175 TxOut: []*wire.TxOut{wire.NewTxOut(int64(toSatoshi(5-redeemFee)), tP2PKH)}, 6176 } 6177 6178 h2b := func(h string) []byte { 6179 b, _ := hex.DecodeString(h) 6180 return b 6181 } 6182 6183 // Create Bond Tx 6184 bondLockTime := 1711637410 6185 bondID := h2b("0e39bbb09592fd00b7d770cc832ddf4d625ae3a0") 6186 accountID := h2b("a0836b39b5ceb84f422b8a8cd5940117087a8522457c6d81d200557652fbe6ea") 6187 bondContract, _ := dexbtc.MakeBondScript(0, uint32(bondLockTime), bondID) 6188 contractAddr, _ := scriptHashAddress(segwit, bondContract, &chaincfg.MainNetParams) 6189 bondPkScript, _ := txscript.PayToAddrScript(contractAddr) 6190 bondOutput := wire.NewTxOut(int64(toSatoshi(2)), bondPkScript) 6191 bondCommitPkScript, _ := bondPushDataScript(0, accountID, int64(bondLockTime), bondID) 6192 bondCommitmentOutput := wire.NewTxOut(0, bondCommitPkScript) 6193 createBondTx := &wire.MsgTx{ 6194 TxIn: []*wire.TxIn{wire.NewTxIn(&wire.OutPoint{}, nil, nil)}, 6195 TxOut: []*wire.TxOut{bondOutput, bondCommitmentOutput}, 6196 } 6197 6198 // Redeem Bond Tx 6199 txIn = wire.NewTxIn(&wire.OutPoint{}, nil, nil) 6200 if segwit { 6201 txIn.Witness = dexbtc.RefundBondScriptSegwit(bondContract, randBytes(73), randBytes(33)) 6202 } else { 6203 txIn.SignatureScript, _ = dexbtc.RefundBondScript(bondContract, randBytes(73), randBytes(33)) 6204 } 6205 redeemBondTx := &wire.MsgTx{ 6206 TxIn: []*wire.TxIn{txIn}, 6207 TxOut: []*wire.TxOut{wire.NewTxOut(int64(toSatoshi(5)), tP2PKH)}, 6208 } 6209 6210 // Acceleration Tx 6211 accelerationTx := &wire.MsgTx{ 6212 TxIn: []*wire.TxIn{wire.NewTxIn(&wire.OutPoint{}, nil, nil)}, 6213 TxOut: []*wire.TxOut{wire.NewTxOut(0, tP2PKH)}, 6214 } 6215 6216 // Split Tx 6217 splitTx := &wire.MsgTx{ 6218 TxIn: []*wire.TxIn{wire.NewTxIn(&wire.OutPoint{}, nil, nil)}, 6219 TxOut: []*wire.TxOut{wire.NewTxOut(0, tP2PKH), wire.NewTxOut(0, tP2PKH)}, 6220 } 6221 6222 // Send Tx 6223 ourPkScript, cpPkScript := tP2PKH, tP2WPKH 6224 if segwit { 6225 ourPkScript, cpPkScript = tP2WPKH, tP2PKH 6226 } 6227 6228 _, ourAddr, _, _ := txscript.ExtractPkScriptAddrs(ourPkScript, &chaincfg.MainNetParams) 6229 ourAddrStr := ourAddr[0].String() 6230 6231 _, cpAddr, _, _ := txscript.ExtractPkScriptAddrs(cpPkScript, &chaincfg.MainNetParams) 6232 cpAddrStr := cpAddr[0].String() 6233 6234 sendTx := &wire.MsgTx{ 6235 TxIn: []*wire.TxIn{wire.NewTxIn(&wire.OutPoint{}, nil, nil)}, 6236 TxOut: []*wire.TxOut{wire.NewTxOut(int64(toSatoshi(0.001)), ourPkScript), wire.NewTxOut(int64(toSatoshi(4)), cpPkScript)}, 6237 } 6238 6239 // Receive Tx 6240 receiveTx := &wire.MsgTx{ 6241 TxIn: []*wire.TxIn{wire.NewTxIn(&wire.OutPoint{}, nil, nil)}, 6242 TxOut: []*wire.TxOut{wire.NewTxOut(int64(toSatoshi(4)), ourPkScript), wire.NewTxOut(int64(toSatoshi(0.001)), cpPkScript)}, 6243 } 6244 6245 floatPtr := func(f float64) *float64 { 6246 return &f 6247 } 6248 6249 type test struct { 6250 name string 6251 ltr *ListTransactionsResult 6252 tx *wire.MsgTx 6253 ownedAddresses map[string]bool 6254 ownsAddress bool 6255 exp *asset.WalletTransaction 6256 } 6257 6258 tests := []*test{ 6259 { 6260 name: "swap", 6261 ltr: &ListTransactionsResult{ 6262 Send: true, 6263 }, 6264 tx: swapTx, 6265 exp: &asset.WalletTransaction{ 6266 Type: asset.SwapOrSend, 6267 ID: swapTx.TxHash().String(), 6268 Amount: toSatoshi(1), 6269 }, 6270 }, 6271 { 6272 name: "redeem", 6273 ltr: &ListTransactionsResult{ 6274 Send: false, 6275 Fee: &redeemFee, 6276 }, 6277 tx: redemptionTx, 6278 exp: &asset.WalletTransaction{ 6279 Type: asset.Redeem, 6280 ID: redemptionTx.TxHash().String(), 6281 Amount: toSatoshi(5), 6282 Fees: toSatoshi(redeemFee), 6283 }, 6284 }, 6285 { 6286 name: "create bond", 6287 ltr: &ListTransactionsResult{ 6288 Send: true, 6289 Fee: floatPtr(0.0000222), 6290 }, 6291 tx: createBondTx, 6292 exp: &asset.WalletTransaction{ 6293 Type: asset.CreateBond, 6294 ID: createBondTx.TxHash().String(), 6295 Amount: toSatoshi(2), 6296 Fees: toSatoshi(0.0000222), 6297 BondInfo: &asset.BondTxInfo{ 6298 AccountID: accountID, 6299 BondID: bondID, 6300 LockTime: uint64(bondLockTime), 6301 }, 6302 }, 6303 }, 6304 { 6305 name: "redeem bond", 6306 ltr: &ListTransactionsResult{ 6307 Send: false, 6308 }, 6309 tx: redeemBondTx, 6310 exp: &asset.WalletTransaction{ 6311 Type: asset.RedeemBond, 6312 ID: redeemBondTx.TxHash().String(), 6313 Amount: toSatoshi(5), 6314 BondInfo: &asset.BondTxInfo{ 6315 AccountID: []byte{}, 6316 BondID: bondID, 6317 LockTime: uint64(bondLockTime), 6318 }, 6319 }, 6320 }, 6321 { 6322 name: "acceleration", 6323 ltr: &ListTransactionsResult{ 6324 Send: true, 6325 }, 6326 tx: accelerationTx, 6327 exp: &asset.WalletTransaction{ 6328 Type: asset.Acceleration, 6329 ID: accelerationTx.TxHash().String(), 6330 }, 6331 ownsAddress: true, 6332 }, 6333 { 6334 name: "split", 6335 ltr: &ListTransactionsResult{ 6336 Send: true, 6337 }, 6338 tx: splitTx, 6339 exp: &asset.WalletTransaction{ 6340 Type: asset.Split, 6341 ID: splitTx.TxHash().String(), 6342 }, 6343 ownsAddress: true, 6344 }, 6345 { 6346 name: "send", 6347 ltr: &ListTransactionsResult{ 6348 Send: true, 6349 }, 6350 tx: sendTx, 6351 exp: &asset.WalletTransaction{ 6352 Type: asset.Send, 6353 ID: sendTx.TxHash().String(), 6354 Amount: toSatoshi(4), 6355 Recipient: &cpAddrStr, 6356 }, 6357 ownedAddresses: map[string]bool{ourAddrStr: true}, 6358 }, 6359 { 6360 name: "receive", 6361 ltr: &ListTransactionsResult{}, 6362 tx: receiveTx, 6363 exp: &asset.WalletTransaction{ 6364 Type: asset.Receive, 6365 ID: receiveTx.TxHash().String(), 6366 Amount: toSatoshi(4), 6367 Recipient: &ourAddrStr, 6368 }, 6369 ownedAddresses: map[string]bool{ourAddrStr: true}, 6370 }, 6371 } 6372 6373 runTest := func(tt *test) { 6374 t.Run(tt.name, func(t *testing.T) { 6375 wallet, node, shutdown := tNewWallet(true, walletTypeRPC) 6376 defer shutdown() 6377 txID := tt.tx.TxHash().String() 6378 tt.ltr.TxID = txID 6379 6380 buf := new(bytes.Buffer) 6381 err := tt.tx.Serialize(buf) 6382 if err != nil { 6383 t.Fatalf("%s: error serializing tx: %v", tt.name, err) 6384 } 6385 6386 node.getTransactionMap[txID] = &GetTransactionResult{Bytes: buf.Bytes()} 6387 node.ownedAddresses = tt.ownedAddresses 6388 node.ownsAddress = tt.ownsAddress 6389 wt, err := wallet.idUnknownTx(tt.ltr) 6390 if err != nil { 6391 t.Fatalf("%s: unexpected error: %v", tt.name, err) 6392 } 6393 if !reflect.DeepEqual(wt, tt.exp) { 6394 t.Fatalf("%s: expected %+v, got %+v", tt.name, tt.exp, wt) 6395 } 6396 }) 6397 } 6398 6399 for _, tt := range tests { 6400 runTest(tt) 6401 } 6402 } 6403 6404 func TestFeeRateCache(t *testing.T) { 6405 const okRate = 1 6406 var n int 6407 feeRateOK := func(context.Context, dex.Network) (uint64, error) { 6408 n++ 6409 return okRate, nil 6410 } 6411 err1 := errors.New("test error 1") 6412 feeRateBad := func(context.Context, dex.Network) (uint64, error) { 6413 n++ 6414 return 0, err1 6415 } 6416 c := &feeRateCache{f: feeRateOK} 6417 r, err := c.rate(context.Background(), dex.Mainnet) 6418 if err != nil { 6419 t.Fatalf("rate error: %v", err) 6420 } 6421 if r != okRate { 6422 t.Fatalf("expected rate %d, got %d", okRate, r) 6423 } 6424 if n != 1 { 6425 t.Fatal("counter not incremented") 6426 } 6427 6428 // Again should hit cache 6429 r, err = c.rate(context.Background(), dex.Mainnet) 6430 if err != nil { 6431 t.Fatalf("cached rate error: %v", err) 6432 } 6433 if r != okRate { 6434 t.Fatalf("expected cached rate %d, got %d", okRate, r) 6435 } 6436 if n != 1 { 6437 t.Fatal("counter incremented") 6438 } 6439 6440 // Expire cache 6441 c.fetchStamp = time.Now().Add(-time.Minute * 5) // const defaultShelfLife 6442 r, err = c.rate(context.Background(), dex.Mainnet) 6443 if err != nil { 6444 t.Fatalf("fresh rate error: %v", err) 6445 } 6446 if r != okRate { 6447 t.Fatalf("expected fresh rate %d, got %d", okRate, r) 6448 } 6449 if n != 2 { 6450 t.Fatal("counter not incremented for fresh rate") 6451 } 6452 6453 c.fetchStamp = time.Time{} 6454 c.f = feeRateBad 6455 _, err = c.rate(context.Background(), dex.Mainnet) 6456 if err == nil { 6457 t.Fatal("error not propagated") 6458 } 6459 if n != 3 { 6460 t.Fatal("counter not incremented for error") 6461 } 6462 6463 // Again should hit error cache. 6464 c.f = feeRateOK 6465 _, err = c.rate(context.Background(), dex.Mainnet) 6466 if err == nil { 6467 t.Fatal("error not cached") 6468 } 6469 if n != 3 { 6470 t.Fatal("counter incremented for cached error") 6471 } 6472 6473 // expire error 6474 c.errorStamp = time.Now().Add(-time.Minute) // const errorDelay 6475 r, err = c.rate(context.Background(), dex.Mainnet) 6476 if err != nil { 6477 t.Fatalf("recovered rate error: %v", err) 6478 } 6479 if r != okRate { 6480 t.Fatalf("expected recovered rate %d, got %d", okRate, r) 6481 } 6482 if n != 4 { 6483 t.Fatal("counter not incremented for recovered rate") 6484 } 6485 }