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